From c40c772f7424c1fcb335753cb4d1253c004e3bd4 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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",
-- 
GitLab


From 99f4ce9e5afcc117a4bf610c49d512055b25ccec Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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"
-- 
GitLab


From b49d499ab66218cca8ba884bbca8c8d24dd75733 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 62c114e849f8dbc4082755d35d841826584162f3 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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",
-- 
GitLab


From e6becabe113ed76b682f2bc7e62d7fe09de21d76 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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,
-- 
GitLab


From b43e19a0c1cedad136a7b10ac6a13c403bb0ac8a Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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,
-- 
GitLab


From e3ed4eeb76c18ad594565597a66e5db55bb5e5b7 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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'",
         )
 
-- 
GitLab


From 14c68c8692bd9c1326698094f00bfec50c2cfed4 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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",
-- 
GitLab


From 63f2c4ab9856187c95325662cae950f349aef3d2 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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.",
-- 
GitLab


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
-- 
GitLab


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 <diogogomes@gmail.com>
---
 .../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]
-- 
GitLab


From 473d7c484d2bf830944162e04d40c86beb9a8b2d Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Thu, 29 Sep 2022 03:45:15 +0200
Subject: [PATCH 012/985] Improve deCONZ sensor classes (#79137)

---
 homeassistant/components/deconz/sensor.py | 324 +++++++++++-----------
 1 file changed, 158 insertions(+), 166 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 = ""
-
-
-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(
+    common: bool = False
+    name_suffix: str = ""
+    old_unique_id_suffix: str = ""
+
+
+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,
+    ),
+    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,
-        suffix="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(
+    DeconzSensorDescription[SensorResources](
         key="internal_temperature",
+        isinstance_fn=lambda device: isinstance(device, PydeconzSensorBase),
         value_fn=lambda device: device.internal_temperature,
-        suffix="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)]
+            )
-- 
GitLab


From 45ecddb9aa1b015e6276523c7a871603ba455d2c Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
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
-- 
GitLab


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."""
 
-- 
GitLab


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()
-- 
GitLab


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 <balloob@gmail.com>
---
 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
-- 
GitLab


From 21693b0022be231ca563906d90490310edec1f7f Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
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
-- 
GitLab


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,
     ),
-- 
GitLab


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",
-- 
GitLab


From a0357767ef0b1349064ee9564e64554af5519528 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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")
-- 
GitLab


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,
-- 
GitLab


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__(
-- 
GitLab


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(
-- 
GitLab


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(
-- 
GitLab


From a1c26cd4cd65ab834820c4b786a7e27275779609 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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",
 ]
-- 
GitLab


From d0ac1073a0f48f4d1c0dac62a8a8c1ddf1078a38 Mon Sep 17 00:00:00 2001
From: Rami Mosleh <engrbm87@gmail.com>
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:
-- 
GitLab


From e7764b8bf19027cbebf0e0e38178fa20037cdac1 Mon Sep 17 00:00:00 2001
From: Rami Mosleh <engrbm87@gmail.com>
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")
-- 
GitLab


From 616b85df31b205dcd606ab6ad4c4e2ded5ff6dae Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
         }
       }
     },
-- 
GitLab


From 0b5289f7483dde5911f4a268233fea2ce3b417ff Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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(
-- 
GitLab


From 75510b8e90162a5b7a530d36d141cbada3df644c Mon Sep 17 00:00:00 2001
From: Jafar Atili <at.jafar@outlook.com>
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:
 
-- 
GitLab


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:
-- 
GitLab


From db1797beb4abee166a689115813f3404d4cb3bf0 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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:
-- 
GitLab


From d742e65ef5ff8bfb76b7696a68230fdf77811f92 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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)
-- 
GitLab


From 11c09f4fd81ba7cf79333d16600c0d4d992a46a2 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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
-- 
GitLab


From 114db26fcf76d4f5294c5eb8dd8ef7a7191e36a4 Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


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]
-- 
GitLab


From b659a19f4aaa6107d242a42aa80c74eb2bc16b8f Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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)
-- 
GitLab


From ee32e0eb3f8017bc1fc5578d73ee753e059ed686 Mon Sep 17 00:00:00 2001
From: Dennis Schroer <dev@dennisschroer.nl>
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:
 
-- 
GitLab


From ba6a81c565f162c50166bf776b21b6a2271d7eef Mon Sep 17 00:00:00 2001
From: ehendrix23 <hendrix_erik@hotmail.com>
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 <erik@montnemery.com>
---
 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
+
+    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 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()
-- 
GitLab


From d7d6637d7960ed8e4992af27ec12f217256a01e9 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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
-- 
GitLab


From a01f18a3ac38933e3ffca9cb2f22a8bcfc97e74b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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,
         )
-- 
GitLab


From d5b966d942a07036451505a4236d61d2884c7b68 Mon Sep 17 00:00:00 2001
From: Vincent Giorgi <vincegio@users.noreply.github.com>
Date: Thu, 29 Sep 2022 21:55:45 +0200
Subject: [PATCH 042/985] Add Airthings BLE component (#77284)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
---
 .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"
-- 
GitLab


From 21b078eeb7db17b16517775fcdaa7df8f2372565 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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.",
-- 
GitLab


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(
                         {
-- 
GitLab


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 <paulus@home-assistant.io>
---
 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(
-- 
GitLab


From 6e893d9162331e09895b5b9be21af057c70b9e3d Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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):
-- 
GitLab


From 0001270badcfead18d48e004b0ff77eb6f065dcc Mon Sep 17 00:00:00 2001
From: Zack Barett <zackbarett@hey.com>
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",
-- 
GitLab


From bc2dffabc477ebb261a4ab312f587f5d15e90e91 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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,
         },
-- 
GitLab


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 }}-${{
-- 
GitLab


From 52cdae254c619c63883bce0aad9ed740d5d3c3c9 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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"
 
 
-- 
GitLab


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
-- 
GitLab


From ed044acca73d25cd4b1f06fbdf318ec1ba3cd7ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Huryn?= <michalhuryn@gmail.com>
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"
-- 
GitLab


From ca0cd19dc917db915d3a3cd4d4dc18622a6745e1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 6694d06b37e5710dccc674932c9ddb54a05eae0a Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 5cdf4220eeb67b5a5837e70f2cba153e68f86888 Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
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,
-- 
GitLab


From c70ca1572b6bb7080eac3f0d25ce67bdcc001ae8 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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,
     )
 
-- 
GitLab


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
 
 
-- 
GitLab


From 2ce837f588e9211083948ca243d93b9289c3e8ce Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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",
             },
-- 
GitLab


From 2a45738d97da799dde8021cf6bb85a317fb771fa Mon Sep 17 00:00:00 2001
From: Jan-Philipp Litza <jplitza@users.noreply.github.com>
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
-- 
GitLab


From fbcf6cb03c6de575e50ac23f59b6e6646a118cbc Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
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
-- 
GitLab


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:
-- 
GitLab


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)
-- 
GitLab


From bd5ec4e1981a273b54cee9989894a6bc9b7db87a Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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"
         },
-- 
GitLab


From 816af8573ff8576969eaa7358bc7021c2635a58a Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
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:
-- 
GitLab


From b27f0c70be35c82186a80d53351986711412e909 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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
-- 
GitLab


From 8ff12eacd41aa17b73a9aec5d9d345082999ea1d Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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
-- 
GitLab


From ec8901b9af19bc9df7d0ff43e946def6e7783d42 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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 | 73 ++++++++++--------
 tests/components/homekit/test_homekit.py     | 78 +++++++++++++++++++-
 2 files changed, 121 insertions(+), 30 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]
-
-        hass.config_entries.async_update_entry(entry, data=data, options=options)
-        return True
+        return False
 
-    return False
+    # 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]
+
+    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(
-- 
GitLab


From 062ee75de953986bec7537a9b477728b2eee64f0 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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(
-- 
GitLab


From 886e6365650295ce085080701e1067e9648e2879 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
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
-- 
GitLab


From 31ddf6cc31aaaf08a09cd6afa9eb830450d1817d Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
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
-- 
GitLab


From 9c9c8b324a47f81f690543b7c28957a0fb4de4ae Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
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)
-- 
GitLab


From f1f01429f47a12a367af7dac1bf216227541f691 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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,
+        )
-- 
GitLab


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 <frenck@frenck.nl>
---
 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(
-- 
GitLab


From e7724a6593cca35940f6f6298bb34e90c98ac1f6 Mon Sep 17 00:00:00 2001
From: Mick Vleeshouwer <mick@imick.nl>
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:
-- 
GitLab


From 4cfcf562b58b1d60075661ed78ec3cec7b3d1d96 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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
-- 
GitLab


From 2de273500e62f785cc8844f9922a012d57f6a57f Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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"<recorder.Events("
-            f"id={self.event_id}, type='{self.event_type}', "
-            f"origin_idx='{self.origin_idx}', time_fired='{self.time_fired}'"
-            f", data_id={self.data_id})>"
-        )
-
-    @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"<recorder.EventData("
-            f"id={self.data_id}, hash='{self.hash}', data='{self.shared_data}'"
-            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"<recorder.States("
-            f"id={self.state_id}, entity_id='{self.entity_id}', "
-            f"state='{self.state}', event_id='{self.event_id}', "
-            f"last_updated='{self.last_updated.isoformat(sep=' ', timespec='seconds')}', "
-            f"old_state_id={self.old_state_id}, attributes_id={self.attributes_id}"
-            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"<recorder.StateAttributes("
-            f"id={self.attributes_id}, hash='{self.hash}', attributes='{self.shared_attrs}'"
-            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"<recorder.RecorderRuns("
-            f"id={self.run_id}, start='{self.start.isoformat(sep=' ', timespec='seconds')}', "
-            f"end={end}, closed_incorrect={self.closed_incorrect}, "
-            f"created='{self.created.isoformat(sep=' ', timespec='seconds')}'"
-            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"<recorder.SchemaChanges("
-            f"id={self.change_id}, schema_version={self.schema_version}, "
-            f"changed='{self.changed.isoformat(sep=' ', timespec='seconds')}'"
-            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"<recorder.StatisticsRuns("
-            f"id={self.run_id}, start='{self.start.isoformat(sep=' ', timespec='seconds')}', "
-            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,
         },
-- 
GitLab


From 94c825cf4fd11632413081b639a67d9cbbb18f36 Mon Sep 17 00:00:00 2001
From: Hans Oischinger <hans.oischinger@gmail.com>
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",
-- 
GitLab


From 35fa73eee9a472b74f387b526055b80df53c7c10 Mon Sep 17 00:00:00 2001
From: Hans Oischinger <hans.oischinger@gmail.com>
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:
-- 
GitLab


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
 
-L_PER_GAL = VolumeConverter.convert(1, VOLUME_GALLONS, VOLUME_LITERS)
-KM_PER_MI = DistanceConverter.convert(1, LENGTH_MILES, LENGTH_KILOMETERS)
+_LOGGER = logging.getLogger(__name__)
 
-# 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]
+# Fuel consumption units
+FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS = "L/100km"
+FUEL_CONSUMPTION_MILES_PER_GALLON = "mi/gal"
 
-SENSOR_TYPE = "type"
-SENSOR_CLASS = "class"
-SENSOR_FIELD = "field"
-SENSOR_UNITS = "units"
+L_PER_GAL = VolumeConverter.convert(1, VOLUME_GALLONS, VOLUME_LITERS)
+KM_PER_MI = DistanceConverter.convert(1, LENGTH_MILES, LENGTH_KILOMETERS)
 
-# 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
-
-    @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
+        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 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 self.api_unit in LENGTH_UNITS:
-            return round(
-                self.hass.config.units.length(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 PRESSURE_UNITS
-            and self.hass.config.units == IMPERIAL_SYSTEM
-        ):
+        if unit in PRESSURE_UNITS and unit_system == 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
-- 
GitLab


From f64a4f4a954bc22e372b82816263e49bf65bce3b Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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
-- 
GitLab


From 083db97476d7e1f746f7aa3e68fe5b154fe42224 Mon Sep 17 00:00:00 2001
From: Steven Looman <steven.looman@gmail.com>
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
---
 homeassistant/components/motioneye/media_source.py |  8 +++++++-
 tests/components/motioneye/test_media_source.py    | 12 ++++++------
 2 files changed, 13 insertions(+), 7 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
@@ -251,13 +251,13 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None:
         "thumbnail": None,
         "children": [
             {
-                "title": "00-26-22.mp4",
+                "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-26-22.mp4"
+                    "/2021-04-25/00-02-27.mp4"
                 ),
                 "can_play": True,
                 "can_expand": False,
@@ -265,13 +265,13 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None:
                 "children_media_class": None,
             },
             {
-                "title": "00-36-49.mp4",
+                "title": "00-26-22.mp4",
                 "media_class": "video",
                 "media_content_type": "video/mp4",
                 "media_content_id": (
                     "media-source://motioneye"
                     f"/74565ad414754616000674c87bdc876c#{device.id}#movies#"
-                    "/2021-04-25/00-36-49.mp4"
+                    "/2021-04-25/00-26-22.mp4"
                 ),
                 "can_play": True,
                 "can_expand": False,
@@ -279,13 +279,13 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None:
                 "children_media_class": None,
             },
             {
-                "title": "00-02-27.mp4",
+                "title": "00-36-49.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"
+                    "/2021-04-25/00-36-49.mp4"
                 ),
                 "can_play": True,
                 "can_expand": False,
-- 
GitLab


From 944d70401160ff77ccf2e6d4620f35cb82c97d57 Mon Sep 17 00:00:00 2001
From: Matrix <justin@yosmart.com>
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
-- 
GitLab


From 50952f8a1c6dca146b58f4e4cb992f5f6e7d0972 Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
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
-- 
GitLab


From 5055d3ff4bfa79e298e74929ed3582185fe665b4 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
+    )
-- 
GitLab


From 13c8d22bafe74b3be5d039a0a12f23335195dd30 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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
-- 
GitLab


From d03553bbf0ca921ba1bc4c1c4867c5f39320508e Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
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,
+        )
-- 
GitLab


From dac60990ee037e0624d11e9795db4d458019de5a Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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:
-- 
GitLab


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)
-- 
GitLab


From d9191cf2f2db3198548cf8ddd52f6d45a6131365 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From ebc2a751d25bc88a436ec196c1e44f0b97e3d5d6 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 3e411935bbe07ebe0e7a9f5323734448486d75d7 Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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 <balloob@gmail.com>
---
 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()
 
-- 
GitLab


From 8a73795f5082f173159f1d7b78d5a70470c23c10 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 7b8b73f82682e5d276d464d915b76130e4a2f573 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
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,
-- 
GitLab


From 38a680c3eb8bdd7332c776765247fc48922e9598 Mon Sep 17 00:00:00 2001
From: Rami Mosleh <engrbm87@gmail.com>
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"}
-- 
GitLab


From f95b8ccc204bf8eac21c27f5b76760f8f60c4ca1 Mon Sep 17 00:00:00 2001
From: Jevgeni Kiski <yozik04@gmail.com>
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
-- 
GitLab


From 205ce2bac51d3e7e701b513708b5b4d2a3e5abe2 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
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(
-- 
GitLab


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.<br>&nbsp;<br>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.<br>&nbsp;<br>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.<br>&nbsp;<br>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.<br>&nbsp;<br>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.<br>&nbsp;<br>Das Beobachtungsgebiet kann frühestens 30 Tage nach der Grobreinigung des Ausbruchsbetriebes wieder aufgehoben werden. Hierüber wird der Landkreis Osterholz informieren.<br>&nbsp;<br>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"
   }
 ]
-- 
GitLab


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."""
 
-- 
GitLab


From 229e387a1d1984276dfeeff24ecceff9015dc3c0 Mon Sep 17 00:00:00 2001
From: Ryan Fleming <rfleming71@users.noreply.github.com>
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
-- 
GitLab


From 28809fc7fd04ee318c528ee7921a79877b3f58ea Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
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.
 
-- 
GitLab


From 2ea97324195c0294ec4a8bc3239321fab267587d Mon Sep 17 00:00:00 2001
From: Ryan Fleming <rfleming71@users.noreply.github.com>
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"
-- 
GitLab


From 547a63e3145bd54d392ed58aafc6bd60b265b49a Mon Sep 17 00:00:00 2001
From: "David F. Mulcahey" <david.mulcahey@me.com>
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"
-- 
GitLab


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,
-- 
GitLab


From 653620345c34db1f0ba31caffa8f5e096f3de637 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 069818940e5a16c412544e1e14e69b5d0964f157 Mon Sep 17 00:00:00 2001
From: Hung Nguyen <hungnguyenm@users.noreply.github.com>
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]
-- 
GitLab


From d58e16b9903247030bd683725e3dfb21e2205015 Mon Sep 17 00:00:00 2001
From: TheJulianJES <TheJulianJES@users.noreply.github.com>
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,
-- 
GitLab


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",
-- 
GitLab


From 14f60dc87162c1a005ee68f05db4a877f1305024 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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
-- 
GitLab


From f28e3fb46caf8689a1dd70e22d3197da9d24e954 Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 3b794038b1e439627b08a8a404313600e578195f Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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"
-- 
GitLab


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))
 
 
-- 
GitLab


From 12358f24464418698f72c888515b19d366576263 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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"
         },
-- 
GitLab


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
-- 
GitLab


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,
-        }
-- 
GitLab


From da960f6ed4e3b572f88f34a857c73720f30f1173 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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
         )
-- 
GitLab


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."""
-- 
GitLab


From 04315751992a71fd48d35d0b6a7372d6b3774664 Mon Sep 17 00:00:00 2001
From: Nyro <cedric@nyro.dev>
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
-- 
GitLab


From 61f0d0ea15d2facbf9b9402be957c1c24e301127 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
         }
-- 
GitLab


From 47b40e1e619d724adb7de44b45a4f6748ee8790d Mon Sep 17 00:00:00 2001
From: ehendrix23 <hendrix_erik@hotmail.com>
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."""
-- 
GitLab


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(
-- 
GitLab


From c05d3c10db6aa41c8972f0f0120550f5986a44c8 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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'.
-- 
GitLab


From 125c037deff4ab056aa28f6d28dba720a19a1a84 Mon Sep 17 00:00:00 2001
From: Hans Oischinger <hans.oischinger@gmail.com>
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
-- 
GitLab


From 0fdb7052e9c2cc34a59d8b65201ba4e3393de8a5 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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}")
-- 
GitLab


From cb909b4b05ba4c1732be022134d857d4c2991be3 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
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
-- 
GitLab


From aa3aa913584faf93d71096d8ab9367d996518314 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 40bdcc3ea7fe0cc0d5b662f305be33c08d048066 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
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
-- 
GitLab


From 55b036ec5ef570b146a6126408da929dd66e431e Mon Sep 17 00:00:00 2001
From: Ben Randall <veleek@gmail.com>
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 <homeassistant.components.blueprint.models.BlueprintInputs object at 0x7f581e097820>
```

**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 <homeassistant.components.blueprint.models.BlueprintInputs object at 0x7f581c0dc9d0>
```

* 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
     )
 
 
-- 
GitLab


From 3e3f7ea99590ead08bfd9d2a7de032644b830e4e Mon Sep 17 00:00:00 2001
From: Guido Schmitz <Shutgun@users.noreply.github.com>
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 | 43 ++++++-----
 .../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, 173 insertions(+), 137 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()
-
-        state = hass.states.get(state_key)
-        assert state is not None
-        assert state.state == STATE_ON
+    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
 
     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()
 
-- 
GitLab


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') %}
-- 
GitLab


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 <marhje52@gmail.com>
---
 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
-- 
GitLab


From e8650dd4b7261f269e8386379f6c30a069b9fc2f Mon Sep 17 00:00:00 2001
From: Jafar Atili <at.jafar@outlook.com>
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 <levyshay1@gmail.com>

* 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 <levyshay1@gmail.com>

* Update homeassistant/components/switchbee/climate.py

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* Update homeassistant/components/switchbee/climate.py

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* Update homeassistant/components/switchbee/climate.py

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* Update homeassistant/components/switchbee/climate.py

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* Update homeassistant/components/switchbee/climate.py

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* 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 <levyshay1@gmail.com>

* more fixes

Co-authored-by: Shay Levy <levyshay1@gmail.com>
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,
-- 
GitLab


From a2e3978d5312282bc50b2163b817ca738b560a6f Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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",
     [
-- 
GitLab


From bbaac01da5b25542bf3cdd31bc5fd87ab7a5c15d Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From cfda36ef36272dbaf43f41386194ba5915adfabb Mon Sep 17 00:00:00 2001
From: mbo18 <mbo18@users.noreply.github.com>
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,
     ),
-- 
GitLab


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
-- 
GitLab


From d08f7f952653eeb494fc9e480aad323389c9a106 Mon Sep 17 00:00:00 2001
From: Yuval Aboulafia <yuval.abou@gmail.com>
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
-- 
GitLab


From 27effc93ad4b3c11a215197878ae9262a1f3dcf8 Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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
-- 
GitLab


From 7eb101b0c7c0c551e60c4b92d4ffb48c6667c51e Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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",
-- 
GitLab


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
-- 
GitLab


From 90637a721c0a0890bfd1dbc34294bda19787df0a Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
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") == ""
-- 
GitLab


From 92ca95ca81f4d62510894cbb209b53eea7ee75f1 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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."""
-- 
GitLab


From 2768b2445a67e897e8d00bd9e0448a5f8f94987a Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From eda6f13f8a92127f547f26a8c3ef302f73dd2675 Mon Sep 17 00:00:00 2001
From: Nathan Spencer <natekspencer@gmail.com>
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, {})
-- 
GitLab


From e93deaa8aa7b0060e8a838bcbffc5d64835fed34 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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 | 188 +++------
 tests/components/sensor/test_recorder.py    | 415 +++++++++++---------
 2 files changed, 296 insertions(+), 307 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))
-
-        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,
+        # in a unit which can be converted, return the states as they are
+
+        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,44 +3146,57 @@ 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)
+    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": 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)
 
-    # Statistics has run, invalid state - expect error
-    await async_recorder_block_till_done(hass)
-    do_adhoc_statistics(hass, start=now)
+    # Valid state - empty response
     hass.states.async_set(
-        "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}}
+        "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}}
     )
     await async_recorder_block_till_done(hass)
-    await assert_validation_result(client, expected)
+    await assert_validation_result(client, {})
 
-    # Valid state - empty response
+    # 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": unit}}
+        "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}}
     )
     await async_recorder_block_till_done(hass)
     await assert_validation_result(client, {})
@@ -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)
+    hass.states.async_set(
+        "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,
+                    "metadata_unit": unit,
+                    "state_unit": "dogs",
                     "statistic_id": "sensor.test",
-                    "supported_unit": valid_units,
+                    "supported_unit": supported_unit,
                 },
-                "type": "unsupported_unit_metadata",
+                "type": "units_changed",
             }
         ],
     }
     await assert_validation_result(client, expected)
 
-    # Invalid state too, expect double errors
+    # 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": "dogs"}}
+        "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": {
-                    "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"],
-                    "state_unit": "dogs",
-                    "statistic_id": "sensor.test",
-                },
-                "type": "unsupported_unit_state",
-            },
+                "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": [
-- 
GitLab


From a3989b90fec1d09463f2b26c4622978ed6e86e69 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 8d3e3ee6e9ccc08950ca33ece4ea2b6eaa68844d Mon Sep 17 00:00:00 2001
From: MrAliFu <alikapucu@gmail.com>
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"
-- 
GitLab


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 }}-${{
-- 
GitLab


From c040a7a15254794c45b23f788c034f5b30de2a25 Mon Sep 17 00:00:00 2001
From: kpine <keith.pine@gmail.com>
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
 
-- 
GitLab


From 4a6d1fc7343283d8d2ad004abdf933d498fe4f43 Mon Sep 17 00:00:00 2001
From: Nathan Broadbent <git@ndbroadbent.com>
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
-- 
GitLab


From 9d2ba7c0080a4a2a498fa8117a46ff78153f3af2 Mon Sep 17 00:00:00 2001
From: Nathan Broadbent <git@ndbroadbent.com>
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,
-- 
GitLab


From 1907b8766601ecd0ee60e18a86eae0409b13f1bd Mon Sep 17 00:00:00 2001
From: Nathan Broadbent <git@ndbroadbent.com>
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:
-- 
GitLab


From d0f1cba4ea04dde04db16f10827f4ce2bc89201a Mon Sep 17 00:00:00 2001
From: Jafar Atili <at.jafar@outlook.com>
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:
-- 
GitLab


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"
-- 
GitLab


From 74a8472eed41fe8ff7277dccf0857defa05e5f8a Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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 | 1368 ++++++++++++---------
 48 files changed, 1066 insertions(+), 567 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"
-    },
-    "epsonworkforce": {
-      "config_flow": false,
-      "iot_class": "local_polling",
-      "name": "Epson Workforce"
+      "name": "Epson",
+      "integrations": {
+        "epson": {
+          "config_flow": true,
+          "iot_class": "local_polling",
+          "name": "Epson"
+        },
+        "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,12 +3505,7 @@
       "iot_class": "cloud_polling",
       "name": "PoolSense"
     },
-    "powerwall": {
-      "config_flow": true,
-      "iot_class": "local_polling",
-      "name": "Tesla Powerwall"
-    },
-    "profiler": {
+    "profiler": {
       "config_flow": true,
       "iot_class": null,
       "name": "Profiler"
@@ -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"
-    },
-    "telegram_bot": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
-      "name": "Telegram bot"
-    },
-    "tellduslive": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
-      "name": "Telldus Live"
+      "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"
+        }
+      }
     },
-    "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,
-- 
GitLab


From 2b27cfdabb0a3d0737cfe2fcb06dd591449ea4de Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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"
   ]
 }
-- 
GitLab


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   |  79 +++-----
 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, 112 insertions(+), 233 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
             )
-
-            # Clear the status so we can get a new one
-            self._finished_status = None
-            self.async_write_ha_state()
+        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()
+        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
-- 
GitLab


From 56dd0a6867bf16aeb1f4bf4dd5ddadd73955bc2d Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


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      | 195 ++++++++++--------
 homeassistant/components/bayesian/const.py    |  17 ++
 homeassistant/components/bayesian/helpers.py  |  69 +++++++
 homeassistant/components/bayesian/repairs.py  |  15 +-
 4 files changed, 193 insertions(+), 103 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_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(
                 {
-                    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
+                    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_PROBABILITY: round(self.probability, 2),
-            ATTR_PROBABILITY_THRESHOLD: self._probability_threshold,
+            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,
-- 
GitLab


From abc80d8245e8905535bb2321c043f305f76e8534 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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",
-- 
GitLab


From 9c97ebbcfe97599bea244df51bf52c73f5ff970f Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 051374d73e0f8ad8d7c70bf96bbb86451c30858f Mon Sep 17 00:00:00 2001
From: Mike Degatano <michael.degatano@gmail.com>
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",
-- 
GitLab


From 89c4bf6536322cb1ce1daec530351bab945fd73f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 8faecae34d9b45914eb7413383a8134c73234461 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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)
-- 
GitLab


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
-- 
GitLab


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,
-- 
GitLab


From 8d28da83ca4ac3a83617e0759fc17a559c7f288c Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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.",
-- 
GitLab


From 214c2934de9ac7a8e28a59044dff114c4e2dabfa Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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
-- 
GitLab


From 416c10a793a982fb8c17259d36b99be458131cd0 Mon Sep 17 00:00:00 2001
From: Mike Degatano <michael.degatano@gmail.com>
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",
-- 
GitLab


From bcbce6f1591ac005675fa2b720057b3fb20a071e Mon Sep 17 00:00:00 2001
From: kpine <keith.pine@gmail.com>
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)
-- 
GitLab


From 905950f341236d214a27d822b84b223b1985fe1f Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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",
-- 
GitLab


From 92c9ddf3e3374333ba29cf1ea8d78b2ef352c4b7 Mon Sep 17 00:00:00 2001
From: Jafar Atili <at.jafar@outlook.com>
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",
-- 
GitLab


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] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
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
-- 
GitLab


From 18033532caebbf4c9331cb7f9a29a535bc02ac01 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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},
-- 
GitLab


From 33bdc67a61f3be8bb10236c6dbf5742fda496830 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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
-- 
GitLab


From 9dd9147343acdf8b3707257de45053af1ec194e3 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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]}",
         }
 
 
-- 
GitLab


From 723b018b631a4c5ab9324c487daca5b23ac4d913 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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           | 145 ++++++-----------
 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, 252 insertions(+), 434 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", {})
-
-    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)
-
-    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,
-    )
-
-    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
+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:
+
+        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(),
+            initialize=AsyncMock(),
+            initialized=True,
+        )
+        block_device_mock.return_value = device
+        block_device_mock.return_value.mock_update = Mock(side_effect=update)
+
+        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", {})
-
-    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)
-
-    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)
-
-    wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][
-        RPC
-    ] = RpcDeviceWrapper(hass, config_entry, device)
-    wrapper.async_setup()
-
-    return wrapper
+async def mock_rpc_device():
+    """Mock rpc (Gen2, Websocket) device."""
+    with patch("aioshelly.rpc_device.RpcDevice.create") as rpc_device_mock:
+
+        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(),
+        )
+
+        rpc_device_mock.return_value = device
+        rpc_device_mock.return_value.mock_update = Mock(side_effect=update)
+
+        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):
-    """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()
+async def test_block_button(hass: HomeAssistant, mock_block_device):
+    """Test block device reboot button."""
+    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
-
-    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], "rollerPos", 0)
+    await init_integration(hass, 1)
 
-    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"
-    )
-    hass.async_create_task(
-        hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN)
+        mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light"
     )
-    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"],
-    )
-    hass.async_create_task(
-        hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, SWITCH_DOMAIN)
+        mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"]
     )
-    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):
-    """Test block device update entity."""
-    assert coap_wrapper
 
+async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch):
+    """Test block device update entity."""
     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()
-
-    # 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")
+    await init_integration(hass, 1)
 
-    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
-- 
GitLab


From 59d9d3de69ef521d8e733bb0b04181be3553d924 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
-- 
GitLab


From 4d3d22320fa0f3bd6abd109e0727e3bba9eab7c7 Mon Sep 17 00:00:00 2001
From: Jafar Atili <at.jafar@outlook.com>
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,
-- 
GitLab


From 22c68b95bf7047615e4aeac7a0cf4a9bd12f82cc Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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)
-- 
GitLab


From 312770dbac9565cfac4edbb5e715c70cecfab18f Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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,
         )
-- 
GitLab


From 5d7756885be0fd044d86e60ec0d2639f9d114ea3 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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 = (
         {
-- 
GitLab


From 0eb1101de84b6042380a0d2220da182493b2d206 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
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(
-- 
GitLab


From 41d2ab5b375564ccbc836736fa8896a758cffc2e Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 5674295b3cd50536a18f08c6baa72d651b5dbd46 Mon Sep 17 00:00:00 2001
From: Yuval Aboulafia <yuval.abou@gmail.com>
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
-- 
GitLab


From 558b327928d5ab08d8e97262dfbdbc13dc73056f Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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"
         },
-- 
GitLab


From c798723c2744a7b386c56de9627a31c349309bbf Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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",
-- 
GitLab


From ce6ccffd9c39c9c3cc9fad5751abd440fd84a82d Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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
 
-- 
GitLab


From 9b4c7f5dc576a23469a556fadfa1567d3f0abfba Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 93b2a6cc267da1b78233d75586263765c8161158 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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):
-- 
GitLab


From 1e39f42df5d75e4eeae7a0033beb4c3c0f23ea51 Mon Sep 17 00:00:00 2001
From: Tom Matheussen <tmatheussen@avia-gis.com>
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,
     }
 )
 
-- 
GitLab


From 47d0598e75487f63901931875f69f802a477df13 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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                      | 151 +++++++++---------
 7 files changed, 320 insertions(+), 130 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,
+    # 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, 128, 0), 2000, 6535) == (
+        6535,
         128,
     )
-    assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 255), 153, 500) == (
-        500,
+    # 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, 0, 128), 153, 500) == (
-        500,
+    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, 112, 143), 153, 500) == (
-        348,
+    # 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), 153, 500) == (
-        348,
+    assert color_util.rgbww_to_color_temperature((0, 0, 0, 56, 72), 2000, 6535) == (
+        2872,
         128,
     )
-    assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 0), 153, 500) == (
-        500,
+    # 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(
+    # Only cold channel enabled -> coldest color temperature
+    assert color_util._white_levels_to_color_temperature(255, 0, 2000, 6535) == (
+        6535,
         255,
-        0,
-        153,
-        500,
-    ) == (153, 255)
-    assert color_util.while_levels_to_color_temperature(128, 0, 153, 500) == (
-        153,
+    )
+    assert color_util._white_levels_to_color_temperature(128, 0, 2000, 6535) == (
+        6535,
         128,
     )
-    assert color_util.while_levels_to_color_temperature(0, 255, 153, 500) == (
-        500,
+    # 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(0, 128, 153, 500) == (
-        500,
+    assert color_util._white_levels_to_color_temperature(0, 128, 2000, 6535) == (
+        2000,
         128,
     )
-    assert color_util.while_levels_to_color_temperature(112, 143, 153, 500) == (
-        348,
+    assert color_util._white_levels_to_color_temperature(112, 143, 2000, 6535) == (
+        2876,
         255,
     )
-    assert color_util.while_levels_to_color_temperature(56, 72, 153, 500) == (
-        348,
+    assert color_util._white_levels_to_color_temperature(56, 72, 2000, 6535) == (
+        2872,
         128,
     )
-    assert color_util.while_levels_to_color_temperature(0, 0, 153, 500) == (
-        500,
+    # Both channels turned off -> warmest color temperature
+    assert color_util._white_levels_to_color_temperature(0, 0, 2000, 6535) == (
+        2000,
         0,
     )
-- 
GitLab


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(
-- 
GitLab


From df7b8f419eb020ed62a60032438cb0267cc8109b Mon Sep 17 00:00:00 2001
From: Matthew Simpson <matthew.simpson@freeagent.com>
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
-- 
GitLab


From 00029ca344b0e039e37b5af11c88e96285a7278f Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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
-- 
GitLab


From 61deb54ec84b3b732c5e17a6db1aa4f72232a421 Mon Sep 17 00:00:00 2001
From: Lars <flabbamann@users.noreply.github.com>
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:
-- 
GitLab


From e2c1a36e24ac36e2728d9699b30006a08774517b Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 0a59d37e624e0d476259cfbf3b6dad0e1f71168f Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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,
         },
     ]
-- 
GitLab


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      | 142 ++++++++++++++----
 4 files changed, 216 insertions(+), 65 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={}
         )
 
+    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 result1["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH
+    assert result2["step_id"] == "choose_serial_port"
+    assert result2["data_schema"]({})[CONF_DEVICE_PATH] == config_flow.CONF_MANUAL_PATH
 
-    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={}
     )
 
     # 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"
-- 
GitLab


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
-- 
GitLab


From d3fee8aad9871395464e71d369d0adebf32735ea Mon Sep 17 00:00:00 2001
From: Glenn Waters <glenn@watrs.ca>
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",
 ]
-- 
GitLab


From 96a8beb29f44ab50d7e475da2cca098bd7df5235 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
-- 
GitLab


From aa5575ba653a72808fe4e6ac3a465d895524df71 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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.
 
-- 
GitLab


From 51e6d49451924df800bccbcf87137be930eea505 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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 (
-- 
GitLab


From 6111fb38a7397661d16536a05de1357a843ce7d0 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


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 <davet2001@users.noreply.github.com>
---
 .../components/generic/config_flow.py         |  97 ++++++++++--
 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, 217 insertions(+), 42 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"
-
-                    return self.async_create_entry(
-                        title=name, data={}, options=user_input
-                    )
+                    self.user_input = user_input
+                    self.title = name
+
+                    # 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"] == {
-- 
GitLab


From bba7b3b2be0514836f1e09a0a0aa1d1ae226247a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= <slovdahl@hibox.fi>
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:
-- 
GitLab


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 \
-- 
GitLab


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
-- 
GitLab


From eb19927df61fbe7717476ebfe026a241c2899ff5 Mon Sep 17 00:00:00 2001
From: Fredrik Erlandsson <fredrik.e@gmail.com>
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
-- 
GitLab


From 7b2cad388e03e31a04f97096bd5359c39d774e48 Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
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,
-- 
GitLab


From e1047320a9153687d8edb203c5adf7734a8b02a4 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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,9 +10,11 @@
             "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",
@@ -20,6 +22,26 @@
                 },
                 "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.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"
-- 
GitLab


From 22d6ce967d8ddf2ef0a6edb92b3341233498463a Mon Sep 17 00:00:00 2001
From: Jeef <jeeftor@users.noreply.github.com>
Date: Thu, 6 Oct 2022 19:09:38 -0600
Subject: [PATCH 214/985] Add Flume binary sensors (#77327)

Co-authored-by: J. Nick Koston <nick@koston.org>
---
 .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]
+    ]
-- 
GitLab


From 07d4ac42d429e44f7618e1e7c6462613ef8d3f09 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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 = [
-- 
GitLab


From 5694a4bfc8d90ee5a169dc3d5a95e5b280068f48 Mon Sep 17 00:00:00 2001
From: jjlawren <jjlawren@users.noreply.github.com>
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:
-- 
GitLab


From 633ffad4438fd415e4a8272c9797793b98b6c5a8 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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 <marhje52@gmail.com>
---
 .../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,
+        },
+    }
-- 
GitLab


From 90d39a414c2f42cab0d53d3288d8486282683a1a Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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)
-- 
GitLab


From 9a81b658153d3d9cd5e3a055c3f0ddb537144731 Mon Sep 17 00:00:00 2001
From: taiyeoguns <taiyeoguns@yahoo.com>
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."""
+def add_entities(devices):
+    """Mock add devices."""
+    for device in devices:
+        DEVICES.append(device)
 
-    # pylint: disable=invalid-name
-    DEVICES = []
 
-    def add_entities(self, devices):
-        """Mock add devices."""
-        for device in devices:
-            self.DEVICES.append(device)
+@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
 
-    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)
+    kira.setup_platform(hass, TEST_CONFIG, add_entities, DISCOVERY_INFO)
+    assert len(DEVICES) == 1
+    sensor = DEVICES[0]
 
-    # 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]
+    assert sensor.name == "kira"
 
-        assert sensor.name == "kira"
+    sensor.hass = hass
 
-        sensor.hass = self.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)
+    mock_schedule_update_ha_state.assert_called
 
-        assert sensor.state == codeName
-        assert sensor.extra_state_attributes == {kira.CONF_DEVICE: deviceName}
+    assert sensor.state == codeName
+    assert sensor.extra_state_attributes == {kira.CONF_DEVICE: deviceName}
-- 
GitLab


From aee82e2b3babc8e22986b390d841a6072235496f Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
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 (
+        "<Event call_service[L]: domain=mqtt, service=reload, service_data=>"
+        in caplog.text
+    )
 
 
 async def test_disable_birth_will(
-- 
GitLab


From 9b44cf01278a1122e170b2fec0c64fb45d1acd74 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi>
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"<error><code>{LoginErrorEnum.USERNAME_WRONG}</code><message/></error>",
+            },
+            {},
+            {CONF_USERNAME: "incorrect_username"},
+        ),
+        (
+            {
+                "text": f"<error><code>{LoginErrorEnum.PASSWORD_WRONG}</code><message/></error>",
+            },
+            {},
+            {CONF_PASSWORD: "incorrect_password"},
+        ),
+        (
+            {
+                "text": f"<error><code>{LoginErrorEnum.USERNAME_PWD_WRONG}</code><message/></error>",
+            },
+            {},
             {CONF_USERNAME: "invalid_auth"},
         ),
-        (LoginErrorEnum.USERNAME_PWD_OVERRUN, {"base": "login_attempts_exceeded"}),
-        (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}),
+        (
+            {
+                "text": f"<error><code>{LoginErrorEnum.USERNAME_PWD_OVERRUN}</code><message/></error>",
+            },
+            {},
+            {"base": "login_attempts_exceeded"},
+        ),
+        (
+            {
+                "text": f"<error><code>{ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN}</code><message/></error>",
+            },
+            {},
+            {"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"<error><code>{code}</code><message/></error>",
+        **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"),
+    (
+        (
+            "<response>OK</response>",
+            {
+                "type": data_entry_flow.FlowResultType.ABORT,
+                "reason": "reauth_successful",
+            },
+            FIXTURE_USER_INPUT,
+        ),
+        (
+            f"<error><code>{LoginErrorEnum.PASSWORD_WRONG}</code><message/></error>",
+            {
+                "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):
-- 
GitLab


From b51c434b9d2f4a426d701f2ba26df2f9c88536f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Del=20Rinc=C3=B3n=20L=C3=B3pez?=
 <alexdrl@outlook.com>
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 <borky-git@protonmail.com>
---
 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,
-- 
GitLab


From 43091a98562de0a4ccb689a034ad693f92a4c129 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
     )
 
 
-- 
GitLab


From b450514fb3ae8eaf772970eb1309f72578397703 Mon Sep 17 00:00:00 2001
From: starkillerOG <starkiller.og@gmail.com>
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 = [
-- 
GitLab


From 0b9d02935076a7c78c7384392a30d3aa0e5bce7f Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From e45701fe89e4ce523fcd0ccb9a0abd07868eda90 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"],
-- 
GitLab


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
-- 
GitLab


From e1d3ba6ff11d4a7ece3fa4a822a0d4e9582a98ef Mon Sep 17 00:00:00 2001
From: borky <borky-git@protonmail.com>
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)
     ],
-- 
GitLab


From 9850709b37fdfa704ac3db4c45a2660880a7ca65 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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], StateType | datetime]
+
 
-    value_fn: Callable[[SkybellDevice], Any] = lambda val: val
+@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
-- 
GitLab


From 04f07cecba22a6ee31ca6d7a459d646a858b8293 Mon Sep 17 00:00:00 2001
From: Jeef <jeeftor@users.noreply.github.com>
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()
-- 
GitLab


From 59818649922bf1f44282d6714fed37fb1005eef9 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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
-- 
GitLab


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
-- 
GitLab


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
-- 
GitLab


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
-- 
GitLab


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
-- 
GitLab


From a18a0b39dd2e6599741633c27ee8c43b488cc460 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
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
-- 
GitLab


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,
-- 
GitLab


From fca8586fb6e130c602cfcb11fbbea376575c3b0d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 61901a1a60343c42385fd71274a31159f61da7cf Mon Sep 17 00:00:00 2001
From: Julian Einwag <jeinwag@users.noreply.github.com>
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 <Kane610@users.noreply.github.com>

* Update homeassistant/components/deconz/device_trigger.py

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>

* clarify it's a button, remove turn on event

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
---
 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,
-- 
GitLab


From 87a22fbccad9e9263d5e074aa0ca5681c56d9c53 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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     | 243 +++++-------------
 .../sonarr/fixtures/system-status.json        |   1 +
 6 files changed, 262 insertions(+), 234 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 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 collections.abc import Callable
+from dataclasses import dataclass
+from typing import Any, Generic
+
+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
-    ]
-    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
+    coordinators: dict[str, SonarrDataUpdateCoordinator[Any]] = hass.data[DOMAIN][
+        entry.entry_id
     ]
+    async_add_entities(
+        SonarrSensor(coordinators[coordinator_type], description)
+        for coordinator_type, description in SENSOR_TYPES.items()
+    )
 
-    async_add_entities(entities, True)
 
-
-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,
-- 
GitLab


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:
-- 
GitLab


From 4ff26b4ddd4070b2fca5303ebde49751be693273 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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
-- 
GitLab


From 2452e70e291f3257ced69019cacd5d31f9afb201 Mon Sep 17 00:00:00 2001
From: Brandon Rothweiler <brandonrothweiler@gmail.com>
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,
-- 
GitLab


From 24e9f6285da149ef12b3b46df85d92a1837c0992 Mon Sep 17 00:00:00 2001
From: Philippe Schenker <dev@pschenker.ch>
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
-- 
GitLab


From 5abff314371d4cb5f3d2c1668776b01cb4cac89f Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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
-- 
GitLab


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"
-- 
GitLab


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:
-- 
GitLab


From f58e1513e25c1982c487ae5dcd64aec4c8a99c26 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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"
-- 
GitLab


From c6f28f6d59d9be13a49ba28b396898e8e571cdab Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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,
-- 
GitLab


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,
-- 
GitLab


From e7b550685e451f830ebb1069c720c73381e6befb Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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
-- 
GitLab


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
-- 
GitLab


From 6546bba2330e2b46243491a7eaaa144ebfd5841e Mon Sep 17 00:00:00 2001
From: Bert Melis <bertmelis@users.noreply.github.com>
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"],'
-- 
GitLab


From 5dde93b429ea3ecd594abc271416b3434b517661 Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
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
-- 
GitLab


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
-- 
GitLab


From 4baba777801765b0ce9025c9ef170d3465d874fc Mon Sep 17 00:00:00 2001
From: Patrick ZAJDA <patrick@zajda.fr>
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,
     ),
-- 
GitLab


From 506695fdc5c57583b3c58beaa111d55c05ccc258 Mon Sep 17 00:00:00 2001
From: John Levermore <zhibek@users.noreply.github.com>
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",
 ]
-- 
GitLab


From 6010672e2f02692550222da196de1761f4849db9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi>
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")
-- 
GitLab


From d06e064e9ea7a21b79a23de1f5093d928520a036 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= <github@dahoiv.net>
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 <github@dahoiv.net>

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
---
 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(
-- 
GitLab


From 7bc2d97aca4f364e8d2d692ef38a8759414a1bae Mon Sep 17 00:00:00 2001
From: starkillerOG <starkiller.og@gmail.com>
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",
 ]
-- 
GitLab


From c81bf1103fa84d325a5571b3c50ad579b285faa4 Mon Sep 17 00:00:00 2001
From: starkillerOG <starkiller.og@gmail.com>
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"
-- 
GitLab


From f65dcf3c35a8e863623fb6ac0e430aad3f3d64f5 Mon Sep 17 00:00:00 2001
From: majuss <biomajuss@gmail.com>
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
-- 
GitLab


From 9019fcb5c54a4e5dd067479fd1109392732d400e Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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,
-- 
GitLab


From 1e13433d4f942a0e71c84eaae0259aaec521b31d Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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 | 344 ++++++++++-----------
 1 file changed, 171 insertions(+), 173 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,356 +28,351 @@ 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__)
 
 
-async def async_setup_entry(
-    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
-) -> None:
-    """Add Brother entities from a config_entry."""
-    coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id]
-
-    # Due to the change of the attribute name of one sensor, it is necessary to migrate
-    # the unique_id to the new one.
-    entity_registry = er.async_get(hass)
-    old_unique_id = f"{coordinator.data.serial.lower()}_b/w_counter"
-    if entity_id := entity_registry.async_get_entity_id(
-        PLATFORM, DOMAIN, old_unique_id
-    ):
-        new_unique_id = f"{coordinator.data.serial.lower()}_bw_counter"
-        _LOGGER.debug(
-            "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
-            entity_id,
-            old_unique_id,
-            new_unique_id,
-        )
-        entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
-
-    sensors = []
-
-    device_info = DeviceInfo(
-        configuration_url=f"http://{entry.data[CONF_HOST]}/",
-        identifiers={(DOMAIN, coordinator.data.serial)},
-        manufacturer=ATTR_MANUFACTURER,
-        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)
-            )
-    async_add_entities(sensors, False)
-
-
-class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
-    """Define an Brother Printer sensor."""
-
-    _attr_has_entity_name = True
-
-    def __init__(
-        self,
-        coordinator: BrotherDataUpdateCoordinator,
-        description: BrotherSensorEntityDescription,
-        device_info: DeviceInfo,
-    ) -> None:
-        """Initialize."""
-        super().__init__(coordinator)
-        self._attrs: dict[str, Any] = {}
-        self._attr_device_info = device_info
-        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)
-        )
-
-    @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."""
+@dataclass
+class BrotherSensorRequiredKeysMixin:
+    """Class for Brother entity required keys."""
 
-    @property
-    def native_value(self) -> datetime:
-        """Return the state."""
-        return cast(
-            datetime, getattr(self.coordinator.data, self.entity_description.key)
-        )
+    value: Callable[[BrotherSensors], StateType | datetime]
+    extra_state_attrs: Callable[[BrotherSensors], dict[str, Any]]
 
 
 @dataclass
-class BrotherSensorEntityDescription(SensorEntityDescription):
+class BrotherSensorEntityDescription(
+    SensorEntityDescription, BrotherSensorRequiredKeysMixin
+):
     """A class that describes sensor entities."""
 
-    entity_class: type[BrotherPrinterSensor] = BrotherPrinterSensor
-
 
 SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
     BrotherSensorEntityDescription(
-        key=ATTR_STATUS,
+        key="status",
         icon="mdi:printer",
         name="Status",
         entity_category=EntityCategory.DIAGNOSTIC,
+        value=lambda data: data.status,
+        extra_state_attrs=lambda _: {},
     ),
     BrotherSensorEntityDescription(
-        key=ATTR_PAGE_COUNTER,
+        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=ATTR_BW_COUNTER,
+        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=ATTR_COLOR_COUNTER,
+        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=ATTR_DUPLEX_COUNTER,
+        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=ATTR_DRUM_REMAINING_LIFE,
+        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=ATTR_BLACK_DRUM_REMAINING_LIFE,
+        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=ATTR_CYAN_DRUM_REMAINING_LIFE,
+        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=ATTR_MAGENTA_DRUM_REMAINING_LIFE,
+        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=ATTR_YELLOW_DRUM_REMAINING_LIFE,
+        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=ATTR_BELT_UNIT_REMAINING_LIFE,
+        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=ATTR_FUSER_REMAINING_LIFE,
+        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=ATTR_LASER_REMAINING_LIFE,
+        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=ATTR_PF_KIT_1_REMAINING_LIFE,
+        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=ATTR_PF_KIT_MP_REMAINING_LIFE,
+        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=ATTR_BLACK_TONER_REMAINING,
+        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=ATTR_CYAN_TONER_REMAINING,
+        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=ATTR_MAGENTA_TONER_REMAINING,
+        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=ATTR_YELLOW_TONER_REMAINING,
+        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=ATTR_BLACK_INK_REMAINING,
+        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=ATTR_CYAN_INK_REMAINING,
+        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=ATTR_MAGENTA_INK_REMAINING,
+        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=ATTR_YELLOW_INK_REMAINING,
+        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=ATTR_UPTIME,
+        key="uptime",
         name="Uptime",
         entity_registry_enabled_default=False,
         device_class=SensorDeviceClass.TIMESTAMP,
         entity_category=EntityCategory.DIAGNOSTIC,
-        entity_class=BrotherPrinterUptimeSensor,
+        value=lambda data: data.uptime,
+        extra_state_attrs=lambda _: {},
     ),
 )
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+    """Add Brother entities from a config_entry."""
+    coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id]
+
+    # Due to the change of the attribute name of one sensor, it is necessary to migrate
+    # the unique_id to the new one.
+    entity_registry = er.async_get(hass)
+    old_unique_id = f"{coordinator.data.serial.lower()}_b/w_counter"
+    if entity_id := entity_registry.async_get_entity_id(
+        PLATFORM, DOMAIN, old_unique_id
+    ):
+        new_unique_id = f"{coordinator.data.serial.lower()}_bw_counter"
+        _LOGGER.debug(
+            "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
+            entity_id,
+            old_unique_id,
+            new_unique_id,
+        )
+        entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
+
+    sensors = []
+
+    device_info = DeviceInfo(
+        configuration_url=f"http://{entry.data[CONF_HOST]}/",
+        identifiers={(DOMAIN, coordinator.data.serial)},
+        manufacturer="Brother",
+        model=coordinator.data.model,
+        name=coordinator.data.model,
+        sw_version=coordinator.data.firmware,
+    )
+
+    for description in SENSOR_TYPES:
+        if description.value(coordinator.data) is not None:
+            sensors.append(BrotherPrinterSensor(coordinator, description, device_info))
+    async_add_entities(sensors, False)
+
+
+class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
+    """Define an Brother Printer sensor."""
+
+    _attr_has_entity_name = True
+    entity_description: BrotherSensorEntityDescription
+
+    def __init__(
+        self,
+        coordinator: BrotherDataUpdateCoordinator,
+        description: BrotherSensorEntityDescription,
+        device_info: DeviceInfo,
+    ) -> None:
+        """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
+
+    @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
+        )
+        self.async_write_ha_state()
-- 
GitLab


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
-- 
GitLab


From 50911af835ba267d2ef3848e07ade40a03c4ad1b Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From b4d525f6a37a59ac7ea45c9408f6650d61f9f6fe Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From 5e32fdff266b546a6190a898fbab375051e01448 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From d90359b4246877bfdbff0e985d4110e9ad4d57ab Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From 63379bcafff48d4e2d9ea34a93eb38a126b977dd Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From 33d2bec6f1e195752b175bc164ae83438347925b Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From 6297a28507ce5b77d7ab36511c2aa345fc7ac5b9 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From 8471a71b60de3fa5974a8871e359ace96ade82f3 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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"
-- 
GitLab


From 132ff2c41055fbff78afe2fe553cce932a052a9d Mon Sep 17 00:00:00 2001
From: Mick Vleeshouwer <mick@imick.nl>
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(
-- 
GitLab


From d9d614d97f58143392688b05479f05fdb97ace2e Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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
-- 
GitLab


From ed565a6eb12ecc100ebc78a4892379eac1f183b5 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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"
-- 
GitLab


From 2decb85ee61ad16aae376edd15e179de842e54a1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 5b0a37a44752edbbf785d6a200e3b7a3f5fa2047 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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."""
-    jellyfin = Jellyfin()
-    client = jellyfin.get_client()
-    _setup_client(client)
-    return client
+    if device_name is None:
+        device_name = socket.gethostname()
 
+    jellyfin = Jellyfin()
 
-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 = jellyfin.get_client()
+    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,
+    }
-- 
GitLab


From 6eb2c96d32b57d7afa897cdec0e326c112ec8dff Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
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
-- 
GitLab


From 8176400cfd8985213dc6f6bb02d7ef840db168a0 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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"
-- 
GitLab


From 031370358cb3a137a946116cdeebb8e0bf807627 Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
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
-- 
GitLab


From 3126762707f49775c78b9beb484dff17bf6941cb Mon Sep 17 00:00:00 2001
From: TheJulianJES <TheJulianJES@users.noreply.github.com>
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"],
-- 
GitLab


From a2ed7f7679ab849bf891ea423a056f30b122542e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi>
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"
 }
-- 
GitLab


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
 
-- 
GitLab


From 77864ad80f22221ca0381a324da17bc3a3e46a83 Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
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
-- 
GitLab


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
-- 
GitLab


From 618f259fd8c07c32087662ef30c5aff7a3be4cf0 Mon Sep 17 00:00:00 2001
From: Jeef <jeeftor@users.noreply.github.com>
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,
         )
-- 
GitLab


From b7e84543c1d5265b51dbebe4a9cc87f1e5322a6b Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
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 <nick@koston.org>
---
 .../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
 
-- 
GitLab


From 5a0609ae8b39cf0d5a516b139aaf0565f98ac743 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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"
-- 
GitLab


From 45a30546ecf7ad5a9d8e762896679348e3b00588 Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
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)
-- 
GitLab


From 41595b0cbad2d2cde7b5999a02502ebf97d56400 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
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()
 
 
-- 
GitLab


From 7a1939c6082da04362ddebbe0535f50d027a91c4 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From d53499c0bf8b8529804ac8817521f9a44bf01e78 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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
-- 
GitLab


From aca340de1c7a7367cdf5f435338e6fad93e8154e Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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"
-- 
GitLab


From 58d531841bc2197616e7285d0561a9935d9bda73 Mon Sep 17 00:00:00 2001
From: Charles Garwood <cgarwood@gmail.com>
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(
-- 
GitLab


From ef719cf7ef645da46006814bad055841a0925383 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 84acb416b80c84610b71e517954ff80c4b50b6bb Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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"],
-- 
GitLab


From 7fae85ee8566052d000d434bbf6954e23b5c664e Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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
-- 
GitLab


From 575501d26ad867db5ebc1511504253176b7c8547 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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)
-- 
GitLab


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] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
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"
-- 
GitLab


From 907af7ffe46252a4f809a779aa96cd9f64be45cf Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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",
-- 
GitLab


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))
-- 
GitLab


From 8fe504356a8c2dcabad80a1dae54aea8e8edd732 Mon Sep 17 00:00:00 2001
From: Michael Molisani <molisani@users.noreply.github.com>
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
-- 
GitLab


From ffb6434776bb6b675bcd3a6983a1a0756e12aa67 Mon Sep 17 00:00:00 2001
From: Charles Garwood <cgarwood@gmail.com>
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
-- 
GitLab


From c7b56f4079ac89c3d0d3d152be20d9840a08b5d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Flemming=20S=C3=B8rvollen=20Skaret?= <flemmingss@gmail.com>
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",
-- 
GitLab


From d0bffb6c75def1caaada9b0c4a8b279f6d811a74 Mon Sep 17 00:00:00 2001
From: Patrick ZAJDA <patrick@zajda.fr>
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 <frenck@frenck.nl>
---
 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)
-- 
GitLab


From f8f4b059a158b870edd568f52635b172beeaae86 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


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
 
 
-- 
GitLab


From 1e5908d3a86c7ee000fe5e500b3dffc2057d4bd6 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From 2ee6ea9877a33e4cbc6d85de3177f15bb77ed5d2 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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":
-- 
GitLab


From 7b247a79cf93260a6d62534d0b7797ee608d113a Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
 
-- 
GitLab


From 06b1a4c2b440b33cecf807c2126951ecc308e834 Mon Sep 17 00:00:00 2001
From: rappenze <rappenze@yahoo.com>
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
 
-- 
GitLab


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,
         ),
-- 
GitLab


From 1d10822cef9de32ec4ded9bdf27bb84e0a144490 Mon Sep 17 00:00:00 2001
From: Khole <kjtech6@outlook.com>
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
-- 
GitLab


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
zcmcG#byOQ)^e;+riWhe)?hYk51SxKX;u<LKNwMNk+#QOS7Iz6Q!Gp8~in|l6xIDhU
zd+%Czy}#efnyks>oPFlVoIQK*&u2$#X(-{orhJWrgoLZ2te}I0gyMmOgnWUC{_>BW
znlRhT>6NFBk{nX?B=!Ew1u95ZT^0$cE&=<&67A(0%T3wH6A1~g|G)1mnCo{NB&6q3
z6$M#6u-Q=_+84dw*D$yfqTb85h4E|1>#xf+^TEr@(aU!&_n;Q5n6JT@ufMY`FXxS2
zO+E*nj4uXANOX48qU;tY<Uua(mc>^6COhX+Qc^Zaq|IFSVIBn}iqW_Uu+3+^Cqa8D
z-P9kl7g<Gm(%Eq0(mSjSc%L@MRxZx4`8DX;Tv1zY(*|(8+T;CSsmFh{50w6=_ViYR
zvLVCnBzRKi<#V^7zUHgAkc2gEufd|Gs><<5-5QrtV<=R>u4;pPk(g5R->=_<nUT5v
zrMGI@KjM1;TU$RZZpUw_;&m8yxoifuwrobm#~U&9iJ3))<=fiYbWBW4c&tNq1QjbN
z|C9b#{GVWf*6sL+<2u`fyG8^T2S;*jEY3vi*Eg(NH0K16k&zF0|8?}!lz$o9?}YzE
z1N*icl;5`b%3ygAd^8|djHY|37Y#sbY_9$C|8Ba+pS0>5=WY^{H7P0S-5lFj9g5-i
zTkO8TUoXw+$NW!o|I-s9t^&oPT)JEu8s_h8s@wFuXk)Se-S^=Cy06@JgInswF#A`X
zf2|kk|H;z+U!yQQHwe$K$mS<0{e+p3@jsLE?Z4sCO@O}YJPxE}cttTNY7_s@k&0R@
zM<=fMGHDhN)&C}+`+t^d`oA%1Rzg%IJ-!yP{jUQn;`K|#|05<Jp8J0aR6`2oH2!x1
zF>|jLL;3%f_?m3-JyG|6Nf0Zl@aetu_<sw%CFj`sKLy%va7h0Dy-lB%jt(Q4xkp;A
z$ANh1*gc*9#<Twa^w@Us+y9q+2c}`Sy{S=lsr#_AlR7+A8h$<;3utq($~U=zlZrnh
zxIKP{0osCqUH-MLT4hcgNfU~UcAo#uYQ@>s_V!3LYd%}&=YPqdf`C67nrCaF_^JuC
zl3U|~@YH{Yb7MDWo3?Pt>&CHG_f4DGGIfth@!fgnleS&j4*1WSu1^W>M|Ch&7nf?a
ze1Or_YEaPrVoQzx#f0cn1N-QeQefe5r7iRM!zr@(a~eSWyw0S-DGA$rEF~k5Cl&4I
z`y-LBMG9r3VJ6W=e@MZ$N9|U(-3w^vYnT4FLzrz8z{)CKwmK`^&gQmSg&h};^z||9
z?TReO`Fy4St*v)b<?!|2Rg%HOe;?(Qw24d0&K5ugMwVCI&ySk4fj5>};+|!X5?*^+
zM)fw){-?bd9%I}s!!LEXcMF2LOxy=ae3qBmJhxv4l3bquS(QaClU_aor_PE;P*B&*
z|4gy%?%=0w_v2NHV@;<)b=wXlI}guQfwp!tzWttg{UoPrELSR(^l%366HjuuqiV^w
zVMo%W+n=gi4esF{w^X-wImkxsEA!!>$T?IEe<%!ZQ|0&_3|T>3wF{vlH8OS-g$8dF
zPndLhN7>Wy$%uz@uwUCg+*yt2-UmdYZ`uxB?w=!W&YIIcz6_3~&VHJ4!FM&FRI@-T
zIWLd<r$JS{Ri%6+-b{rqE9ZGSuQ^XBI&LykyqMgdYEw?fKb1*W$BXY5o1II2j{o|Y
ze*Bnyc{m4m2_+wxo$K&*^;-%F3HeUd@ERN2b~0DoomWwDC<G4~mM0z5MUsy>BRDB0
zf6{gDk|pAt{m4yxM05l=vF!~(tA||Jjlkh8xW2tT4HQo1q=DG52wbX=6IkE9ZZqx}
zfqOT}@Z2UgGplrCT&8Q-@`q>Q5yx)Q;i9Hb#2l((fHFSe?eBAxQ+l-G;^NKq(dpcS
zj6qeL>2wCfqpyqmLefSmRA@((ucDNo2AA8BQKgl62T7qhC8L3HDa;aso8~`{l${pp
z6<7RrO^sUJf=zsGw`ZV=*w4Jt7(=R6ADb$B(^Iz`1$)I}SPR$}@m3Z}Gfb^xVYpAi
zj4)ci)ZD<oBTufQ%{GoZf&ho|JfG~6k`m?XlV#iaTFaRRkmfC-`*~}xIH8##t)il$
z-f2OAie<$6;e3pD=7&Bf=SQE_0N>k_4#bg^Zn;JpQvzVdOnQ+C39GYGw>&<UoWJk$
z9xZt7)fu>NE?m%VycUMd9MzYUo9i^Fe)y;A(a|6J?~k!TBe!XLV+be_v{Y~V9`drK
zw!Rm~pVx2~LoRmeJ+@V!Tc%EP59VrkQP6P`Gt54#P>MM1JrBZ&Gyx2v%U&tZ#t>S+
zBmTCN_QQu&j*!TUQO+8iJ2jsb2<Y~DHHb;-Ln$C_0(7g$Sd+k(==#iEztUo_tVt+t
zHAgINjnJ#eDMh?rcyiwr90iS5DBJ3nhEW$C!J7sJoQw)9Ma-8J?aByUXzEi+-}@vB
zX<W}EFx!wD;6MCp#z)wbjB;XZbuDj0n$rp0Fv$6R?PrNcRIh6Ee8>qr5M7V)D&rU1
z=bY7vq(c_Udd(5RBWL#71){E9<Jpo*1~nh8FD&!^HkAh4H_sG<@KxTcvr7hf%iQ*Q
zJN<<&keAQeP8Ts>J?W@1=+y*DIxLnod7Uh&2nfvle0Z1ZSz}nIQwGm;z}eVNF34~#
zfV?QC65VpVp(qyz>;W)*;>?=R!>J%AgLSriBK7uyY&as0s%rXgi96H&N^wr3)AW+r
zdvD6*$@@(HfHIZHm`dd}N=o5RY^LpA$uE25E6hY*N7yv<!YrNJm_fH(Jtd9PKt<NF
za0K6I=gF9y@R$C<@1MrZ@IsCY3Rs_C^P)?~?Of_^7?DOT+yl{heN|II8wCX4?Uz?6
zJI+<-1v$H&?l2$)@8TupS2UP@OWbT*cI(GAC=o~IIW?T@0w6&75FiiqRPKKH6+>jk
z%mQABX~Mnl6@yUPQ!t*U)U8cLA~uvajGEzlp<5U=su@f};<!YP2ah8MZpZa+G>&dd
z)g?GcF;!^zYS<Z8Hp1iFoG+`Kr^<wm%LN`~m!J}Lak@|e0waPr6)QeB|43xw@|YR2
zx^>eotQ;WPHHTv@&Z#E%=aedURK7hldM^lB37`lEjyv;Qjc%CKJk-}uMcCF)7Ha%Z
zig(KAzz`8Z|DN6?LCmpL#Ka1UiG2u2jeXvg&T>>(+L;i4G9Q(fooZI+blf{`Sw+$%
zY&w6|fv|}VB|K{m#ors-O=LUA`Ls-LMS!O2Z3XvNs&e>4t{ahm3PcbOUD5@`r(hAW
zj4riZ*e<tE6&i%qH#yD$efFDDBz^lMa38uAFc4gn3@y``&E`Qg>_N{LHIQp;X6f#t
z!9GbQ3j$rgqZOsAdFvJqVH|>ZrXV`*6jseN=J)C0I5}gJ)yy;!K8aERK50*W*9{CI
z`mfAbPl9q)hy+2C%?M`0Y<g+>#t!d8#>aaEvt+xkpo6efp=sLf(eo4H*~wf4Rh17j
zbRg90{<!6SP7fcGA$CY5g=N8Q!j1lpMI+Z9<9*xBM$GYr)K;Rr6CG4*>GNUL`J!vL
zK7ohy<7Eo9&%EUo`Ge0Wd-?Ewd4UUt?fSj+^8-^&_fz}vAxs3|3QdhqL~UkRKI4*p
zy0OmL33~kZ;Xd%oBirtoRoB@N)gM7DlYS`VLUk;hgwtSnV=!jB^LnMBi1bv8)@w@E
zXR%B@=dX9~+HDfgpDJU}D#d>Aeb1N*ITx)Ov&hQ>lYd($)Ahuv`oRQ=6_LfUi=&)&
zsrUSqV2gBn<Qf||67;itgu(f4Y-Pe%xEXZ$4`-VUg2(%A9UiP@fM}XC^H>YU_*mnw
zj?QqmljHgqUh=4nD|fcWv&gAipfvVJYPH$o=j*oLw`LGe%V4!dEt5&27EnL2c(Ho6
zO}0O+N?09TVb&@SXJZJSD^0C&Or?!@llPCyN*eR+>bEQ$TYddcFM2hl{0V`!2zyKV
z9jqa~tsuT}D$ikxwSN0~CyOjL%4E%)NRn9f{$$o19bameQol}1Pmk+4_W6TZ;BC+q
z#n5ZCO>s~G4+b^gRC7HL1rEqfEPlx4bFb$&DG&sD=bMsoJ1;gTJ63neWed9+up`I=
z(jx+&n*7fO=dM0m7aBE`c`SQ>P(3uLQhOJNpai2|8h_px*{%GZfHu<~h;p6eIVs*>
z5;?DU9M7{wJrqk}T@&Z)eo#^V@fGoFK#Y0dt*tl%ED+2{7!FOzFb`a~io-^?%y`kv
z-H+o9uo&MtQ{M#yUK9(!k2<EFLoXDqFg`x>{_%R16D)Do#Sg~ev&RX*FGk~6Hsst7
zer1C>r?*jDe|uC(c5ZVUF+a)@na`cFwURPhs$sWxUNLXXSTOM<YnFQ%?8G0!BhGGQ
zHji-GU;dsG6mvRN^DmMvFmZpjd=as~zb`V8tkkNhKpV%hl>&^TTp;|EBkX9`QWZT#
zc6qy7&{DwLx)$<%EXm%|g7t2!h{j&8Fv!(g(D4t~qQ&5?RrJ@j<!RUSYn8pbKVKq{
zW?titY-QO;D-92pChu*CxLZf73C1vOBu_xDTyh!7nl)2CjwC5k*J3T^&kN{ZIVOK>
z$A_7u8*`<^DKEV#k~ZZ^#8Po;d~+^N3MM=nd-KWsTEss-tU+>)=veBEV4_K>58}g>
zHW*L|u7Tmg=0Cz~+u=mGQA;hWf#$S+5Br&l)Nc4MFYP+QV0h%^nqr8x|LwM@z*auS
z<{S4O!3XCpwzhwNI<^tru=_cn-M<521Ax@yCANFC*cwgO*-pp#lzhV2!!7os<7VXR
zjZl24==TFJrcTUu^CBXtrt9HPH;WG`kBLp}fs*as6z}Hfhp~(rKHyEj`Eo5{Q)9LJ
z;XG#vyh+0!Bes^?9Z$~gyBtt3o-YaOHdy$ZSk9M>#1zu2E+=bPY;96Yw+=e%gXCJH
zML2LQ;{Ozdnfq3+%6)Hsj_J)QU>%GhQ=LAd(5bhg2#+GpARftv{!2Dab!}niVNcIG
z+*yDp_4xgJ(HtKwL%tmTZTk7|(=1)OgZ!!eG+k;0UaV8kVn?xDKSKx^ocR&b{i2F$
zQRwwaqL@~u`@-vfFGn#!Q%ny3)UQW<q-P9n)L(wP>a*x9@)VNOKfG@H>*ojf6ZzK(
zMKcNfCG>9WIL21vJR5N$P3o^aKms?@`V?E2>JJ9DkyTsTHKO}Pq1vA>>taY^SM)hQ
z{EZZI9wp8KI3&~HCXv4Tm^h<mS6c7EY0B|dfR&=H_M4VLwF$$;?nKJ-P(sE&%UPvy
zx7o~u6h&56v*M?ig}^rPl_hac=S)nPx$6~dF)1re|L-R>@*Zlx!Siham=pY;S{7h1
z{<H})Yj>6z*#psD!*d#wbQ{;gX0SWjG<bl*w#U+Q4zrnvo^$AD%QWihR><>Ah~`K!
zVe2N|2Y;IGQ!Gh&KiUZ57779|t~5oi(@AWewVXs?rk3#?HfL5@Rx2pn<BdNm`GF8i
z?LwO@P}atIY~mA(MkX7TNM*|-^CyhP{6q~==OugN9G0A2(O!V>$5aPrhKW;ZYFDB~
z5Bw-vIao;3r&F_ZE;Gi#otg_1WxAgqYDC~}S>xH_V_iQTbj$x9+b3;Otf_qcrP?Rl
zW~@)2?!Qx3TdTbC`CgJj>VeF2!nP>enyAQUj{##O3zW+N%7gr%4qBR?>3+_iY1NkZ
zq;ov>^Q5|YpTQ%z?;Uc~1d>kKDRO1sXqSFGdat`4pxx`nbo{5rT=TS%#(kjBW2zS;
za}&Yfyx3F+&kS@7Mw7HUhfBj$HizQE65(zu^yib0_<&Cw30d2(t@1f7LoRRMoql=T
zI_<}AUTI$TT~*H(akLr@ruH;jz3^%H^!pPN<8#_d#}SwJ!7sdf9`V(ez36(f0HQ}?
z2$uBoX@PIPcvVdZjA8Q<;XYD)fZMmD?$xfDngR%(vwxu@b?EMT7e*J+>o)gImi;DA
zxW_i|Etuze<+^qRKjy_mo;p{)IZ$Dxah)BYD92Ck$Cps}e0^eT{9~@#6jTuHNmEi0
zO~kr*`M1pz_PwUCwN)YT;f$3tUIm1AWZvP^eAM(o+AnZsXN0wY9t8uhHb4{$w+y<y
zLL=fdZ!hnC&4=Jg*Mk2T2Z5WwWiPt1HAo$D>e1vdI~4zsg1Z?YVk^$wFfI2uXrGi}
z>XRj8PhiT%XBnjyjdY1(UEQH&VPk=#9q*3!v7*ftX01pO1f%hthxe{Qtf{A8caQOO
z%<mGCSKqv-^F{GXFze7HCIwnghm{-2S{K&6*Dmbk)!rZB->1TwN33i3)*-gbCpT35
z5l`|`>zP-zQct9yQ}oA_&q#FSJ0n{0$`4;{G7o-$Zv{rE?h3a8u1kqyX&J(ahTf0d
zH6{OT&wKHGhjZhwZLNFl_7}dT<SrbOSLBdNttJ8Y3iiQrr<VlpqA_CptV~U<$)*!h
zk{l>fBekpt%+w>ap1Ys!#sYy~fX49(C|3+cQRkuc`JmzN&|nl&ec^cuoW=n^i*F_N
zy-%fFR&<O=eZQ~u(Fzaee=oOt$LzPYUQoc(9pk(@-NOwu=C!!slRRvBR$ru?cN^7H
zOb@b&K5X1_O<auj&t6n&p~icEIuxRqXH(R%KBe*CmBxQo38a)V`BQCV9VYh`!nfZ%
z#o(Tixr%|a^g-j@?bZxNY@x&sod0|bE<F->k8q8<QXyg;J<$JIx!1pD*d3V1KlN^e
z0+jEd5IX1dT+V-5zF_^PY*5587HMz3qBz%gJ$#7%z$pPY>PKNgfZa3Wo7qPmKOCzo
zS7)5g6b;(99VhKSOUiD;CSu^yt^@fs;X>qKByyRJ)LD&`01Uj#s>Ni^?q?~TK=P|j
z8gU$J4rXTk^H`*HljU~bjN*9lk3Is?3(l2~H~skM|1!)+oPC#E5IKQ(m=q`M;^Kn_
zwU}d1_c)2382IUSWm++);rCOT(t<<N1wgOrmLX<%MM=qg;BK2v9j#yA*_G%Tpiqtz
zQgg90nTF`5vg;3O4V0_nlOyzPijxf-&{i6e8v5Qtu$3ouwr4GZx;8V$n^Mgpk9H3G
zK7D?sL^q<XM8}Om>z_Emw>cD_=Gy%POuseOVFQqphd_JfWZiZaKvYR@qzd#zhzEmr
zZ+46kBXH>qS9Q6byw3+J_3vx)CLkd(jw`DJD!N;b*U~~mfktBl*D9+eoHecU-7Em=
z-8^6ERJy>MiY4L1h<ik5Iknf+6N_sjzC&e|N!uv2f&KiJ%-jf`O+~5X(z;HmD~>KY
zm0n}Wo*?}c<U>FrimO)WKhCd2ZtX$lV5eYQ+ZXSsxD|Wv2*shgvPRRi3q}98-R@Y^
zxWP;eZRnXAEl!Z?E9)$j@2-cVV`>AgGMOd<j%@DN(K={P0!DxBpWp79FP!R$0wm<j
z6Zm2A=m3WVz<CK5uGpkY$G`Lx4ZxqT#w(h9dmTXY_5`WB@f}W#nY*+PW>$lGLa~(p
z%ApUV&raDs^i}wjHVTVfC`T8#=(__<cMDUzYDO0YK~)LO&c<UCqAz;KQb<o<D)a|l
z)t1ytRLo@2<1vXHG%rf|L!wcDd<z%n!xiiDDK+N%*bIAlPIJgM2`7uVYX*<0(`I2#
z6~)I9pVh$oDjd;f6x^^9Ivk6zGj|vbnFj$iC96^#<@WOq3PYSsSsm$mEJmo`TS<YB
z?Ej*2x}TpGBd!=~D_HrFUgi+&*a}41wTzcX_mEA!r}TWcFs)at?z{foNlM~=(8Q=&
zN{V@|K|guPPpAD?y2--j0E&*7PvgH)ad5rk`yFlA`*Vuj(PXG_eV0YC(CBww=TAzs
z|FWNQgosaF$o12Kxo!;4PGpr%H4b_pduCgdIN<$JLqe0Ly;C7RZQ*?30rkx5W**yD
z`&91Km1^{oNr4p~>Ne+jC~IBw+<;e12ysNTGRmm@Iknjz3q(SMay9%N*SKV@^u8pu
z38#<9Hefrfqb@$MbG|`Hs;x=p?b2=R;o3*0(w5e+C1Y&t?UpoK?xl|3<<iF4LaIMa
zHp-fI@)WldmVcUk&0Q4HKcJ{1=Uy87z8ciR0kw@upZTUOe|XElP-WKiF_4PC?`thK
zPnR+tzZ^2k*MPv=TM*=SFvBUtY2inymq^;{s+xte#I=P7i6=V8H71^!*!dqIoRJgB
z&&og3tKKd`Wjf*rWiBQ68vN+mo`$I26a70{Xt1rUu;-V`5puR8!a^phS&7{jj7_{-
z(JT75{q<*cszDht#*R+096r!xlGGXpPZ&)j-Na{byWX?LOhi85_LdBY*~uMn|1YKU
zYGn*>RWoF*laVp<S!{_#?`q+<K6}9V8SD9pvFpf5z%`#-i^99CFp#m~c*A?)`;?yN
zn}Ae*+mmBdU0GAFedZ@g`F;pNYIr!>Rn{3)AhyuZUVyFW^xm@HX0&9)1O9kD!t-=<
zpd}YC^wsycl*~MJf=YBmhN-#3>+q8jXbb#4&XDSCtj1t3&vzZI-uL;bxjyTi?GM1i
zTBrz<snq~AlcxAo-BYk1q9No+z>`Pm*E9d;MlQZItCTYJQupGaWLp7}6-FT4gX=^0
zd0a)R@2WL^+wZ6qH@&oA@spaMtp$*|(0$r=%+5p(rS<jadr`{s@ec6KI%{l_P=EIj
z>kd!Hkz8*KDOT*$OJHkxhX{j10?G~UVI&d4b5xVPA0UDoGOdXJ(U9yI!xc4S%3^r^
zpZ_uwRUoh#@7d%!2doxGu2e~>zox(ftiu8K$u#T9ms?*V_a-G=_z@hf<U4t2uf@aY
zkO}dr5I!}XccY~lCKjCE?Y{~5-DQ#4juM;Gp<s;0_(d$y8W<waJ&*o$olgW}TrGoo
zRy;<Sca<KcPTkNW4mJw?&!Z!qnY6_IR2p%LF8fC=v~Z0#xh%i)n2nqJ=xQNEoaGnv
z8?$#cj^0ZeBTT?-!2YE8M>uuW1&rD=HYg6i8l|7Qm#`{v^C3zMl^PLKj}a`xL;;Bm
zR*+j>Z6@#l2%=&$+4jBIDNdasV8#swN(NZdd1Y4eE3gy>wcD4+quQo0>LGRnD#I2J
z9ksGCnW<NjByg>hpqXU8fMAo*c&+RHqybn)6+dVp1m@sJ#nOM=O3^&>ZfD4IgGbf5
z@znul`L%-3H%vt1h~g#R;J-u#;rP6@L|<V2-z0Unj$S!zP&`LELGRHuj-E?nw@8QZ
zIvzf&EL-zFXAhr3!M;%-)PDUBrnZQ>dW#qa4ln{7TzlOo`?;S;UfOyElETtU#j>1O
z&c@nmHk#gZ%!DQG^bR$}v&&xAVw$g4k5kBnZ;B9-+UR||o$=u`)!kJ{U`jby{iMYG
zKy?OZ#ox*0&*tE6YY98X3N|iVs`F*3qSwVTJ1f|QNh!WD$y!4X6OL>-2*@ij9u=Ze
zx2;aP_a+x}`xeJWUy%IVCR&~GIE^;n0|ptBOJsX5q1?82*YUQQ-Cds3&PTGLS0|lE
z?8<l9N!o@>tcJ&UeF2A=m=vZJ@UU~_iF{EWeZti(@jJG;Zxe8<+DW!O`ddF9?^*2{
znwq(jR-9N!g8gr~IzS>vS;Yi8Z%}2dlGE&LQwFJviDQW9g`;df@gEUdVNiR0%LUJj
z%#1-T-ZlJj)iS4CIIa0pZ?pf#ah%SUqhM1*thYt|!tQF`rtFGFg8Yn6kwra|O^MFW
zslrmp>S(&5e2s)ro1U_jrMgN!GQLb^q;8}BLyc)H_>7s7V=A8I0*!~}olokE*_$~a
zt{X2p_r;!RdcD(cnRu^p%hW22(yRrzlYNlmSnO$^QEgfp5QzsoeXx?(xZazaYwke_
z*_GA}J_ZTE_;=a~gPqlPigbDBMm+cacIFV8V>ZYgc6z+N(3+36n1p#ZiiBkD_Sd!g
z3h==mc5AwsVM`K-S7JW3Q>w2O0HBNA$&`jkahS>O)79-SVTUwO!kXQ8Kd6I|QB!Qs
ziAzC%we?<#d(hpPQVu)#$%HHmaSJMFK-=B^U}hQSXEY|b0t|Cky2#DRc66Gm4GxIN
zt$Cj@*=*|#Rbeg9b8inySkqG+CS$OFz7B%tSUX?V5T-O68zv|=GUB!Fo-B6^7g!G&
zovrt<MTR}u+J?IX5BObdrNGk<QGjE!!gD4pe#Z-<Rt%dF=xG603yTI14RN&xbH2X1
z5Hq_ujJ}4uy-m2y<sSc&u+*u{id+BholYgXkFT8Iw>z0aTUW0crwnS0td-nw(#zW7
zd!|(LvBCrE7oNh1;_FZf#31aH%47l8d)Dx=OC-EzYJoIb#9Nd3!_2xMV^_*W2?7nj
zXu_1F^HM6PS!~gWatdB-bDQNR%LU+bq<>4vDr>=KjdU({#CGTT==(TwHmY8}HztRZ
z5{S3hu?*W+S^@{G2=Q!R6O!BUV1d_~I6!h>e1nCWZiE$q0@2422%-Cn1y&do2e&UF
zxj(GwZswjp3A78Yb+(n`#F8Eu-Vkpk!}Qkpqst>P7dx%+Q(!x9R&>}oQK66KBV;p9
z^sjH+S$|(g@19)h`YV)JmJ@LTUwh`qRLq}!Q!#dh{z_^di1nAI0!mi~ZqRUC0fJmd
zW2wYYU8r3N8W5vY&6{2hm4CGJ9h~CT^MvT?{Qb~yvHx0wHP;4KpXZlE+n_*@#Eigc
z?0kAe_{F3IuZ{hQJig@HIrE^-PO0YJMc>Mb{tA!^5dfn)M`$9dq;MtZ?MrC#z!tJK
zSE0CZKtVu)`w6o3t8=!*7A0W||6cBOx!UFP8yS_lx4wH^70X5Vor;E2+z}5EQ@u=N
znAghL+pe`}xx2D#G({m@+F^grmiVu-7{h#H9ycfDp;Uk=C(S@hKT4Zn(JznTG@H}~
zAoVc9{eug*bW351lcpK-@ss@#^<>O9neS*31|FSGO2x0X3A&#hjx_RL%_(I^r^4z6
zl~YnlV%MUbV_F$%k|IW6CqfZ?ke5L4vEYs<<!^EPzONCaSFmO|trYy5xLSQw!nwWx
zdP6MGHt+T56_ht$v?%%!Y%qQt9WW7?x;Re(ke;0IX8NIAZo-O<oQweYT9}{2fD~7l
zo@;AqAZhm{figEt4`Q@)ed4VeLa30$i!+)VjfMSOB6kWrU#Vi~no)ZJh_9&T%(T)L
zSzP)u+A%YqSa;7_jIspscDWSOGd9^%PRu$OV@w_s9q3mKjH}9Iux?$qU61Pn<NJf7
z&<8Mh@wMg4aKau{P~Xy*dn19_O?3HY1ZUK(>@91^wB-v4@c>^70w1C;MYBkzRW>XN
zRs(4)iLM}ICIpS);hRiu@%$u!LVEDS(y0VDF{Y_6^R`ULTc^}}k5ywa<;21_41I6T
zgy+aFlhz>dl(!wr<<~A92H^DE;$U{9R@b|2l5FG|%bu?LOk49?ha1iF7WP)~3qpIB
zMHox8l|07NWHqba+Ne7QUG*vBd+lUdg~#UA{dnR>CAZr{aLh^_6pOnVLxMAXSkv)i
zsiFU8$OkrJ0H@sUl3X$<;D}#Inb-OY48_6fbgjXupyFl4B}$?OYU#v1yc}$?#zSA=
zL$a(!@>RE6%usAza+AVvuZJ5f0Y9HDue~*s*?i#e3@d+axD@65ZpZ~(m_+GzB$JHC
zglo1e1EPT<s}*!(dHcr?YEPCM6t7NK*V9q5`u7DLJTTw1J}+S}n=v_lHXsCaG}xu#
zs`a^RygL(HSdLmZWB@$G&T7_0grRI>Gr6Is!++xrVMgezy?GzFo6Xy{^~P}+BV;`y
zSZkS{!=4@AgaAzby0B+T3GmL17gIuxmcNf6l02|Z#%z5_C{`TXDz=^2`ptL2vmwQh
zUM+&fX`WnBoq$n^-qUTGlz7*!TRX?^gk?83n+e7Y(ZN11X!zpsZS3wJ+?9_-E~oYG
zI5J|UFNk<^^`ONdrmeZ}^ZUuci{7chwNjD-XMO_*KBQ_yzTZh*0m}Nj7l_Nu4`&58
zA0Pb~{L)fYVM5)06w%I~&4<OW&2cGP!z&fZq<}jqHZ{@OAULtZ>io4@C&Phl42-;U
zJrEx`V4aZoOC2o^>MIASUwwW`!}T;FM=wj>?0{t%oTO>m?<e<A+i5M4#G{lz>wH1h
z%yva07xGx^`vNrEU0{ghHAo^7yrRo#I}$dnFQ%rxlovGidxwQM23#-u9-EIEQ`63)
zmFO$YyYsHotGnZG@hU+#n~EyeT6*e}(Xqb~ZdW!;*2nRK??<S@@R;VQ8C0`mXhC|W
zluV~@H`Ns$P#aVrL70||Smdb@0fwjX__Tm|AN``jup!)YX2#@O<}+yh8kZZTZY%B8
z=3nxq2+!;NSy=irRJxpcowr%~Iv67vNIH7|c(n*mKZf$2r0n)fY}4jl)ahZ=m424K
zt)~4+0raAv0bu|tUW*wRs4m!^9sRIa8}+a#X*iWz0y3V~=Y@z7{9&E`Xc>sN1jh}<
zI`wOHMpjWd(=?>EEHDxTjlPF>TcG`3*v;4d^sr{hNR&kQw;5m39P_$%@g%@WnH9zn
z=^4aevHGql0Nsu2n)Eei=J;(9ztavp$Mv4s<Cxf@cbFgkC3eC>=;W2&@OLFz?-JbO
zhn+*$EkWJsfaga(3a?T6g?^hYt)iEmji`DRY933m>8S0wHwzM_8Y1KMNj{~u4L}<~
z=?}Uae6C`QA>(BS)uPanp$k=TU2YZ$^AHy+JsWArFe34`-S{_X-teU<H##ktRP$L)
zaobmnE5(xUKa<iB&y*Lqt4_UU`FPrH8n~di-3R$jAWe6d_H3+JMwPUC#g-gc(|PsP
zgMq2e<IW*;6sw|Xh}<INR_j6P0N>H#9p8s_LM7^rXHp4p);?m-ZZh{gf41sX)vn$E
zCgru9SnMJaJ{kfq^vj=ZH`})nL<|bQ(XPMLk+x1{j|)eHG{!1ho)Ug6%WF^cK&~Mf
zqJ6ZA^6B*>Ju!6Q(_8K%*Tk&)Q(_9pAaBJSRCbd(h%ro8Cg9n|q{a~oBZMsC4hdLc
zyT0OkM84jVc^ahm_A8|gxYWLnHPp;gg!H@f?M0uwq68Cuja}LYo6o0%dcDQHX)LXK
z>2s6)l~;B<(LN7~vCvAaFd+!sYpaw+2e+m>N{3fWlaF_m>2IzMQk@3OJ@y+>96ICJ
z)Y(^~4ikTUOM2UaO_xlISropDTsW#ZKuBAwg}o8^5Fv^)t?{u(H@vnih}g1DD2#qm
zc{hG80~%GHb+H+2CD9C$hQdnFRwQ7o%PHpb`TkK~to76>2?!(=FtsuwI6cMC8N$F{
z3EWaaxe3<vS8Y;Izb&{M{rE7Q&`@IFD_0SCLIj^e0+j;GwyH(3ltWG}FdsMWJpkiG
zyjDulUT3HEP#Hsk>3T45vI(i6P3Z*to|oE$ERN+@5J1}L)c_yjmrt<3Tz`Q0i@^q@
zuNT8@r49v*U8NpRr%LFnIFTMT)3=s=#l)9fh1vjJA7+VDyPqCnj*FY!dgVAjtI~Nv
zKfjHC6l2YbntBffF)78?7C_FzoWsY$YCmmrK%Uxmomkp$n-@n(095M0;fZBN^&DaD
z45JX)2&E%>;vZBmfJ7+YpXp-YLZH+0!(&SoD!Ujtn1zx>GuuAjypEeVoyX{tAjN&d
zsT7v!%~o;3gy+1yqKieLTIOCfYo7i6ROi(>=Z>G>VHOVa#&>Gb%<=cF>~rHyy2>eq
zF^r9d`XpghZJmt7^x(}UG=oa5HGYr69LNRf{4RMcg`nNS?%^7EmctWwTE^_w{f2k@
z9QARi-6yVV)=jM47h%Uy!1LrPH5B{Ec>s`Y7e8)HDbY?v@dzu{|Czo$AUrO*O}sK=
zF;z2Rs!cyB;S=SA8X4LZVcL=UuW+p`fmWEsJ}c~uNK8hh#`JTzP*_p8tmi4$yGl{p
zzZ-+UrM()cMl}@BhrHK9l$hUK=J_2r%`v8Lr$&d7yaSr*hZ4$jdai}kDbwO%Kyt+E
z*$|k<MNL`h4zQk$6?<7$lVk&j(E;UClT6U0QD!J!UCi__>gMznN{J=~GV5Qo8vBxn
z_7}%~<XPoN5{)t$FN*ID`4!~LW+aNMMXl$>13z-`s(f5vRB#L-_KCoJa619REAr=f
zF&tFu1@;=kq{ZZ&e*lk@t_qK%lM2h-urYzVe>ajkwx>OM4!>)UD9XH6RRr*)LNIpC
zH~Xze^Dj~Gl2WUeA03NOm{v#d3(rN-rGTH;HsTMyy~?U}9L1kGe|iZXKfg2eKLxIP
zv(zNk3_n996fhBSLyS;W@YZ~S3IZ$gZ4E=UvMZ)4pnColW~b!Ue80$>sh26rs_k=Y
zrYnv}v8#P<l~RhLhn}K_ZW8(oIMZWNk1CzeFzog~+RRQ8fP^PwuacDRn74>)By2I@
zV*z;a;m&<^D#g~Y_@+4$_BSDiYmaMbObS%fV}StkC>v}E3r&y77lMbvO3-t_A@jB%
zthm~H6(3d-iwO{!gI{c!Dsnvk%syo{!JILb*WDqZ>a|%>(l-@>jdFAN^(q(*eG)jI
zqLA6DQ=(0CtcL2C`9GmSuJe4i(#Xt{I}ok%E}i>!R0b`=bRt*}W4v97R5hI%C*9Xl
zki$69tt!#gWEgFrf=l}o`t4~be&sMW+I{!^<YN5@dA8`1zh`)Q3v$Yf<pG%gg&bUx
zDJLl`Joq|Ui=vr>UUs%fXlt@#tNmw9`UmsRMk|neXUJ0)X!|P1HA%v?bK>=({kJcc
zHlw_1EZu{8&2y#!aRCV}s~$g>Eb{jQeDUkT``dO4SW&JP%H_~h%a}?cRa5-RN{1Yd
zal%Mzo++%4Hn4e8Vd|8h@2gt2f@->0@R$5*#IMy2=Gz2)y!iu$#y!mN?*@cjW`>`+
z1Cf>9Zb?2q-P1j}+NP$0kEhOE-ccouH=_TZxCvs->}!Ia$<fs+1MM663<)2%mk8>`
zTyl90q~2U|A2t{tS%&rY4F3hm+~VDpOcw;6hnKV#j^Az1)xXmCrVGPknDuDX;^+6M
z&WTQ8LBlPy`x_y%-hFNMSX3<}9~|WwiIW0b3SDx-os_zFlBh+GNcHQzal6wF3$3M<
zb7l<IhJB-mk1YRDh?X<e-V<J1sm%QgujD;&-=+uk?r3UFnT>u0iJp5S{9BMgYkR6!
zFj9ZaHkTUz7efW=LF6GGOOt6H^49gWcK+1hcPMAXc=<ndTc*I|ye@<Uc;)VI4JFl?
zzRZ3LZ6SfPtlI`-heE~X)B@BX-BsVB?2V*VCXWg@5^~dd%8$}GOmkROIzQ4@xTc?Q
zCvP=&>TWG~3LJ}Yk2jy_45sM6ub=ER{AB8tR^oNUsPgaN{ihn!(konFm7TNludI^I
z1imu`M9~BsN5pjsAH8|OE_sb(9VM38yI=V=i<K;jNkgdD6+bgAuLaBTC5ejRb$5(u
zR!92}y;C4S6a-p)ag#r4qta!6u5;}x<(v`Uh50CqGrXmE8;Oq9_qh#^(oarU2FbG6
z1baocjE+8R(davonAG)lvn>TVYSPH(0czy^?F{pDGBa@RYi1{EfK`;~Nruo@HYN@v
zUcos2!ahYT^S1?_>Nq+Bl67>CR%cRHM8FVd0@vIWkNoQ>)|}nwVIS#TH@D-$d}D8-
zIitf4VEMSPoQrzbhu1%q@hdsm>Nchr1P1}sACME!fz<wwzdty!4yI^IDxS37^FY?t
zBe%LR=u&fBJAP|O^hAZ#(Fh0H)7+fhBerAU5r32TYT9=`wOt3_Z}%$ErgtDZVew_d
z{s3a+UiwhJyUk07+3jC10Fy=@XR{gOB)JiPwp_MUWRz4N<#M=^z93}dE#zb$G%ids
zd`o7^=DRGYKr4h;U%{Z`v$|xF(3i2me9xw%t5XVz+1XhzWtuYy%7fsQkM>4VGh$?_
zjk9IXErUaG_~GYn)?n1b8ig@DZXQfIbm;90F(7t>0u$|M(G|+h6hy@yN$vA}$?K4t
zWf?$!opadLPfov!BlY29i?ePOe4<yf@2!yQ3fJB@`Z_u3x*p^pkHkO^2&Ao^t%s>^
z$w{BNtgqMK%4IWg*Hpi6B-ru}1$+OQMUBIEMXOQR;mhsekFR+?Kg5DeYO&^2V8Len
zF3Ugcq?qnn+Gd;JC}z`~H}ISKU~Q!0QpK?wpomu}zZ|7+LlY||2on;fvL=T{ha0Ac
zhQ~>VGs>n=V6DJ{940b-{gvRUHx%Q3?yGPvHIv}<H)h&mVm2cA-@SMM6xM`RdCnFz
z@-YpgaM_2Kkjng%W-vDJ$1Hsm_qq%9l7US&M~Uw2+ct}CJ;=!t&h6F@wcB|vcy0>A
zF0NXN2(Dx@;fDU(->&pothtQ;%)q9U{x_w+0*;JS7C}LWX)z>s?<_-4H3%tt2gJQm
z5zC!g1~1<JAx>;?=9*va5XB#fX1x6g8tvY*&;_CrMVN$IG3kmBeVzwyaIKVyUfAaE
zLF@1+#_<%PFvG%EF})d?^1P|)L&9QKWD?O&0Vm-CU+(E`Qi15K&gZdMG-U|xd>Lr!
zd<A&b-h}aQFi2@ZB@}->KeaG5uH*x{6~ib)<}RHvYT+<uTUh1{MrM85p?s@=wm2i7
zN!|&j5oI(OU4ec|GakrQ&7_m1MHKaZ*n5&kFmTGm(?iv!IOwEh^b2WjV;z*yDQjen
z^@&&8!kC9et&vE)0DP|EapVqLFc7(m=cjyo@f~+5NP2P9N+&O<<^b*N^nQ4?w3yHW
zlpBzK<nV?|7ia2qe-8yX@-yPEgng+-2<S_^c6UN*C&pz)=N*EkG5m|H*v#zAvBF}~
zw;*cZyKvcB<Pr}0+2`AGUTTNa+ucP6T5{(UZF<O+q<We7USepe<>nqhO^r}X&(H?d
z@$1o_@&xJ^kJIC_4mQjWqj5wd(`DzYx8YMJQu{MW&mVgV=~X0Kh>a_c!$Cx7eR%yA
zJxTl;dU5o<gc#5mKmEEngMAN^1XHN}XJ|O~Sfpta-<%f7F7u<;zD^gf?3yRijw^PF
zA4znC+o?+?XbP8>&v)Kb|I0UA>RZq4Q4($8h!^%Kzy{L*z_<2(4eB8Bv|3^+dZNu3
z{IFOI8oVXk3?`W9xg^O%xBUI%+!lwSW*p7XsFiG{mFikX+86~u<@FK&(q<iEWq)@}
zb8aS9jKP<0&+ZHGbAl^FqNDBuLx2AaZasW|kmekHFS+h*E!Cxrz4E(in&3k~rXP<9
z(~7Wkl<yOYj=&-CjK^OGN(GB14v)8gjb<xmW`Ih7oN-GVJ&bkjLTKd#69G39xPT*+
zr7;^V@+A+5%MlcdOd`4U>EAJ0YEk0VZxNNd@6HBZg%BaY^z&KkMeR$4EiYO~kRl+h
zSMfAcw)LJxh=@PR&6Y<5ntT%O4en|CP|d7WX?49WX?^+zm1?~NfYPT(lQiPlTO6HO
z^vpUJiIKG0FI?s>JwF}xbM=MgD#8(R!NAc*%DRf)A+11t*FzE9@WXAAq5@F$dZYfe
zABNh4q@!mY*WbRq<R=pM`syCepek-`qfKgw-zw=x<U7J^ib>O%_c0l|X+s9kt?Wai
zrcb3K%FZFpf5Jjg`Ftl$ZVzI3o#}Yr1b(k2e<Q%?us_2r8|NVkE^-?BMDuJ@lk`n%
zDY;>Ah1cbiFB&;MX7>c}i@<|pYaa{wrMGn|{OIF?M=fCj=KiK8K=#E2;G*k#mTf}@
zh4WLZNDuhA;?|MkmC?Fgya3x{FJuFAX6$02c6PS!hFM%6Tj!rEVm0!t4%KM!!-2J~
zXRua@Mu*uSqpKJW$+99#Tf1cFxU4u~(*{4^RHN}0<REk0AZe?tii@TazR&_qr+ldo
z1#Y14ol#<QdZa#;5K;y&sbvc>@11QJWxDiDq;qe>^_MNSX4+#3*Ue5x*Z)PKA5gD=
zTA3-QweqI=)DH!(cfz=*>(xU^u(=o4ztI+L|7FM?O322%UJqlQ-Jml>N0(b(Be&Cd
zZgNJ$?Ji-qxReAg#WY&1v4li2yhY<IwK<X05%_1qo^_oF&VasU_B=^VRtX*GxU@|n
zV2YeUlY8a@DTAT<VY>{Jr|50HWxcZs(!5aREzdrJeO_2pMijxQCOW^zbqiDHR8ahs
z^a@!Kmb)D7rYIxqW?99rQr^Ed9)6iqd}8hQw1G8*=P{>?DF%%$fosv%=U&Pc3l-Ng
zo)|{jZ?!uN_}pc*t-k}a08l8~PtDvVUmiOE{yA|GoG>klJsuI^kv^q(PJ2D!iBDW^
zLS_}4sOi*YVT*H5KsoYHyp|3=PVq0zH)J-%xiyefw9=PU6krbhmNXisl}I};%15J*
z;cf(^sh<cP<y!RiEc_5?rmx3dL+uyXFizc1Qf$O4tnxAIVnCqj6>rkO<mp8)FdC(Q
zHuC+cew?SeqT;3XPq2EWd<ijex~7F57oz*Vnj4%T@dS2odgH}5kM2qXJcZF2P+$EG
zR=Ycc(Y<ddn{XQU@$P*TAm&4voSNexcT{ItzqY3rWO~i2ng4bygZrW;4sWNR_*CNR
zX7dPjlep)e4*cy%!tEQ*{fV&z1d4)As%UD_yD2kdvr^ZiW=?}jN>6N8`SUBY77h`R
zEK_8e$O2_wOi=L3^2)mOg;4U@eEC<EZd($EJRv!7!*8hf1x5XRMty)0C7&<9=J)zi
zPp<Zw3h_qSf}E9MA$UIwN^oR&#zit~>z`&1oqDHUfD@7D%S%ojrlW`wkj59)$4wrx
zRql)y@G+A<yX&onqgV(YN-7#ccC7<*VA(<$QV^xJBJsG<YK}AN##f$yVZyi8!h8~_
zKQ7z+XdQbTu1Vf|)lTU2DxmB<LuI57G}w_Xj@4hjD5H!LMMOHo--p5v`$h%$0G)^u
zy)#+QD~~~+TBYU1vqkxbiOY~0Uc!Km<*}1wU82=@UCU0O5yj?2K)G{<#Gr6`Bgn|C
zi^=D~bI&v`CPFKJBmuDW>MeXIh5zuQ6|2TwC&Qz(CtnM5+?t;RBrM@GL@f!<9pJ@y
z^<eKs>7p95wF1NV&D6_-D6c2$2*0LsR>H7{IvLYjdGcSv6NLOUyp)9*NB!o2vXfr@
zFOCEEBTci{@#k5NTT7P>)>;&R(lDQmU$TbobW4KwNg0q2P$E<+c(APQDS-}f78LKi
z>0BbWWbNw76+b3kybq$mcTekTN|qF;<*$nGLO{<csA9HKn8V%iKg%g^?+M%D$6_yb
z%GNhdd_8_M5@o|7fJ;l}dx(4p%nb9`J#JY{;zA*E^bT&RG`+_Y*-FtS%DXuCEmWD}
z%^bb__rW{`_f^Eji~D68B)wje;$&!5GMcQ?gvC$&l8$g*LFgC^B!GdC$XR|%9YrxY
zN%tOvgp|ia@EbGLvUFBrfbS<n%Lv;4?HD^@R#KfL1sjUE{xzu{tcYhn)sWuAd0|NI
zQ`J0cre`BFoIYyzXd|^+SHK>`fnCoonRznM7WRQ$kaDlvS!sr$>-s$c${F)O0*;KQ
zIr0w%%Pw2RkB`4aTrYdp93xtVamJMq9S7{LhUc_Pr_h$600~?=7dfoM(Kle}-btF+
zqP}4(UW3f2#W>cH-A?xuT5HggD$zIL)-uC;F4i}ff>v%sAOU+&?_QKhJ4z@+ZPPn9
z29u65*=|t~5BGqnSaGO<9WV!VX0Mr4I4!tV;hTT4rjbSGh73*5l8y|8Z2NrrT9sR@
zaWz%H5Ov`Rr}pU%cXBdwO%^`G!MBh!;BWwlBj#(;+awu2hONwt7<6kVOsHxU2l|an
zDg#GuU{aI_I(ly)(hN#b{FLkwfn4JPKEF)2aTs!6&T5{l4uNEDLN_K(v$Y>8O;MOf
z*l}L3QY-K-^DOT{DN7*~M-I!CVmD6Dm*4i4Tx?fi{IJ3lga4@>Kb*3NwRT08m?3#y
z)I=KK<T}GV7mV?y77gMP)pot&2ah)x`<`eby{zZPM&C-{8&<ht4}QU+oU`rds$-r$
zfftZ_8zN7)jAoIPf=ox<^0D^wc42jGc4hY8Ogx(vzhC4+5SX9xUmdoJgbDS<_^r3W
zP|Q45#EY^vJv3H6c(3t`+MUdaN=W0!qHYXqL|A(Ve|Qq@?!j!`@YryMN3eSUbA);=
zck#M$(C&FgxRai9^ywY3{fDGFgyE{`vmZ-U;qU6G8Zgnqrrj<CAcSB`0<<VL)WLNn
zT1&la<Q$~@hwk#qoSb{5tpF#p4Mm(5sNW#P)4Dvc<@S!M&2xC!$K${PaDO?wvm`xg
zjOcVUMu9S$aZ;Q6ucL8tvHLqrmv4XBCw%9-?<4@yT&(x_vjAGqCcBvE5WDy_d*r<s
zU&WAboA=Ip`-RGv7W$BqmaIkB`ur;Eay4kCRmFPWML?2)+(vHcEPTiikOtln)Zoo9
ziVVEUA`#++o_jdR62&UfeV%Xr!Do;4c-~hx8uf#<0Ic&QOp&hgXGBXaMJ7UBG@N*D
z!jth)@_GaNsdE7D==?l}_b;KC`>gciQbV&AlitP8myk=sxJ2LggPE$IjrJNlSJ@#R
zRWw@>yMF6~WX9_eQP7R#_oZ#@NaRN>hOdM?ht%3O{}><pdt{h<mrBC#5nZ4`x37c(
zFUd)1Z#*BEV@MYX)cXXevhHurFW}pUmg|Dd*CQRpweGJCXUrRKkCu8fQXQlAX3Kq-
zQrwh6kTY-hmq=cr!~@RvbYFssE*;0{3#A{ndZ{^3HV%3tX+=pK7Ld4J^5zm@bbhsR
zXu3uxL2KGnlzTGYw^6QFoiEB!5Jxj()$F^4fZMIjW7K*5kz?N_Yn(3vHJZDVc{an#
zE$KuA&zZjG-w*Rn0h3|@iB()bL*r={7Z2UmjFupC0o&CL^7qXGZfjCcuKL)0XH)Wc
z=Rw#Swij7ccY)I(lupQYqrC5a5fu%%-TlVe7UmvQdw7o>_?AuHL3`)#jsJgn74mGv
z<1~t#QY)Ard-9$H8@~$9BWdA__5HyGvch{Qj*|k!BP2{x9cAd(&zRz@#B`d6vV+9&
zXtjZF(9lo5(Wwc+EX^seYF{bio8ZG<FOf0j@NrSbv|<2%dE)PE$gr^&KfFQ_U7Plz
zi@D(67fE?Krzl>ch=${KuQzQ!Cfc28wf4Uac1D^`C`Uu!_aQGWTHaXYHb~|x0+g0q
z>A^0_4dltwBFd;bX}4UoP{*L;<mdRyAZ*#HljL8=XlK8c7rPln!0}7i&2QwwYL%Si
z^TmA_5w8fzeso~MQ96<dX$Y|ZR0_!XLRbCySpINuU)>{{Z>=f&m^eNp%Zma`>m(OO
zc%Rrc^5GKSS{m^9@bp5^Ui6A9$Hx;}`!b_tak#~I4QrnNW`eT2ey8ku;(zqz3-hWH
zB?VC{-EJHIKeV8;A4%~lhyK3#%(ybS36Autc(S8i{y>?ebnysXpIYfFR(Qb>-xZSp
zQr$oxRN@FQ!^k-O2o>i=pd-lPUmRsJb*im7-_gp?h>Co!G3z|?IqJVXr9PKfd`HyM
zv7Wt<4($y{g16F+x}6kU_e(?(W9?3Y094h|e7LA@S`wK)x)i<87kD^Q^&y$<Qeu|(
zA5}aQ8uT18wv+x=(DFZHgj;h>UX1$fkT-~uWMym^o5POW6fX3{zq_mIS@DtUQRBGq
zPwKghbElh1EJEi`W>#(DuW0^`%}c}P!vfcOF&0q!1!XYRMDPO=0v}b00_am%G?K*|
zKw~|R=n>Q=n>8=F%vY5Q9xv?c^dAs`8?Tfv|M3JR+JyTm6<-L2E#~GqSfVp>=X0wf
zmi#zki$PYz{w}DR3S}y5I8W=OY0&o)*9&{o^&#;cI}&}-10%`_%YIj<u14r7p|f&w
zn5<bW#7k5c3;aFGLL|spl$IjBnD6iYyd+nc`}~D~4kexbK(iifIA~MUj(b?~Y-lG1
zz0Zj&Ov{^d{YTkLBRGIkD9QBg`_jYBCE{<SvM^+u8we?*dK}8M$VGi}kI5o4@zLS0
zlR8&SSW&WsgtAh+t<@NAb085v)hX9$W^_7PQS!VHcK6eS@sPVL&`rXd+))F6(G{-_
zF0X&O5?hHV+t6N+;`lFpG|zcc_w9c#dLjPZf!qJX+*=35)qQ=wNl1X;?(Xi^xVuAe
zcWX32<H6nCAy^=|J0U=D4Z&T5yW1R|-^}|yb?a8$s#`TvlRv1U=yadnXP>>-UhBI)
zU(msb+S6)TEOoB8#;?}mcO(@Z=jU}h9vaIuenF&$PTQ{A?-vxQ9F}|^qPO2Oo-im6
z9JM+4wOl5^4}mptt&X}*ztTrJG7q10cpgzSt-2MwWFk+I`)n-nU#_7fHZ|o)ZN*FF
z)c+RaVKM)qrX;9mK2D}lHub|B5T$GZ4XpM3@X%Lqz$j{W@pc=9YYd>m1-XBbB&L2u
z-(*b_Ir-A<KN*(4hUJVyju5aG2AxB_yX!t=6onHdTw?p~0o^w!e_t4$d(oIewg25y
z01wtXTxM~OiPkX;M1nxmx3m7FGPx2VGsqt*_M@t0eAa`q)~&l2e>|}u2DTCOt(nIN
zG?tWiL;YYk_d*cT`AvvS8A5y#+=Ue6&MHK76FmuC$cG>UZD^z}Y}CoUf(lJ)KwR04
z%OB}-nk8+D^XH2tOivbX*nx#tyLb$%0-ltW+AP$8$Me|5ujanrKdwwR*L?CF)cNgP
zNSo|~k{-Rjz39T)9GL|zT}Tcb?HQ_oAQt?7Q{o)gG5FWPTKl$ku|}dp{<hsaBITz<
zkms>B;b}hfDDthZ-5p)3>~m$JNaqm}&w9EPJt^3O8INqNFzJItdwP7@*Y<G?e6tF*
ztIHFZF_TPP+6Gn~o?}r^4XXULU8s(sxTI+f;D<nJ{&r0SacKEGGU=EalCr%xi;IhM
z@OK~nijF@D=JcJKhCX7x_Z~qn_!V`_+bNk<-2IEZ*?(mCARm_3L)%U|GxhwIlm8gp
z`T^=rK=|D`Xio^JuA%46Q_C)($1*sjj+-wGUbu;=b0Ap=ye;kdgM9E9&PNLk@oguq
zoPVY3&Z<rN=vwyo@gG;c51Up6H5gh+N7-q@Oex;vi0}vLMgp2=c|m-j5}X+eoR=l{
zanGt5zu7>ci!D5PFR$T>QW&wgFs+0_B{S{1Ub`B?D+_*D3_Z#QXZZ}c;N0V!=i^}#
zqR<8D_Cf8s!-MjI@s-eiQck<98Cc;^$VZS4FHbl}myqBf)S`_nVj_qyDR3`uy=V~e
z+ak=M!SS2Io*T_7nbL}7z|3F=xv-M*0692tmg#A_2rFzG96^25?hhlfZbw7<p7c;W
zpV(amVyYFdwKGC#Vt!pR9|D%+vud&_(GF&D*Z|~M_H~!D>Wbj3E#~-*nzr;2RGWAA
z6lWYDkpx~Lx&(s@(Nuc)cQJxy9Pe3hGB<;vh8XyN6x#CdA(#|WP@TcHYyq9dU|3?B
zH{Xm{RvbZ+ViAMG3hW6K!(mn1cLwtt886XJN*Y_q19Wwrrf$wZmRyP_bPO4QX3Hc{
zrkt!)+lb=T0I7o7b{-Y2#sqD0V5*X+v@2%&BKneA87e1+MLgPl<D0;NY+TY5?-Wb%
zeM7%k=87!V<Li~S+`%nf%VXL~ukE2vY<|A;Z{9=|4)!h~q#9<UG_rUDHB1EbpV1G;
zHD1Yu<}rMBbFpkLJ9aO4t4|we2ndA6Knv{CN+&Xmq}mdARzIp6bBUf9p5P_ZA$0I^
z&)fwk-r%*9lRxwvn(#)@bcccbaR_01p`otGVYSSIY0_wQR0*L=|Jq)s!G`u|@lzcs
zcEHe1ov_s<Tmy+C%jFpaMMny<6h|s1`_M?z{uf{51@1jDQJRv_({u<K+5Oq=WK)z(
z&Bv~<nAz19mH4rE7o?=0=R$!typ#7(VmX2gK{I`Ij$j-lB#sDHah`RtC73-p(jQNo
zd*p>5yL+BmKIB3DqCiFH9t%z*;itz#VSqdb5cZ$(1_$`C+6Nv*rtzK+M38z0IqG}v
z70p0}Sw>%qw{GO1(jxPpenG@UoPmgC2!Hv&M{Ep+EWc5rHs~~3Tz^~Gg|UZ)tWym}
zF-902a|Hn+{O9al5#BYQGrm2zD$=JQh-Oeq2wK>r@}D~f3soEZ?dCqc0pcAw?Rhm>
zPZt>**Ko-Gk$I<`-!*vvZjDk8{2t=jNPzn??l&mo(Z{5CJinxjNWR?o)InQCUwDZk
zerKdRN;38+k_XpM@$!0xSY#isqQ11{>1SMi+jOa_puHSA-Iicmfo=J7XtjpM_eU`$
zFtd0-amOLVb0g{K%TS{clYXNfKrHBX&dGBb6V#94ArpnjV4k0Jq1y?8hG0WCV}&FY
z1-8Xsn)WwSbdQrfm{14bFWoGAm9VnV3*B)P^se|m2yEn(lzZJ&vTYqlkh-+j3C9wA
z-k%mZ?mDRq3$~+RSvzTaK~)n;9Tf^fy<Ye}e%F<}d$TGv>@wc9@e*<>&Hi=ctYROv
zZEt5$;hqWp!QA-@P=F2LB<psat}Nud`<Vjtt&XEY^BPdyCmSp4``&HO%t@}1PN8T6
z9bxq=cnFN*S(}5R45Ue??<WmEY!V`4K!XBc0!OW#Ry9ruSf}pb`3)-vJ;oO4+YPaf
z@dN2pj?<kpZh{5+ubhu+Lf(g%WO{AoIYuWjK$f_j&EvoGE85cKxN*y=*0D#gEpo-b
z{eyUt(rLqix1PMYJoQbO*R?ebB|&apq#!aOt#>p2dqM*3GJ$ysuZ*+BfjVeBtPyE`
zgnd`2X7#y8jN3+`hM&7c*q1#38MU8^u`XeIYsQ+b8JuxhT0*9%LV(9@LqW)wLhO7~
zmv5MI5%-CE1Rt9wtMh(eH=W0+_C@y*0|R5B+e6gpe2_YcU6jjVES`JSbKG->4(eCG
z6tS<)*!N_cF)`v^En&#hX^+yjxIKRq&RHAlU+mkiyLP93B9|aQfCHg+JxK9D#63DM
zN;RP8Y;1xKC31EI^|%csmAcKN3R_K5NBDD};;iTYj5B}F11<dnUeu3DbU*D2ZWU(W
z#|h2w$!RMm%E!2y;IIN_98xa`2Ts&G2Len`@@->?dz6ZFE&-MxbwFMzLlPf^iZuy+
zFNc<o<xRHxr%z;+zv32@6Bbr!WjeZ0l|tM4e($r%>_0YBT;bBnFfuLKOAwsepCz!z
zp{il8=@&hV*Z=0}4O+VNQ%7yuR4JX1<sN7(H;c`_0mniQPJOqpMNFo6OhA?E)X|_J
z*Tfo3A&W1z$P`3W-JZiLbvCQR*wf&fM{-ncaRW<~Y6E>zlpxR`#?t7Prs-cklecF*
zM)Y2XBHrHK$sEuW<f<{xrcs{{7AVW(1DdD>$n~G^`ni)>92qZmMpJ!#ZeaOX6nRe7
zm)uu-&QJW%i%y7q0`Le%zK0m;wN=`Uk@R9i1zax7bAxQk9ghG!z<2Nad2B$FjDvYm
zucp+<c;a8JWMowYRlt4RxX82}^oapQ@y<_4$nf6R|CzZY*R2*Y9RWILu*GF}c%x75
z(YDtU>$if=!OSQ(wFZA2m7(XIYpHqEES16#v@<O9&BJb5S2;JT^l#H&&*nY$EuD}x
zT$YT}Najwew!>PrY3RWjq&qSWloLE6?)#BRxUnB(kj1~ulB*0sxa)R7!U9j1zug3&
z^hn;|ehZ9;1|j9Agmq>*bk2vYk#6C%2R$3FL({7yRg7{WSA3S1#Z>Z?JDXmV;zvYA
zS6cSnkm20DwtF#nlf$=7mOm`-{^cf`_T&BKo@o!Qt>|W~DyUK0s^}{XW@&^n#`z9F
zO(HY&d(tnh8Lzp&IwB8*$tA4&LV~b)G2X=my*;aH64Nwm@U*q$qF6P-Jc2m;xOBlh
z{CBc*{TCr`y#qD2K8MyqW>-!10BoG5p-0d4)jLr<=C;e9c^k$Hm$>I^fw7C0J3lV?
zx7MJ{Ci2itY<vo%@a@HyfbiE~3|$&$labrdRzSad?HNjp9@i0C1@ST;*ZVlVb~ndM
zoWXHfK8;1jz~c(h^zcFQ8p=)Rsa_4>#vmhmF^F#yx&!ssDefv%v&?$8GSn|Hgw2NX
zvv2i!h9!FyvN-zS2GkTmGD!SJMp`wG4<~*Dk<1+yx!=Y5k&d=AY?)$d-b0asV_AQm
zF5d`Z6r=4;6(#C<PkOlVhDlF{#W73FkNpRk#h;)2X(wI{qlo|0WWb;=c(FbO=nzQX
z1lJ$%{4z29fj%@-AjOR@i9WUlJr+K@Q(BUf7~d7!NH*r4MX74$dq8NxAzhg-7V0+R
zwSy&mFjJMvYWkA0Gn%oWc)!jclm&k>>d+Nk+cV1^)d>X!f68!*1T6}&)+cA9mS@OR
z&+$qJ**3~&bBVRO9@rcUksm(Q5sx2Vy>yuOh%SFcQCeGb8IN0bTl~{iOs40RCuKkG
z>r<f_-)(q1oX628(Kq$0oqomRTR^I54RXKN-u6o}(k9Yj-7r(f79h9%*wO_xJNnmb
z^VgFaT_K!&#?Rjwb(H;KQ%E~L;zaMmynZeYd3(yG-iIjc-m!!lHXZ91I&Dq_g6qMH
z3!ZYz#`yqpCpFiP$T$}YszKa_^tvMx@qDa}cnF5r+XN>5ZSNApMa<1oU6oJH`Zv~`
zcM|h4|8ro8Kma66RYCnzUut~3{d-;abS|G(P_fcTck31Y;ScN}kn_mG{Jh3A$cTb{
zf2-NaZ?V%g*ZW?gwC4Crx8K;jr8q%w0u~v+rCPCKszT;aFM;Gsu3t<YY20^>Dy|Po
zwbN|<&QT}PKzmkHdL7uX7SI97OX86Bvpc%{c!t_;HjU+acuzd%UoCaNFqX#peW^2w
zDU>E#z$*r^$?UVn&v&$s%{Q1kE7bC7G=_cx@`AUg=xts2EAWAh1dcy^uU%+(Ec4<_
z9b*%fc*j<_O`^c3L@|U9sUwEf%5aephfe_bXu&<O-!=m^r>B$Hex*klKLzKOV4=yD
zTvOk}y4QXE%k1}j)CmOXO+l>n9Dqa;n?3dA=1c!1Ve1edImgAWp=6rv-BNpIF4*%k
zNkxw1PuAL0RF(A{_|M1FlVh&U-V*b9tV<`Wl5>ijCUGhE3k|mOmz$MQ(*2243XM5C
zv4TW&YE-ET&qxZHT=9|2ysKj>*rjM{P-_2$k0amWs8>}f3ok>G5D5~v@*5xeDj`Q7
z_68>Ey4BS-vZ)tfT~)GgklzNK>nWYAbZmtjR7F^ox2@ylct_aM9OPmmNHUv2Jde3C
zAW=v{k4B)UdJ$B3y4!nv=zv?cEdh|Do~X3gD>(-0KA+J0^zAce{yNb+G5T16YnnEp
zjl*<H!&-wrh>Yy{oCx^1`Kh@1o*;$*q_YCKSax0!;BGf8mdWcPH~Cy4gPnBKokzL`
zEZzw~!^1!q6&wF<Nv=nC*j$%br$fwQo`^s`_2n;xOgmHI>Kid$;budZtL+_!&n_a?
z1F8QDTsS!)D^V(<?g~#u298~RNK-O6Q2A)Vp;o%_p+{MA<UP=MJO&gZO{hC}?oGN4
zdWd`|XhDN93phiLQoX_Gs-v0QiE>PWhDdWl?c5^@)Z}ikH{=9KD4ohdtyD0J)F-v@
z<~M?El9VJZu|U`r7;)I{qJ;0+{2jFyBDh>&Bnh8WYZ@b{3Pj|?7%OL^$iP}S#&;9f
zCzrAi*DKuY`x7LC9dpBilg;OzG|b#K(79@r6A87*row09be+}WhA}EE9a$(Nu2;S4
zZ|O8=46`_)%I`e+6?nc%eg8upjFnOZh<@tme^kyUiOXll_y!gEuBx9#U}?9s;`4;9
zCh|Hl34!xx`sIE@;(PpjR=h0RxuV&zTDMW&!O`J{cWh|<FhS!#{GrN?tY#~twXr)L
za`BJ-pI{K?<Z7e-YB$+uV}9Ttmo~F7(^47_rzW5WAa|`C)=NAl>wk9^PkgI=*0V(a
zz_nz1F0AfPmoh%xACb<_&vON##TxUl4nAK#m<l8FD*^4VO;;&>@co<1&RGld4d4OV
zuk0DSKc!QHm;nM;yz{e%rS4nIVHYP7M%TkRgZr!H<%mN@Dt*cGtarN`cdkbZ5=@=9
z{2&0RP=Lhfs{H>lxA&1G^3QwWdyUW$pnu+JROpy~fO+@vMEf_@i1nXI*sh$ZujcIZ
zMrCOHjCwr)n8ePn#^Yjla47kEMci5r>lDCmF%g%;CV%wQQ6lI$(EKn$-~Wc`x8+Du
zsai>@=YBI)kc1eyy-t%oB|v9L9n0p^?ne7A?U?76z1TDV%}MOSj@~o+l$TMvR-H;I
z-R3%-mR_^GcBOhN3BzU8H5vl8eQ$*Pkb00`U1<Yh8ctzs8aLao-@EpNFck^}>2};-
zm47pLOs|xT<8qi`IHrXX^$$(Bc;+@v7|G!5_oW_HBSoEAnBy19dAnDuzc|y_W!R1g
zk9S`5zsQywA1l-Wj^m#o4R3%M`7bu-87#=qUj4t(jnMy_(T)H4r2p-N$p2eD#xxYg
z8K8h90&{K<pixi&+vP>oCo+N5r6s+V!`k63pu6;<N+pR9o*KFOR9gitn$vb>0MOc#
z4h{~IrUDzgg2hPM`l?v(!PU@Qjp_E+NTviNLf$#B=jC5g?iEPENUqRoS2FUo>B|q(
z+C$*&+;Sx%vF@6f+HxGP;JRuM7wKd_#pmWSull`oWO?qFPBlB3W=hJ;&Kh`C03lX)
z@dH@FbzWb07%8S{Os~NvnIDKk|9FCRHwhL4jtmCkS%L0(N`(xL_`yW#S7hZ=*Wuh>
z7l3g&?<9;PaJv>Ll3F(|sJlN?VcS$-5B!e2H6e!F;M#+0nPuDrTKfXE1^m6P9~b0Y
zxICj!y?JxAsJY^Op+FA{H1F@Ps;V>!%B3*8BA=wjcn=t?C-VH;&xh!N`iszTMbiqM
z%kEgRKhRJN{0?9gC7uNuiP5@O9)SA)_A7TOApz|rqs-Da%T%{|8NOwEICT-*eZjg!
z=H>Zm8;G83D%kjV@AZHw;YNV+iF?)CWdeu+E8_u9+WgTs{1wjwM%z;)V!<U(=((Mi
z-JH62l>i~@`X(!jpsho!db8{FC;a(}*U4b6-!m;hNV{+-rO{0V;))?c_a#zzY^Hi5
z(9||?HOxSbFjrxhLz(1nj`z9DE9mgi3oK-(;Kj_&{#(!xOOXE0xEu49l}{R*c?4Kt
z$?K#8+{fVs1npIV1e&J+XN(?T9Yu~VH7_}dB$jf67btF)Tw)Pfjk`Gy+;D#fumiYF
z(D?v)kwJ&2o8j2adMI&9<8c&`Kr6=*#oW&m;o*-=&+tsmKU$x)Z~3jKC?5FU8=R1a
z5}3qxKedeG##rUZ(ZE);Y$eEv8ms~{0-XE%wR5nqug{I}rq9K+!=-AHpIhQ1h3dVF
zk$$r`hJt}h-iO#3-67~Hx_nT*jWYuGq=9bzBJI!&bU`Xgp}@_On$4fGZNr1W35KBq
z?K+Ej@K<AizLo6zbpL6AJk$|jeqn=swx`{hJ1$jQTGLvCS;k-aEO^ITj3r|$wdRrf
zC)25lPsh-kSPo{*E;Ql&0IkjD13@&r&;8|D6(cz%WuIq}$0|G)!)aA?XH1xR8vuGx
zh!MEf%Iy56RJ~rkO%_b1Q9Kqlv!dJPR<Q)MDRC`L0~$7D`$=qT{C+{#Ri#27D~63b
za;`HxUgM(4xv;T2viY9;u^NGDeRvSsAVB`K)FrW!AYd{2Qdm?pkBw{fr5oGOoAi|^
zhCsxxC3ydr9AC<-2a$Uq7~8;#-v1Rk*0>T!0t}*08*=|^-{;mwBy(djCL>?2?|tcX
zmEo#IE0C3&v7c?Q)wG3eO=a$SRDGqmW|p=H=Jh3}d<6oD#W)N)COYB9{5Ou9dd&IE
zhCWxx71cbJYi|C%_j*gpDrlmcdc<-D1_mt$1sk20Q*zsXG!2veUY=$vAAp`NJ9E#g
zKbngOSVt~u7~GSELXLED=SUs{gHnauQOaSIRq>h*ZAV;-U&BM+vb<+SApF|VXw{^a
z1b!=t<T?n<P$Kp6_|O49AZL70^$~e5>z8uWHbkU%8KAkI&zZ&!zP8Sb6(aE+va|rL
zrrX7t_H?_1dM(aHdhW}v*I6g+7mC)|K2KM(x=!xTce|-GCpg!@23GfAjFQmLdddUi
z%Hfymv6BwBtBe*J5b!QD1@(50Ki_Jly`}x(xXpPZocexN+)%2wmhjpfxQdF=Unhki
zyB0_Fe{LxGQ=$-TcQ2m|(IOCElr#3bgu7)5f1M9)y{v3Iu5Bb44>+k=2#Y^lH`<-E
zXZhG;3&($Qe|6AN{N{2jlY3lORkc<hXeLy+xISK9v}H?vvWGLN@~Bxs2Rg>vfF)q+
zDC^3O&wMCp>WV7gOMobLmd2-w7U8fJ5uXbZU3LQy@3sxdkK2pcPIKH>JXS)9JO^~O
zw6t1J5-8=`fn!ef4bZqptG{c&L-cS6EFVR?TN%xbkxZ=vo#>W}09$dvCF9+XN@UlL
z_O2deA^t5;Hs#Fj20^Tg3nCS}r@{`ya*P*iu*%+5jeTDP<~3wD%O%GB`SET=BoKOj
zypY{D_NIk$O4MK@+i@HqP1X-zr;Rtxpn>mQ8eK}bQ-Iap13<D!p8yzLEEj`=$%4=u
zfNDa&9JA@{?Q5E;RmGUWUnuuEa;NSz64lgz+|y1U%f@M{rY@G108FS-qDWZ9stnd=
z^?ig?u@VgvX7-gTy&FE<*f7c59WZ6osXvVYsH>H+JDyr|A>jFb9LH-MAeo}pvWagx
zwpeeic5=GjbHr;}z33D8XTE#i{rCi^Dpaq6B!L2EJ1|q-_qZ6gyk_`z=TuU~M=g`C
zqzGNrOGmNnGL7WwUB$~6od}PQd;t!lLz~-??1ho}VI}Z~>zummL?vyWe#WMyjgIRD
z+au7|`Cp(;7oo|+o>t&il<4Ke&PaY9!%pjxN%g;WTm#u`Y`hjjIq+_9?B^>5oa^7D
zy*Je*`2txEMB<f}H%F<j+XZ4u0MylZ<%d2vG*s&cbTqC`&Ga4~+F7r5b|B&w_I*Lx
z_{CDs=+*2(=m_P0I5*jt!LuHWba(@-@WU@pt1p8<7QE2w=GaYu(7qoa6Y2&nkunk-
zq?yJ5JB`cE$XQhdq~oy1T?a=Y<ff`Zx_rFPKfeF8+-dyMg9Z0D69F?P=O52iEM^0{
zroO9jX=OW0IJ{SyGWZV%X33L4&TjGHoOtH5^h;1yTygd#G^1`~60jud$Zw;#7_ei8
zUX}4V>b36$q5cjRT(FI#M5dMfBpT_vW<)V23g6O3uma2kyH<+wxX&_?yrkA!q=NWj
z16}60)8+&{+TYlLTV~pmd0LDbzD~7tb(#s~ikahZ&hDBMh-%06h^_=h`sw)or@rMy
zYAb(rr`>XE!xbH`E64-If#NAV%X?7t8{tD+$BFEQQIsjZL4Vumz`$wMmB{B{zR_A9
zQvb;r)EfJK-W#_<x&2gS^~N!yHmgH{eU3veX#f1@t@L>JfmkK}v!IS0tHzHMIX)yB
z_^bwA5HF)POy?kli55KWGQRgg)j}Je16{8vYLr3mHx$JmbCT50YT|hY<;|*rbwFc`
z|3V($chEI1d}+;~gsm_Tbk>PPAs=J{Jn2uiLq>$8#*+CDvq7CJj=dLp1OQ&dlzrnh
zmVhCG7_|i7n!(Y<oCs46)2Q$#ey?nm1+ptU0GvZBC$rWR9el6b;n@J@fyKVZv^6kI
zs=K`W3>4Yase;Ds=Bi2eZ6i5fhaA<u8eKHxW0P~>TS-IZAe>()zy5-omMtF2QI6|P
z`ruwt1Oq<SjSIc#6)WX30sDnVX|*$~yTIelmm0}gXz{||&HG=BWBiG<44V(^LVw&(
z>8Gj@ISK`-0o{$0+vnQ=zR0q~L<?AJmlb8?13H9;9H}c-T*d{qMVSjK$iFT#QBz9+
zWL-0Sl%=@hJuke7p=sRm*qIg6>H;~YmfncAl@2ceVyAy-RX1l*V}~xy|Ii#hBN^3C
zv|=!4A54%QI)iKyUK$4(Wax!eqv>aBmg2D*ET{$eyQ!aE>UZMSzJ=k8aiP$`-$xL7
zY{fhGVQQ*>TPn*egD}CW(9QlZf4aZYXa^}f8Y@FN4cYhb%<LA^AjMA+=Pp}U@aDfO
z`7}+s(2jcK7NLrsm-7>aVkkhvd8hDeyBoTE+Pcq=I5`X`cz;=;LL(1!a<++rf@$x+
zu_0C>r>FMI$}_yy$%09IKzH6tY8f@&Bw7q)KT7LR;GE{^@$`8zPvnKK-2+zgSHzE;
zNQ+G2jd_@9$*7zo)q(itJ=kz~tXWWae+LT#9h^``w*XnUwCLv_g6l!(cJSTPrE1_3
zc{IXow&ggZy(k&@9ss0k&|M@?DI$VCH}$c6(PKBe$harunX^W1epl3guEoWY@0}{e
zTrSc!kfuo`ybS8{c8|dy1~%O=uBS;jSsWbe5k$rkNH^}tn;wpo_K;b|39+cqfH+i9
zVPMPEI*<Em2mji&YHPL*MuH9$MPZW>Xyp8H+G{HU%bo+`-m>Or+`1bIV`z_`=99N4
z8fwd@pla9s>G{zKVb+x*&g!Vfs)@WT&<a(Qav9}%7oizF3UUwyIT8?M##Oig5|0T$
zW^xF?RT%7RyN3Y-VzMj+?)^K@1l(tsKBXmA#<vtiH*8iDB>3d=@2H46*_7ueYrEr0
z>LEN6XreE;FmwVt>!hZW=bIz*n;|N0R@$UBt-7>UPR1BSI>|cqR_liNe91mcL8K_2
zKLncnc2`io)px4Szsr9EA;LYoSaKO}@VguH47+ZjjS#EfLl#RMN)&tpY+08Mlxz+w
z{EKBWg5>SwDeaQP1n#+@b2M_pBjCWo#Vq9ouGu1$n8%uc6+HkUI{H^^7pk3OUN{-+
z*&`B&WnD~>{QYtk-t-nK+gl5_ir6IH9%+f&V;MZBn8`Jof{BwOYw~mJI?6y+NO3Ce
z;pyGeU&x6)vrGN8R{3JhSO*~&@|7yhMg_Y`!_9!yWpdx*v2b7spBYC6_+7Xqu`)iZ
zXQLdc)ViX-_q2-Cr@yoBzTE*^^^yj{XQB#(-lN1poB^m<bVkDZTlD?|w@r3Cx{I>-
zJsNQumL>S)53{b@$(jr6+PGxtQNz9^xxTHVe}jCQ(La}~U$?A@@B=9h@G!~~bhBY)
zJne@Zzh{rd+<<rRo9P<Z_q|R>mtuLy(3nC8cLEEqPtRnrkR{(wkMKTA{6<24NOgsL
zDbB{A?d63rnOWnembx!NZac<7gT#WON*NwPsT7e1#g^#P8$hH^9Qw@75jOGR%PQ`E
zu88>G6VVl8o|}pfAw0imTr5>Wd9FTqgd6v=%G1M3a{Us#8D3+^3N+ptJlK-iSD<9g
zUo!vefp?0+)oyvOZS2KbfpLPhh=3o=MtGtEY|{*jXX%0^kLG-1(W7EQ&C0#Hmc2G7
zXLcx>QI^83yYr6TU;A+eK8WCra`}t5A?HZ(yYdqsuJ2c)LXFof8xe&OZa6==vFWkj
zn82;TiG7D=0`|%pv6svdqtI~kTh)}TtXk5v?EI*^gO${E`Q^nJVxbkJDhIajgpxC6
zs(c9h0Gr)7R7M}L8(0oC4c5sbRgH{{?09*8s3LT00G?Ji)(??{KmUM)b`jk=@@MMQ
zcUqc59n8~Nj79gZoW>E(Y~WZOR8-<So&lU@+fU_5hSxLka?a{JQ^xMjiqK?=IALW=
zG4uMA;kP4c!{Uw8N`sEp%Nwop1z^i>2h0E(JOW+Vu6_VL#<EQ=DUeFWCAU^_1CHEa
z!>@$Lx%n;GKGLBGk&4-q6}sFHeAdb53pncAQ?z%%`CU*4;mVON0_%;tO+So(mi9!w
zwYngg?OA<2!|Ul|myaQRV&<JCY(9c#Zddk1I$nm=;>(Vy8;xnTL%u7|%lpxJ@_MkF
z`Pgpy3<R7lZoF_er_T(N)4*;Ust=H<Vdh;<0LjQoRp<SX{XwA;b;mCg_m(Ep@cxy}
zmDhuI$K6h*+y%d1uIE0j7r$k$^y2$rhe?7|{$DO@D_NyvUu#eUvzez|-}8mm`6y@u
z;JDpD$<6y7Mk21CVrFP*M^GjjDfmCtRFZCu=4^C+!y-CMO0na9dbJrL{hu}?8Uhn1
zi6TL#W$T<AqWv$Cwd6oV7UZ2sL{=1j#0Dgq|C5-pd57~?ep1Acm%F@Vm55xXEKF@f
z<i8w9)X@<l`Yxd_Ig4_sefs{oC=ibT-1vEaDGW-Y|MDD><?Q}K4jkK>2>%A{y-C{$
z6bZul5hS8S7QOL}CjY-pOa8$6<l=x6)-P~V16n3=NR#iRF)0oH>un;w{-3oj`M)JG
z_5a83W#}X5)e5MPM)ng5gOc^D9gxI-+X01hDwRt8%Qxm5$yU89;6xMR=ppm^D#U*|
z(eQhest;I8sweGtXE*(mu>QBBO+pjhWxOdZ9o>1A3UJE@#1wtu#Q&df(d>;C<4gof
zDJ-PWt}zJ)YVo&zz^kza__j#TRgH@O@!<*lmk$q9uEft*XCqE&8JTblY;536odbk(
zA^^vi831cAji%WZ(8xMw$)~2IPyu?~1kbB|834mdNlq@7keCSGc@OyQp#8`C=sz_q
zjyncC5ly-+t$`%7A3#EH9>`@p4fDENOh=W{m^ZWI<_<#A)6)}@lg(ceM3&Q4fbY;7
z^RG*i6LfLFk5DCrc`R}b=25^Bgs~7Fjf+r~G@{z%W26QCxvjgbaRCEI$@R}D{m0fw
z>s))SvAbF{23b*Zv+8%ohCvC9EMy5cwr|;a@9_`d^^+|2ulqs;7!aus`13*8P^l-~
zI<Xa9%tyao#J`=A9Pbziwu_0C|1&*-pEiObV)V<o%YK*=9~F$`WUFT|U+soK|GEsT
zb8QMb1h)+a|Fbb9|5Z$LDJqr%Q2K<xhVmMhVyDULAOS8<Vv3zpubxO!Mnul%$*&*G
z`;W_0z&87?1CMNQR$Uw_E^9C;3+UTdni>%z6Ict$6K6~LSAV2W|Gt5AxgZZlf+En^
zU>pMRu-WGfTue&Ae{Od|Vim#bh5h;0ED9R7jF)yk`GU4ZQeRF3+&Exbpn1SqfsNn3
zei6=p<-q;#7<5E5$H=9AF)%P(0&}j2EiQFH?KtY&tIZPDKQ>GMyqsD~Fj5>O#KXrc
ztE`eVBw_%Ug3I~(x%vO?#}qVVxlXhDS~A{u+GTwGzXJdIsG6+`H~}?IQ##o{C``=%
zJVO6+)C#9&-a`@lU!F=dA0>kY0V5@K!tV2=*KyPN*WhbiBzgWHbX@xXT()}Z0OOh}
zk%-91qP#p|XJ_X^;E?^C3)qLHet^f42A(GhMn;8%1aB}EB_-wSQUE8@>+QBpUa*~Q
zTxa$>Mf3$v;!K5){?iz5dmX=*i{DE|hX0IZMCQD$)bK2OsC0M}K*+6`>*yoFo!Ie~
zxh}k)okm!?V*r7)`H5onf94Ybc2-Hka4tn=&&)srr|p@&_K{&JO)?DX%}+};cLvm5
zbJkyB`rhAvn08Wl3}JPqn%o$loowD)dxd-!xhKg6Y_7yb0f#JA1uJ+9lZ{bbYQPRB
zWrLPDtb|50ftP@8&})Sxr}e(mhAcc~eE;aiA|R5d;2Vmjq?<?7ZCQ2>HKOgdbl@RD
zX=*aPE4xdPqGaXt*g&ezo`uXXyPUsSA*PH@goBI%T|&|pnhab21`fff4d(^2ZDd4Z
zVKs#|T!iXjf@T^S=*~JpEsj)oq?VRhpKHr};1^IivX=`fU(RB*b+#ESe*El$F!co#
z2V$B{`?i*@E3~2a(gcou9WK?_EQ|*>W*%G+R=^SZKF^pZwQgIJ(+if}>YGOD*et;G
ztCXsKLrypXzVI}ZzK`!fPK~(PCNM;R;%@GSqSOuk1+J(#&gzb#<>dRb5+|<T0{2&%
zK{YbQgNju9(NwwAV)`Pn%}-PLlXTV&&y8#cb<sw39(|7|Y4{phcU?&tBpO;Rol+v!
zNcBH!4*3Gc4-6~?$zWX0r_9do@7Ww&P1Bpo;zwBhEGw<(*i$%@B@XuGK9Y?u#l~t?
znk_2xAB(<}{PSJ#5i81HH+)${QTopJKVh2_lF<txy<jqWYCQBR@9yxLzoXxnX&;0r
za@=6TsTmh<0&7QANU?=V2m4f+T_UH47fb}KfqR0N->Ax4!@(Lb8rIN=I4fOo`Oc4u
zMN({ZR;Y;N)=q<KIOUN;ySmYTM0Z}?qRNJ$Zgds2bF!bsFdvs`ox-5qMJWdtk$BX}
z`4-bzRzV_I@9NcN%mzj0j1pGDLzRD1jrREf_pCg3w@zIl{cj`@d$3jbs%IbFh#gH%
z_IOItG9b~jT3a-nFa8!6nKNBzvNvJX@Yx=?Ec}|8#&sKOeZTDbvMvLi3_uLREti4n
z&IcFdP`Q%CC^z#bweR3y@N2`Bd3S~++c0dqHodoKEVvPqZYwDx0<LBMHFBCJso(g1
z2Z@X<yd8fiGCObE7+kf&KaoRmq9u~CR+pm~B$u#l3Zt%?^S;C^UP(fT*Lb4bUbE~=
z{4-Fg)<{UVCA!Y};#DZ35rc5ZD+leCPU_{FW&2P!Srsj4w^thcwsV&dp8#?fTv`hf
z(&ySX*K$~%Yaul69jQEDj8d$nqF&Teh{>$FS0OE#Ru8z2kA$gJU3>1H@%Qps;~KkB
zet^ysWs>D#!u0`?2Co)OgQKI#<>d_4Gv(>imHLZV(D6);QlU2+KkA&}l#6x>FbxrZ
z>5{FC$`$?HJ?Q0o(WIZUn^#It&S5B(?4>LznNyI9kSnxujY+bj#ZWVu2g_Svl>XLG
zMe-5hA9dpl7;ma5Vv&m2d7{fncz4lf7inAND<xw;GweNkURkC>x*M33oT5sjZT(@Y
z_E-{B*sl&MzUk6vFQQ1c8v6b|2~mPlMsnIUuIl-KQAmEzUI3yC_q#BUh7+%il?j-Z
zoXKF`xsUJdH@%S?9Nx4wafntnz_rAYEWM^O(*d8aa^0peBJMaKd-Ph;`n)$KaXoKI
zJUBKZNbP^RMC7rB`obMwsoyq~CldI$nv@Zv;mwIbz9EYh6Su({8}5DwIZHcnb4{RS
zu`QBz?qHCS<@`Xy`R*O^y2T36+N4K2pT<l?et0`NgXy{`drqV&r`ja4AtTe&Pn&a=
zj)C0P+Y}6XAIJj1yGoiC=@D0DKjM=8Q&RvB`bo*htxqbD!JOK8v@<N2Z7EZw5lJc5
z0siyuPH;7>NhVTvXh1vLZ0Qk+%~hK{z2U2M@=cZ6>z(DOuk{R*8n|;d#jJ5V(zyeC
zyDDyR_}t3NwQ9(|g#|u7dTTe@r9WOTI-CQb+##UE!1P+QGsJ{}leQcj9tP^47?e!1
z5ru!dG>iVG;uo3CukLu&FgugX)5~13Q#2SiQj`>vba;!7hVc(6$I>eQ-nW#IEntYR
zr^szKoo6wOkXojf=vrl5S!%e(P^~U1U)v_3lpIRGaJC1cn)a!0GZrA9*siTBmz4x}
z=q}Q#oTo-Tzth;Mswe(IbyQ98sUEv3oH;UiMit**YLBb(rlSkLr^BYL5#P<i8V}~q
zs4BqE@ku>AuIHO}JEZ3I0Du09OpMCHnFC@xZ=lY14tV$t?2KXr$dajOY0&^46e;UH
zp~G^_9Tcx6qt_bLKjjmkKuH5s6C21lW<KZBd<y6GOT>8c41`E6GZK;iNc*Cd(7vH?
zAuYRUCsUDV{2}w|XN`|@SyJIN+un==npXlvQ3|S;f-Sz>SUG!938sgKZ6Dv6EA8{O
z9<8>}tleL1zvsSL2zadESGKBk`&8MnXUj`kmHA`#jV`Kt2r6=0qc(*r`?;N`S|btv
z3GYtJjeUo-BKOH#m3}Xezs<}=3LUN%@S_(%n8ExBP3I0^hz<gEFO^E6R52TyGQ^LC
zhI)XpmB8~<k=(11orr*A*29z>^OfIX&AQK$>#Y8>1DSy5WgJlAU3>vA27UpGIwxie
z_13k(iM(1g+I_WH0so(xrS<RP=Y~aYya-j7)5>%vN>ifvgqBrpW`gU=WDVRw4d9uc
z=pWV2wCl}Awbj0g&bJWPU!gkNs=Dy_^SGFV;Z<a%A0w?OGGDL$I8&br!8@W@DLu&?
z9XbyzF~*kRwN8Vu?E_YvpZa1@bhL%&rCPalNiB~!B^8%Vv!-P<Cp8jD4;+TZ<6>V{
z^jGd(nA02E)60iT&1GZeCMINyiW5%rlNY2aqs}(_hx>{B4qr4ROBA!s02X}`z{FjQ
z{P-Suv`{8->$SQ;n)0JB7dVDb0V1gfAE^>5_?Fl5I52$R4|)u7JjlF%N#=q2E@dCy
z_#(5;*`GI2w{$r-9gr7ajduEHryUB59Mdzi5?R@0hO@%k6~rVKD;ZCyvqF5*U1nig
z;*Ma+`fWT`O?3<5MQ0b&&Lq1L&I3^O3rq|uisYWPQP9#L-BwxPk@mp)5$M~)XOQpg
znX}e7ty`}QWsO#5BBhZ05pC{E+xJJFhBmvA$j;nyI8agXSD&r2Pyo=J{sA2(N*n}~
zlYzuG&aM=(Pkol(^Ih$4MZOQ1PG3ST0C83vkQg6=6J&afps!R$O?|@>Hwt_qO~u|b
zLEQ!$%1}bLG|Ww4uGIXaDyT9P0>m(i)}n<I{gF~MU>0P11*1P@pJsYSes<7>hc`J!
zV#N9xw0G$-*FG}SZ^Y?_H5WpsC#ZJG_ttb$(25mmxX28#?ES%^Qv_(UsydAj8_v2M
z-Y%^AgT#YEIp+eM4F#tUiZ+HkMYU<Y)8DtzJT{lOxBJ*v8hqB(dnu_9ZNTwr?k~Cq
zJWoCzUZ?oAwg(cO?_RAd;ldWl`>&bXPXidVYkRvwI!R>yvD#D~(#rc}36u;}n<^VM
z+SYxhIbxRv;>lmF3b%jKpdJINUAcjHsgL;*1f<V@<zNbv4=URf@i@yoE*wX51au9&
zjyNCE<JN2|TYmY*V1m1sLbe=$#T!{}u#me?P0vbGyENIPS2J-zGVM;M)%##HA%iku
zeh#*cqrb|6CHHni_Q2rc8E3jTv1{c~y|EK;T8YpEqYWamj(GKqC3sKW?8kqH2I4jX
z>oR-EN<ELYD(9O?L#aPgtwl7WNYTWIm+%$I8N?jXSe}U#O)R4?jQj-D>_x%}PfYf;
z13;ii>m0qH<%#qHvr4>rZA~kYki0XTij;Z&+w9NILg&$lQ}&a<v&-pnt~n$B_u7-n
zqbI+_1lhwcEQ2!UcN{npltkK%@!os9&BX3R$PyLx-leJGC4%RT$|hWGFKq=|d$~w)
zS1sw4^i^S=uknbYkg@=vy^qu#VExj^chaslmL~+b#hC+q$#Xw7IynV&XL;%~VAn|P
zya4L7iwD*~CHRTx;}jOr5}+Q?js+_)-hLXOWLYtx)ds1^{t~o7J9OWMe;WBLQ)W1<
z$Y|7+>DQh%wC5W2qs)?_3&R$N?6$qw@y22!oqch~p2MVi<o1e9Ut~q{YzJKJ(x)=8
z@S9|3#vwjow8g^bE~EE%UL8gDX*Mvu&Z<<(B90^mNMo*#_;u}hFJVB{iySfvF$6Wj
zbbKd%f#$dM$DNp{Rw@qJQLR@hGIDf&b`h7Tb*)3%RXLWO!Q6Oxwsn~23oQ<^4<WXP
zH_>;v&iCK32Tn^n4`|%qzhGPH>o~{rdrnz8Pj-?IgKv(uvbUOdnNm&kc2H?^yBMyG
zI<LG(<Qd~iZxG_S5OF?Ja13+Lq|9&&n2g~wc+w1byleJ;S>1|97px$W?@`gpqUaj?
zOBMU<m=={bKOe?-lSl)ka#YnyHAt~PQn$TdWFFc)0@g7E=QF^?Dh1ss=ntx7iCr$q
z<?>0E;~-!g#A|~JLot@9BslN6Q9q$sMnK*uYvIF0DWt!H?v%9ii|0C{Ppv7XkflKw
z&tz1SCWRvX9sl<l3Uf{rrCP(@^vfd!MeIR)8X4RMK*L}z<n<e;V=DDpAcrE?ax{$1
zK{iEy993XYrW5oR&rD7x0))kQ$*{xQo|!5)nAFspNvxZzYTi#GP$bPnc$-Ohby}U1
zSb*k))JJv3xU5cHW^U^1IboW3UB?DzrXA-KzzC9{RqdL=YGWqbO5M}n&J6|wXQg?j
z564AwO<NDCrf%6JCD(+SHjyB$6NBl<c~Zxk+s-)h9*<Unk@&45)CXL(X*>KQk9R3u
z*!A(8pqkOZ%nUsfYCFHa_7nPRV&f8a<)olqvqaUU+ycrKAJHT1@iVln<{*N=L>BWe
zP!;SYS<bT7^rJH&rO3Xw>o6V%05C6_d!;(iaqUz1tCR~@z_b3<0{8s>plY#O4<{Y{
zEk5!k<s-mF+y(&N^#<}gBbx+BK}5WA<`2<M(eiJ*8(vQQUJ}OzALN1Rpn>A&eyG4H
z9^-h&DH-Z{U#FU%f>-;0F8qKLTLI+fu<H3i1DS1chC}D&F(;OdYNBU?9SrALL$hvd
zCaNuwxE?QbTFoyMM@mZ71e>YPM^qc@%rh0*uuVxih(#22Im;nn(r|EXbeh~S6S_xO
z%OQe{wdyKCj}Xx5YPc?W_I&j%>Cr8*MVWIUi4DbRmY83Ui02x9Zw`{|2ObTfII6Z2
zBWzjUOf>KN#S7?n_g1{qmmCLT-O7S0M1yRq=E5eBUlx|VomW-t9tFB8P+iVA<=w36
zb&mOV;9K@4h>zYM18I#KG)=4RZz=^_fGX?E5B)VN)&uC6Ze{)zAHKqLv)M24EWXg;
zK%#-B1#%$hiZwGgzncvSp+%L9!ZDUGVp$K&OjO}P=2?4VJ^3}*4{QZ1v<(3iSeI3_
zP)&P4;K2mMTg^*A_3pXBWR^66BxPa<(1Q&F${Q1@M2aO+lzU>dYhI%tUz#N&DGHre
z+C8q>1Lv}we&PQ572S0X4>O)ai>QSX8)ks_(Pp+1EYNk?Z#zZd9fs$_L=D(aR;7z*
zLsG&908;XTV3t4BnIqB)Hc5N>HzJ6AD1md>6GEZWb#(EhsF+IQyA`yv9fs6|8srH1
zi%`$DI(v=QLXI-}91267ZR&O_$OdfY=c(%1L<$3O{R(K(-<<u|I}TXmDXt0Xsxxb%
z--@{1vyW1DSr@C@7}dCU!cuAB^yW;t=RYMNK{WUg+weJYybRQb5(n~=gVYS#$7wwN
z28|!WCfA613hqXCpC8Zt{c7CGq0RLB7~dpY!9PlW&fwbjl*(mfZEqDZF~-kt^f%Rm
z$JV7LbJ7V|+WhwEG+w~-r*g}4-1;-;_MNY+5{P$yVrXD)VhE5tfW=<KpG0nLelj`V
z+wKmh&^$ngY*Z7eZGWfFNlYq?aCP9Y8<I<1?Bo$Kcm%IH8>isuluoOEo*vpa{vNEs
zq*FieG6+MiV6T80IvL#*9*-;*&NJ8SR6yv8f?8{@|Ch%Dj{N;ButeEBID9~N`Z6a$
z1IWu<fX26%`&o^kBc2tY%aKsM_iZq-0n^wX)pScw_k$6ceUKarDohw|dq9ws^Rd=i
z+87_T2&=1bVGt&>-Y$JryGbsg0Z^YOOKyR&97!Vu{6OnoHI5oqA1>mYE9`E7V~Wl=
z!%3vAA&^KOC6Q0{Z#wV8He^Vp0<4~Gb4P)O6;-;8F9*iib>4v3D)~Bt?$i5sHhrs|
z%0s1LJR0|K0-1tGh7bKe$5$?SHwNv=WE#Ji>4zubi0qn=HS2VGY-WMwRDTyyF#h(u
z`m$OM!`v31R9Kh^K14n8RI>0f1#!^`q(9Aui9Lj_YCB-H^$+<K-G*KinvH4hP}FQo
zm_KZ+9sk}sF6fGo>jm55;B5Ol#Bz{ThTiSS-K_I?jXOCXkqX^)eTv~tZ<(r@v^nTT
zXsEYs{$(bjOY=VXtx1T|0jY;XvV%OXV1w?G0q9$HJhMCDRRh6_ON3waR|?qpI$ub@
z5%GT7@s<A~&#Kr_8zFjX96W~7WMl70Hs5TFC(f|wTp|8Wt`|UyIIQ{lfgDhK<)#7>
zwujjym`MpMi+U0)*rw0?JpOBx;v5oUH2K(!ums{C$0<?zX<RRWQd{yxu_TlU95T%W
z8n#?lvliem)`4jhv&)PDODj3{>HrN$H-1D)!G7oa%!yM-|DM#uP)0cd7>1qx9dkB`
z_ZtfgD`bnJRxlLCZ!vP7?4OG*?k<AkjV<(LoI@bC+*nE>s8+t~<^pKA4N+@im#nN1
zFzwk(2o8F${t>jQiFmdW|9!h-P7(rJ*oodX&7)Os@!fIJpD6~SMf`q-gW^c{jmQN{
zdzfO^4%hRI^b^^{h0I4=G?;eDljuF_sz<>eT~z#SP5LxSNcRIc1o^?A7zh?z%G&+b
zZO`{4m^HNo&zMhgUp(h5`Mk_cak;B1T2h-w@|-a;`Mhr1dWxD0O|tPBqwBbb;Vs+g
zkMM4x<5ZG@@@lnt=brOp_6&%dqb~%eI~Q38n*ixAir}0wi7CnEcGz_2$MsDtBn0PI
z@h)Ip4HO_Heed_E53VBIy88yUHjp6eu>PNa&;a`4OoCUX#w&q-LPQh=l^~1&rd=85
z1h8VW1^J8NQ08E92c0_c?})#)3?B+2B#wWJwvWX|&$t1sn3k#^VCDG*?P16Hj+-}M
zb-c;INw$fjd|bda5Q;RD03AYn>@foQ1+a`y`3mUKX4c};)2(4J*1dzk*C(s1A><p;
zLx6wz;;9~w62}R`X#gm(anr+G^u&m)rq=s*Jyhc}RKL5Z5^zfP%lst`6DDB!%zGRd
z&z{YqIYr@vM;J>^u9ySa2<PHRFg+z|vFv64ne@SF{DI`@BHOa>!TlZzEkO>#bf68K
zF-@y_J~DoUncZ`X6=S%Q4;NM~gW%m@S^L#P+Hy!`mdBC`k1fO9uORVDpQupNUlcqQ
zhGd&^kA(Js*d@zm*t6Y6#x(=yt%Yxi!myYSs7`^Ae2uLxy7FR_a#x0Ru};aK_G08=
zQ<+F90>lNChoho#IIIcFNjtzADx!{uYdMwAJzFT@HegSc^OS?R=UN)pBDv?=2x-kc
zsQ42VIKP~HD_!sHcJiId2hU!>JB^^>;?)Vq`)sw6%}Iv-rRkKTyg|E?$0&%I?|rqI
zn2*QP&GU}h)@3t@S^rsgPDf*R`nu|)f`x2;yWRT(MaJR%O`{QmKjMNov$I>H1H1i!
z=^5%|E*f0%+VmMiBgq!plk&yP4pe@lCnQe`<Ec42-s_@-Qsa9R%&`PC!F}D5-=nfW
zW!7z<W*1A>@t$!b&5Vw)o3P0EW`(4xZ2>Itxh~GP72_D(wl3I%Bb?;kfu11ou~y8F
zjY{7(n`dGEXS$2g<|c{D)jg3y>Ce_n+0X3pYW505v~J0;@3eFiVU*;W*vBlw2=4cN
zAV(LR+e`d{6Fig1XHUQ!x*nSUH|Z%00gAR-aZ)goGVEm42x7?d)vTdIY$Qsaqs@G6
z5vEbOdnA`Q;nRm@6sAEZLY`lM2+&00{ZlIn(a~a4>Z^EjtP5-JLok%_;dRUu^(o+$
z%#{5*+LT=U)uJ8re7pIidv~0PBGNyM70zipqywA1sli!D`o4-im1HBbGXUtVX&jH<
z4O?pk#L(r+?j(;>{x=6<I+gLh-i7e@eWYV3-mpGX&;d<A-T!+2%`RmJ$rscvB!0CS
z5d6kUc5xColKOi%MZ;fe+KLA`)q}TW759<U9i1{;&xp5ckj;?|#g-^_>u1{K8QV^;
z15Oq)rs49N3Kwa%58q1+$m2Q0iG|dfGrD9fh<EiKuX6i6-)pZ6#ZZIv6Ed&j&0L}f
zckmK*k0@na7aiZMLJuKHWZv6uf2m}DT1AVE_DHXVbPQUmOceChRzFQ|7>_I=IX|TK
z4^$Wu(Kq?HMlBhq@O26596#^S+^zoYIC`{OH^LarEp|=K3OM!I#4~q@Suky-6%rGC
zw2hU8@9}9HsWr}zNI>DaB5oFvlB@O+C8S<z$vVluUXBSLj`%_2*0YA93)M7<1WOim
zV6=jY-~oLDo0XL{AqrEuNcc6}-xuIg1QSG~s<=-|VBdA!<&7r;f@|1G>S2Fd9DazS
z^tzZKyHs<p*xEoA1UXQj&!-QE@i$@)W0Np^sP4Dls>+0-F_y7&;$dv1{5NO_D_1}x
z#QMw50nUC@-ab-^S)1}W)|ngN1^TMN)>6cv3y)REq!>3U0BMcHKC0i>p}Jq!-BEvU
zq$mpEI^>7Fl~=oF6%NO=Ex#iEb_o%*Lo&`@rdg9R4=r)|ojlck9D}OC=WA^1Vfy*S
zIS`_b$W;KNO_rz|M`!rBxcI`zF-ve;r)RR;7MT@_>R4dmo*;4X6}>s%O2xh7Gk8Bz
z9*{GHi|dIZorK|wA}t$yQ(jZ))<SMt6LAW3baV`r{Rt>F_FgdXL@Ab=gLU&*4wu`S
zuBL;aPU85c(W{n#NW@gAO@hpkc|8bEau?|T-Uloe{7H*_9<HzaE~3*0)e)k?2G!w?
z<CGgE)+G6aqaV^yB5+3AVzA%ij7Qsw+fD5v){F=<DN0iiaqYG^Gsb8ph`5)NItY9F
z0{b)R!w@M!HaPhR-r)OH+FeqTLs6g%b0Mx|Dn=qDKhq{##^sN{sR+J5Rqj$>4r>ls
zeHNR~^dlXNbtU{QT)AbJ%WG-98!b-*g@jBDwHra_m+wD`Z0RwaU-jyn&4-*ue^aRC
z9tL^6koF&*8%}?H$o&8|TWWVp29D>o=Y>x1t~1q>g2*xg$V31z?xfNH;#0|2+>fYY
znQ?uNa%&uK6=@0<jbISYgdKEQSnfYse*R&*195yOpgM`Pfy#5);i4ag#677;96*tC
zHwPg)R>42RBCACyc@>ZnUd2!1FE64{HTM&F4ZJ$22?Ph_ONs#^;>TR?@hV<1YgC&h
zD5!Y;$I~7+dtvAlkC&&LZD8ue0eP(17^c1JpS&lDH==%AFp^juC!G&=T_DM?h2xY3
zY8oXdpKRWQ(G^U2Z`VKQ!+4#Pn?7j$4)PPau@|9()<`yxDlbuP)J=QLD1|{wp57uG
zl#SHe<+G!ePZbn9P8z*S#@|^b<r7#%!utKIs~(V^GJ!{?+Q&kw(aYFL277vt3J9qg
zI&_#5a!cHr3RD{HQ9P$@xjiIG&#wyg3#)0KPi3M12YYAXlt;6+`6ReYaCdiicMTFW
z!3plcEjYp5onQ$BcXx*%fd{t-mj`#*;r-6p@6_)80b5&JMNw2S)6>(_(>;Cnb^Y#O
z`GuIMv4byn8PVdY%$`#vs$DFgDt^|zY7<>P*rc>AX2aUD9!Q6IA{Mr+LV(G%T6MWz
zBmdAd;jx#Wdjpc|?`ZyQxSr8bR<1+I&)S!x_sY*Q=jc}TO+%FWJhY?t^<_0aPAOxV
zAGLSJ%V`-?&C*@1YN%;-74{NIY+t^)hQN>vFT^qM6OOXpHPbZm^_SMh+8FwcE+m;W
zt{myz8pP?(RZvWQw;|yVMk+*VLD>X6I&0S!Kbeq^AtYs(vDouL0itF@anpXEJ-(lG
zJlH?*V<KBXJT@&|J2yfJ%e#6_LqJY>5G)B=%qYiwl3-|6Jl|D3;e-#OaQ7bUMzU0|
znn^~sH{?A=)Ljqo+_6NHZyho(P|+Lmp>d!F*3*{($?c<!Nf7@6iO;FHHw*MEurk+C
zA=1YFAv~SzeVSrPhBb2rKL3Q5KZ~D5jlL@SMCQpGfP#q{T+R`WyzbkvlVNkTtK&2Y
zw+XK*R5-Q6NiCuvBC2C{W_dB3w5PkDu$;%jx~lzU-kv%k!G6@sK`+JQfQzg1%zg~w
zN=S(GcOH&~s=xSgp=nOE>$`TFK2HR#Mf*#m#F@PTO%A5vmi!q_V$A%5>`29up3tBV
zV~!WG?^-eg<<vR;#Sn4*1=Z^oM3GhYh^K+v%#sO7bGANH{O2W>gqwlu`XCpnFzpl3
zYsTj7Do9WSNM|Qr&>O<e0p6>m9*0+;Yc;8~9T|_?0p3<OnVhm(A?%5(RE#fAq#g1R
zZEE4n7V|@~Ka%vU?l`U+!hXosZUdUu?mT+jc9#Yp{`}MlN?e$y3D3osGNJk0=3Hcf
zCR@iQIkHC*fA3%)!4{t&AEr1ZRLI)rla4*~;c>>KL<i4N_}>xN$TJzJ&3kP8Um>T_
zPo2-g`XqiAsT@;aliiqcFpEnL&-lUUkA+e2CY&fck5Ru5_g>zr^OG*r)0&dVgJ}!0
z=QbRpGlOKU7Nc1g#+H&E9jhF|vmRS(!&v%9S)8%)lx;;0^Q{!{^o0||^$<(`BBgmc
zzn5koWM0T+*DkHDE@Aj5$)9j!FBdOsG8?0PdX~tvvo}>?<-VJCW^z<99?jsyGT5k}
zW4p~1HR*6dNJf1tO}kmZX_u4MnMu9wLmjmvKe{+F)<!4>oFoc_lsQ<XMb;Vo1&<z`
zj&J$B?v|Ca)MqMm3j669-+>GbJpK=4(vs_LLbi@!0fG8^p}qLZ95`7n0?-kB20}e3
z<YPvRXAyjPs#K77bWMzc<`QgtECy1I;l299a=8G~;I|ze-*Mecxux5xA)6ef2N8z*
zWj=DSyTqE@&I&Lf-HdoLB_G2ewts*zq_(LHcYGw<WrH=pEzntrPpkb|T@I>ql|cCw
zbQI;&Y__)c<Kt(4N*PTL@ZYtYc(q!FwRJ&Pc^6M#@EcF_z799f=CAm45JI(wzF4@?
z3dz`17@JYq>^ROfJZ>@p47d$oq6cIB7)=x<!E~3ebv7kVZ*9qc$;ABLS;G9g>XFm;
z`;|_=gwuCz9439jOOI+MA}Ee&4YS-;;-h+K`-}P6{_{VX8$kXa)0^Es7@4v0vZ|L`
ze8s4WM5b}VyOFr8<#+g^zE&Z}Iw|E$xw7;2Ad@-mwql*sbfkH_*6|<b(~ld;+>9LH
zi<`w!Sx94IZYXaeer>I}<Zlco+n{DUoGiyfo}!`I{rc@z2mPTiNJDCFhrPM5MUkt8
zSk6*hB10|iJcnY6O&RspvlZQz3@@{TP3tu?{q@izb-=@2N<HE9nBQAkg%6vU&hTXS
zwapJY#^1KeddOno`jAcjJV~%E5_k9TW5l6`^SP5&e<0h;$_Kx#-VVo<Kv|Izyp>%C
zZ%$OTMy2eBb_B(*cVz$R(gCv99d5~ZkVAzmf1Oc&)lx4hXkjPTyYEl;1L;_Z?9aj{
z9si(B9FJ3tr39#^@qF8-KE383pv_S|85j1lBoiVO`ue(M0wCUwoq>h);&W1PxIpyV
z9~#B6MoDVs-a#WkRP!{QqyOV?P{ERh2}5d4&CjEG9r#k{tV*J>oJpn$S1L!mZ1t-#
zy(y_;rZeTtgwmr~=kLoNpx7OaODjU-tGnRU1bv-mfqh^$-tLB=B|d}_V>o+W&qJ9>
z8r<0<d9)6EG}%0{^xTSVKN501W3#eCN^hw1+OhuFKmsDi&8{l>QKDtbMK&0f-6prg
zj3AtGcyPskzLkbAW29AAR?Dfe9sYKO`fqr%U5w}hYkDp-q-|Ag;Aur{?ziH5E(0ob
zS4ORp(aKKl`1zbe2Eyp38n~z$IeMn^p~~LOkw@K2_bUk2cE{3tCj@>mO_=Mqg%0m~
z7Y7-3(9NUD_eh=+l)Ba_?bBepl-G@@yNy-_8u|Kq2dl~dD2AkkgY%8wS-BhX#cUIW
zu_^3=y3Up_j$wlMj|#4qVKz@%VU?U5S6t^8$j>E1L?{Th_(>^k1#Sk&sD$P{Hhkf{
z&{q<_B{rlXlKn9~5X<ec6t?nsFE1NqKQapwyd`ik@irPQo(4@DGt#+i>8W1@VVyOz
z?c+N6ueewWUu`T>=~XJv`&}q=N&BOxLdK9^em{jsnvJ^T_^U>lUT*e0PEwz?=keno
zV#9*6q;0%F{hiU7q_j76tA8B-be@1kUO-a}E2Ct!EQ=+B&4#4hUDtz=A<;4(A`Z}W
z`;ziL2lK-TwWw>%mJN(dEb+8!#KzO5{?cz;a4J&R69Z*M>+R;mP}hECzAQCZOfBo3
zpTQlQ??*&6SqtZMCv4-NFV=oG-<R02xOzc{>Dz9<>AbEn`93LLONr@j6`-bS$kM2W
z{F*SwcN$%2>f7Lv|I0(}<I~L=z{P<@CDJke5HDstI208%1f-=A3a95iF=e}rcpwUA
z`CUXOHAK_dd4KSWCO)}{Lr8k!E3(J;W%3f+8zA(0-}w>xY%^z`MeE%I)w$5S_7MBD
zi<q}kc;oC^>|t+Y#`xEP{K@;c%t~%UVl=_rE&W7?#DK;UTfMKlKsr*w3mO4Z#QwG#
zAGs~jgMnC0p-^)D65pQvHx&uVp%taC_o(NJ&mqy6Adg`?7hwYKm_&}F0|tEOf?b>R
zbLZO!=#_<&Z9}Xg<a_Ithw&*lm0GD@`EjdZ>hnh+kn<2~uErWq$Fq5GAFG-WeTQ*h
zsD%JTms0|{?!1i!7qHfgkbanFU!8B_Lxzz`It}eTkFy$oSY=z5M4b-BYIqL?)=!1W
zarkxzt&x!XPn3bE#pFN5kbpF-%{7U@VJ)UdvmxY?VrwscZSwE83ERkrLA0sa%WEJG
z?!slARu0Kfzgd+O=1L#G0YToQL#>**$LNsgX8QA)Dx4Z8-|fOl%y<Rvhd<f~?7Ji;
zxqb3-<dn3V=BDfL?wz#jf`^?CCxV`jXM>~42ycSvHa{YAmF{2kVZox}ZLIlRL<^%m
z9o|mdArw6VQ8_zNy2f4DDH)F(D3bsY9eZ?b<ge_2nO42}8*bzV1fP3K*qLVD9ng#e
zt2D<*Vn^7Cb&_VB-)Qmd&S1IL9LQOZkSFr;M%NXG4t$P+tlWi?2TwR*4))W-Kr|t(
zs_>QOm@fgs!q-)9im0u#EJ{yeVjw4+6(rpveDA9_<qD-cMY)79ZZM$49{|jO6(6L{
zIPjF|CLNnZ+}2P0<@8WoiHeFP7M)Vg$y(FOGcWr$tI1=(oZ&?BS`_N?$jOI?tO4Iq
z;U!If(yTh-S+dLEvbK^3{cT%8<5)#l>VJ?rj~Q8G@N`gI%0F__Fbn2u5EE3ST);)S
zC3tF%=`1L*_#I~HxP3=*_n0qmy#gO12+DfQ88n}F85a3{=|4fOpGE`+e}0L)7qO_c
zHW7}UvYp^=KYtK89}9m{7+m^jjoPtqTX4%NM|sNfS#M~y6&h_fyVWBaB^TLDL#3mQ
zbBwp*BUydz%=S?s1zJHPcNf?F^RJY-Oq>|aL?i+WY{kSQ6ZxN|%8L0hS)8=`G|w|#
zxMJ_2W{T%i{0DjJ3e8U}`?#&m$viahs@aopgSSkDYK<<#kRnZ~#A#OC&}=th0|I4f
zKM=i352sGs7;gr2{RfDWCoXo#c9T-8*eL9|aI~N+XOdv$JoI3IFr-(F1^+-s0@aHM
z%wE<-P!!Q>1VO~_#W%y3#L}!cfaogPU6y2kj?Yo`;K+Bedw3GpSv4)oi!A=i3z8<d
zRs5#|^uZGw<KPc6h%nIP5@P`!j=|+E4|PsNG6bKi3m(nNCQ6e{2cqJE-ISV3B0%R+
zN#wB}upLYYNw6bAV;k%|26DnFdy7lHLBW`CLKV3+!e=uQ+i#6@YYCSkRCe&r2S-9l
z;l!T%4v!_7^+YLqf$h9%Gb3pKzC5ejd=HCfI4(`@sOg&R$H?!wHtr*&)UwyzU7Ca!
zT4<Yw7Z$oNRvKTRs#z8Nqn>@e$$u|+HaoAz_$xzd)FMi6zgYe_`W!r*JCwKb5}))9
zW1T`gL*(!Z$qZZ`({Xu>OWy!Sq|o7;Pg^2Mx%{?qCx1_A;hOT$Au<5ObSntT69r@L
z^#r)Dxo!QP3|WC8KInMY()d1*5%p0T^+4or4AWgAhVX8uXACxXN5Oom#i+T$T7)yS
zuDQG_eF7`1zqnWZB}(4)R^7?@ZB;KNV)G!ZFGIUN^N*p@E{m5F25n!*pODC>_8#nv
zr;$10h8ayyaPZ_0zchZRNQHzkC-+1f5dmE-TPD%Q>=-@#bZ`gny=Zd4)7j<K;K+8+
z{lmP}mzflw=AG)v?QsW2#8UfXg3;4m))ccB-!Mu};g1JqsSEJPdd@eaqC;Dds8Y6V
zG4%x5-=e;T%{SA%)Z^R6-WP|R+kp?FEcN0<avF_UF5i~@?>0KH{RWiu$R0jFn(Tjt
zw*M_+{!Pd#C#3iMFQNJL(Khz^_&Pc1P=qT#_QXQF@B8MP&0Z?OUp`Q`#R&-KE1kI`
zgQWFI%%0utLFxGYH8%z}W|aYqMJm3AxG7gQHQ>gLakIzHD=OYTzj_Wu@S;AAVzw7D
z(_Xx&40j@{2>n_;177{u-GqpLOdTYUvX@fdS$#X++bw1JF(UmgloNKuH#OP+g!`?#
zh>NXNiX))Qf@;$;^f?Q;`qU4F8(}=jc4yx!h=+ISv6MyrKp7D4*A0WK2@mDEzM7sH
z<vCc;kKxo!;s`i@d@iSC-Poe#*rEelW3|d9P|;n4&>CW4dqnwFgf8aa24<trcEw4Z
zgqWT`qsxVh;7F0ebU;latI}-L#(e0V2^IT#5)cvS=LfHXd2zlydThicq(9N^7nAli
zm<STTd-6kkA_y)XT@PAlAJ&0=9)hLi;pdrZ-!x~D<2jJ?o(~mj%TYOaa(aH^xE?74
zvDr>?7#EI!{H7J6SnPlQR-=?NzH!!?Jcnxy`et>O>1>uqf+b>vpN<B_(`7z#km=ec
zUcNOm6%Yw2_V#B+pEY!A=a<I)62bgDBp%s92%ezqjOV;2qFu16MxhEpcg!^CC>Ymf
zU5JIJd>EI_0NNq+#r+ru`$V#@rIK)$51y%qWu*zypP!|}{6t9i>|^!=3(@6TyenVQ
zln1TeO3f(&E7Oqi`DBS6<QIPNVQ`dQE)i|uD%rbJSRJSG7pU@c$S1Rg(72_tLJSg!
zfz83w?LkDfj3r9TF|lr2fr;e_%jb+yS+s2CY<=FEwe$v~E||$-f6mlJi2cFeiMTa`
zj!$j}vtykDvP=>zSlKbYayzVs7U%JTaNm!bH3;u^UM3o!o|csi&q_Ys4;^w?fAvjt
ze`5U1WtYG@m|Z0&@Rie*%C$0b(<D}jX#PV7-lCp!kP^xpS)zt973Mcni2RmQOsW$C
z{taAQyK8E}pR#b2q|p_T(fsFhEp+qI_xtSW-c0aqAq!%Dp~8T}`-}Wb6|ayII<TAj
z4{eMcutm9UJmr80MC>%#@sOLGchW6jQJ%i8XEfy&#)`Dmy?kOrCmple9iS&O4e*xy
zM0VK|@TRA=&+!>p4R%3=&1119KipBu?H7i<F<}vtpu<qL#|UHFx+8Y%db@qS&ClNQ
zqcu>hX@fqS%{cI?2OA$FEbQ{tOkTi_zDgOCae1^b(?N(xB}EvRfzXX{@QL*BF8df>
z6(!h)51=n7$Woenw@n<4UgAx~*#ESp97@4+nAj|LVd8qRsKoe04a};z=|z0kMWr9w
z08v@;uiLTiY?q{*C0_&SvY}}d*X^Br+M(#&EUF6=A}^BsxC6bt#!wy8?u$?hhTi(%
zjPPaonkg20la={)KF7}|5AbcN)7~66>m4OZtyl_L42l7>K-@7L>W06U<@q=$-sE?f
z1%Ze1wE)!b6<rD9SPUVD`y;95DW-0zEOnPI*r9ROItoUR`UgBe2BscH?>bh|ouJ;!
zyI*D#1vM;!{>q=JBB6?r-%L;CKeZ@~(rmXkEJe85Oq+K<9_3N6FxKk8o^<wj4FoO#
zj2wgg6^D8vx4xP<3bP~9=)j2=obN@WoM1q6H<~h@U*CN#a}Gp^c!JTWG=&I)bNFFx
z%TIgu!a}vJcC+0Z2Ke%m@Q&~X@fPtm@ixNQVGp|=q;3fxLNH~z#ZK9+X-opQjKMA>
zBD~hKLosCEjmU)1TnP~-qUB7}yq?dQ-gIw$V(dX?{RD`}jveC3l(6!@$#rw~UpX;H
z@yPFy<&aGr=6uS^zs|No_VDvaMIUB@=kY^>-OYO;oy;*k7y59S75fCwDfNk>rtpkL
zI?$Ap`t1z#AA+#(t#qgHm=Bw*kZkuJxg;S`nL^~N-xyPu0cLFPr=#`XoF4%$J9#iZ
zn={?uF6d}0cr2mA1F*$PBwQYK(3}vn5=&r;@8Klc%NY{EO%1d{U7HIJx~LAKBlhLC
zeuVi+kxP$}cGfT?^P-7UR(v!(Mi%tYQSujCwZ?PThn3w^$URY4$vm;K`!U)tzcLGK
zSCi=Powk9+XCNNK4(n?kmKt~Dlj}Z_sim6Gl4FjD#SQxTIu+k^==r^2341KiymMEx
zxpS4_-CuAnt1Ihl;96i<LZh)7$#oF~eP8a(Y)b@VYZ1ZsZUDPZx1x65*484eH-Bqb
z@KHalZFsvUb_g%s&Px^NRe@Aep4Q6EpSjJ;m*N;%{qDnruAYY?M&T&UR};9MrCv@3
zRuOWH(=RIIpB8)G&;+pIE)6i>IUkU>E|$UOun#^SuX*xhH_7w5F2D>z=pRnB{7@})
z+MI*h?#9=ybpqDa1ZRaJ<%BLa=yW@qH$C7gq8x>us#a`=N;CIew1@N!RgweA2pT+V
zh4?Si`V&$}1T?d#W(&q?(oeN17ZUWRXqboOQ$)OfCztC<*H{`iDCH77wPo7O7b!+e
zePh;h046)0_=3#{h&jaI82n9GAoG%_Ri|8K`40IZz3vFy_(4<G(wr_!X~J-#7se?7
zZ~3u8fhef`XUu3dU-1PSV<e>#Uk;Q(4&e9gdr>ZzeXkl1H8~z&;|L){-CZ<YDB*~6
zLpKZtC7^rmhN~jF_n`$cE*^>nI}_!gkf#Q7Vt)C~=Ms#&<QUu4Vgw1Z5i{Dv23Jy!
zG~;k6a5<s|XSs+>;|zY^G2p6mURr-TAUk8ZAHSjI{46BXv;;POKKs&`KqbNWG#ui7
z@PKFW>~_4v%n|5c%l<9`(bglC*CG-l>AB-a{gUD|d#}`im8JQ|TH#<V*ub#;nkT=+
z3)rXxR1|GON*15UPW6jnR)y--Io_8&*2$-qTw4Unbt0}h4i0_FIu4>)<`R-+{neIT
zE>qIr2Bg#FE1_+6-Kz4<q0NPKtD4g#!K1#_-IbeoWKs<$g;;~^jNu2gEc%rt?XOg}
zk$*;v3u%VsdD|I1rLp_<a#s({|1`V!T-oFC&Et}~#fc6kk+#13)0g_0M`Q8p0+xnl
zk>rzCi1TgysM#KE1$AZKW{9PwIWk`;D{iRxgkgtveK-_i>Q5mSv`NpnYB9WZz&y-I
zDHC;CdDeZ!4`8dG5LcRH-@r^jm;xR`dz-IyuFH_A1f<k!zl@icV6oz_iVB&xUFiIV
zG9IWd#p6RIQOb#2@*c1tfYOm@C-vLnQ_B7(o7~|{6lN2R6Qk@!eY|-km1SRH@v{#J
zCbu%Me(m<Cjk6eaT~s504?6|>^V##RXv}9z6mEZh-2Kp*RD&?x5$)Ir%#7s%P0!5|
zsO=xAEb(C-qMvA(t;+Uk#W&iObr&T_?w;BB@3s}e)ud=K3VrlTxas(1&Re=2bmd|V
zc=0!5aE0KteMN93edMFfXsu0Z%5=Tm>FJoAxbgSOPD?fdE~fLp-Wcn;vkvHOs%(Gv
zQ`$~v5U4U;W+#5q85pnJYu;V$7OU4mMVXZ0SYZdU54JfWAHO_6I+uovp-QwIgVr;-
zf0b|dV~_x1^LN_K(IDro@z0dh{J;A2Q6j;=dFoY96{s~$g9?HNC}&c56J`DQG*x`|
zO`7j}XUrCBzKB2kpyNhcow?jr-foFiiGOb(6-P>=k;2VOosa?}$RK+SVHqd*^3a*A
z2Bhbk?<8O>q#a8|$z((%lo@Ti{n1XPxwq3m#tb7XetNjj#xGzMFrL*&-9NvG@g__~
z6~!P|fUTlC0FEhDFTK0b)V`Grwf7hm+xa8>0KqCP9439C;B+n{x=e<dfdK6?<{o7+
zU;<)@Y;~spMLv`(PaTqw)S0h`qpJ3sKkUT}XLtD#MO}kl0Sf>jWtmMz<I(w&wkc2e
zdsksVj{{oWzj)uc2dgyLhJ>%%l{nv>^tew}Lk{;1Z&G!4p3X>;`-0<!7j^f$Q^&pz
z%SE27@Yw0-tB9J-fAi{rqtp|*dzQ1|Z82U-i9VF;%(m7F_h@O}y+k+u&X`i9XdLG_
zAchy<cFja-8)Rp#v{f#UU&7#Gxym4`BI*+Gqxk4e1T!?CAWs7#Kb=`=g${r9fuvTu
z9liR4f!|C|crDOTj7VhyHL;3nk`QQR@?9e7`m(NKKK80l9DZTgI{#Z;pqZ1^!{vE6
z=~r{u?mq0q`iX`W&*^423$;d|1V>(F0rnI$pSCOd#opWVtTDj3Co)wY;(e0BD+8g<
zM52i8shIT<sJEtjFVbW{<NH-I#JoV4r1_qX@tQ*1zq~`RG1=?4=MIsaZ1;T=b-LH~
zJ|V@8)6(K>Wg`7J0Ssxf?{O$sy67*JJ`Sss%a$6UKC{hqD{6IKDUdn8QvFiduE2u(
zIs@g7+nB_MkWbjX>m;v%*JF+CFMv<ijm|eE1PM2VurF9dU#V2tkV#sN;k%-$yjpw`
z7FZEM13AeyyXZ&L2ZgkhuD>4&r!*Bls)LmiHfCBz>q*fPYS-|Xb~{~Tgicp5&L56c
z5)8A-qv8J|Pd0W5?M}G}3<s?dv+Q>I#$=hVLP55|%2ta0w2_)WkaJTn#H@l^tlIMw
zdt2gVW#9-Nu9usmFAp%i1j@x0xqi`x-9S!Wv|KFL3IZ;Vr&@fVigQs2_j2cy=E{r7
zDRr#C$ss_Pd~eWBNd(b=|9a82{f9*`z=K}Lyk52l`C2_ge5-vTW@)8?!q)cuDw}|N
z4u?+v=6T|_;;IdpEY%ofb*p#iSuiFSqc=Y{%P45sX7{%`R}3v)ZPF8`E*RE&)HQwl
znPNhTw^MG<WT<*XeM3m@esqzlP}vGe$rRz2ml@x;gaI%DKVfqyCof`~iNPtVs!0Zt
zCn<%wtx6{k*K&8(60GU9Kfh;ZeJ<k%iI-9^>1X0ZA1H`Yek(XzXRlhs4uaJo*PSU%
z%%=Cgw58PD$L>||{OPj$Wz?J1J;jsr;4ZgTcg)ZSFL97JAy(9q_18QM=_O#uOa^cd
zY}ZbgkP`cL3I6rG?VT2iFjIR6&4)`UL5|>$93X`E>KCcQWdVMUW9)<BQDd?=<5YVJ
zoB1<nwuA~Dg6cvSesq6TmxvAuyt1WZp5%oVs!{UGrM`G)`Q<6F?f;g)rs2<fP~y*9
z;@Y3F58xUbi(ka|OaK&fWy4eTZjf*+9NN*H$FB=XndCMuwO}C1e*EYss(MVr$n$sA
z+B{N46g}N&22Jc~5kDatdk<r?|E`1c_bK^P%g78b2`j!#RB)yr+05f=ksToBI=wtz
z)iY>$GJ9!USITTuhy8R~UcRR3w93OfPWU%Pl|)4L`T+ZR!9H>y&S<6h6aP-j{B|tD
zv!{Mg*L#N*E<?SBC8mBrreBkn!la+Bg4>k0u6DTOUP0tPA|!u)IR9Y=%?vM}_clRy
zx?_pvg`o10^Jf|IQ(KWI&PGU$_x)QMZ$rzI>@xDS%1Yl+Shr1A*z|?PKF+6a|9C|;
zhwKR35)Ij?Uz9iL5e?<-ncJ{>Rwl70MJ5S<Nrn+uT5AqT_uzdoh&3zORon&Kp*Azv
z(`CbmR%@2gb-z1R^mug`na9_TxYoc|f)xBPRyAgyeRcb48al@GSLD7_STk^@^8O=V
zqM<zSA-3-&)chEY*gO=B={)85=qETVc#~}$qStmY1mnDZLcBeQ%W~$Agc^9*N4S>c
zAgg*)Sq4AwPzHBQF}dpF^8sV~i#`JXt<|y;|JKBf*;NbkGk0>K^o$VyUDVxlja`ht
za*{+$xxAVM*5-|2LrOd)q5cLQqU;bm!oxge$&hUCOJaGQ=J1GIBCKmIc+b~+WVN;K
z!lC`@6)M3EkL|n^-a~Z8LP}j@m0zh@Vp5Zws38u66H$lKGKcYQxcEzQ?c4$<f#p8)
zO3pjJ!hDYm2y?>U<xcf71<j#;RQftCuYlRfk$A84W_4CZ?L~a?`T9F`%1bSU)Gwn&
z*%)36R?mJpG~%*V)Ye=)4-b{1C1js>Dy@v9^f+s|lC4HFGa)GEzV@NzYYFDKZBE?A
z8E>$i<K?Bu;leB5;tCmx^YPD)C?$unh^n&)Hv9LrH8O_~W%_eH8a5woOa`iCqa!V(
z_d8GL!A#qwUL_=bawFadUiOq3s%N|!x&zgbe+=~lsT8YJystTN3toOKvN7LdeQ$pM
znCv_q&^k0G{cFti$EcwO&d4u!)bsyn7{D|K1S71Y_BA#EeJiHbjAI3`HC6VNv*$Np
zlaN_wPDG8?mhUZR9%y1yA9SEzm>ig+<L13RdC?&T%`E!+MAP6UBAcT=Zd|l5r#Gw-
z1lomS*v@EkrP3>AjJ~5>M2u7xkT;n;Xd4+Rlwjfb&H#>xRQv(qf1!OD|FmPMk@MS*
zaMYpl6HD}=EQy@CJ|aII%h`zc+7sox=u>j}O5C9#A>sZowe!Tf=d@LcD~&_K_a{+2
zld~ayf84R3%%mOjDs#bmVY~3n)rq)6%hWdD#?D>2+?)#Y`(<qBz10=)(pt$#WMejX
z&Fe7#9}h4C7^~=BzRpL>p_4ayD$SDk{0S#vt?6fCJZ-d%LjsK}k9^?~k(G{3?JV!+
zZxRxbi$;|%9cCrl@?Wg;v)AaU%RqS4)CouQJh|-yDw2D5&(1o7uv?@%CR;A@@<8}y
zQhL?QG?!{2Kq?|4(Ol6u!ggXZPT3Q`Mg$H`k9jH9ZhXhKb~YiI&}Q_fa=m%b-HM4F
zSq$DP-cPI935Db<$_c=%cRf4%TEit}zX(<)z{tvfyEpg(E^ig7+VHh!Tb>mCg67FD
zVJH}8#|+Nv3vUMAt7KsBiG0g~DS2G2Be6Wn#A;qd7M0(ptj@A^p}Z_jwGcw74I6to
zX_yJ%GaKT1$S6$(B<y>}sL$tTrO)TV($0T!K7&)8IJX-v&{76hpFbL?18<NZ&%ayj
zANPOAWbu8cUD)lhSlib!d1{#<Y(>2q3`crtz!Um|VsFe#>Qe%S>u)i$i(%8%Y~84D
zh5ksJ6?uHp5fKT|>?tN;kAs;z9&4CNnf-m{wAXPK4kwV8hU2obyQ7T3&bZXjm^1%K
zl7tC%DDR<6Nfk~B7Jx~8n0u?fO2V_+D4un+RuY8l&C|jkV8NtAPrLRA(DoarfpC<b
zLe1mDv+EoqE{aDoYRe^cBJW5fThk`^`TVwC_MtPRcnKkP=v{EuORMBsP6c?B7Tr1h
znR`RG&AA<!P*}Nxlv7gXiC1;5d{ELN=0K!sxbx}|N1;_Evq(1fYP#c4ri}Q@IYd-3
z<kKB;Y@FacVskX0x*wiYOmPskV>ErSUN_VjxUx9130Yo@=088!H8mfOKsop4C8Ax!
zXBV(Pm*B5*z~u+bWEIh`Bei&GoJP_CEye}D$qB$z!2H8mqRH#A3GftLqA{I5Bwj2;
z)MuKRs&usH=#*wB8_S0FeHbXLb6%lYsA_f&GW!dRcv{Mbcby?6RaH9A+0}iBrRw>n
zw%1X0n*7$eBs(pShZJ3<k&!8tykVtu;Dh;z1R9i)NgJXlPsKSoar9V&;^vf;9lrzw
z_bAMq?SU*I!JpevA&h1tS=cphy*Tm-CS}Q=DaQ486xv~%6Vi35_S30%QWy;yGw&{J
zMrRSQbLnTWE*D(txok1-v$wa4hxXPo<ox{Z=!vcuDuRd_w6)LD9y02Ul7%k1AvI=K
zu#5+jf(^Mu%MOQ;eE%Mv|3Baw;9x-ip8!8e!NvfLn#1)#^Ivcs=>>Mct`Bf8=oBQX
z95*C9fh@M>C(F&}N41j*;^F}>xIj_FX#Zk@Q$fQw^4Kj70Z@2qZtjnWNJvkez_~{r
zN2d}obM3%N&(Lkn*IiykB?Z{m9gdeJp$D*)EnqPCwsElmpcL!5E!pv=vzvzcD{r59
zwR0HvKsN(mPvCtZxn8~V1`xU<k9QYO`X7Y>M#%Y$s^~C4sNXg$r0ezs^f5if-g6g%
zOMj(+&1Mn)&2UPy@eFPtCIleR6@kP9j<0rGb6^K3Av2RP^|M?0=Ue1oS`{@pVXlXB
z3y_f*D67yqNdo6QlDw=P2eR3Z?#oVL6`iZTH=p$<1nz=-C7AQ4X{}CvB&sC5;<}rh
zwjOOHdtG4#{nj_Ze;t=Ja76(jA<q+BA+UX3V>LtLT?r7(;**lFURcff?&xRF$}au^
zTDL^W-E8<7oGnZK;80!=fM#^~m1*QjpnnL^VVA7u-%PkdD>hDGvP26^p1eZy?*2pz
zuC)XHhv#~8r7S)*07j{0dUzyd9)|6#`4w=#bX@Mw3?fEyP6}!GJ)X(l0frS@3cvLL
zI9ut^?)vsux;Nn0O|zT&1UT>8=K)T&?(ep9g*#wvxBU$YH4eZfk9WHM4GcI1&eWoN
zgJmxJcEfFge-|_f%-$(Sbu%j0E@Vlv!2Ja=|H7iiN-6>{*|imu&j3kw7{GXw%gdX8
z6m(q*LKDIJ%D<ClN+m43@=6r-=CqhdqEXHmdJT#I6zONfK*{WV5tdzBz}5Mxk=dT3
z>>&btp0uy~H5J}j+dwZ#?L`$-0wn$j`lK*6eO;7c-<DpXmVkFUd7;+g!M{xI=453X
z!22=?i^_ZjEZDYC2v-k)SaxfzCaTMKS#kX$;)VnZX4P|-cxm3nM&h2D7*28R73%i&
zWO;!nhHM}(K0TctaPLO8pI|d;-Ok^?QZy^p=y)Bmpb<3(<*NXe@lvM(Ho*D+0(?HU
z9nM$YWTdWV^E%|r<Do7+nIP&r0NHKZBtwzUcEDiGTi=l+dIJqL4~M1=*h={*Y`?XP
zNDS>&0I=y@qMV5XK;m#T$i0Dvw)2KOGBYqR+&lmj&GsNTv};s6wk|>UlV#`5kNq{)
ze;F#vz-`5EknSI@>s~QK;0gdGzGyBxYLTofN4YUAe0bMhTwDZr%Lj{Hr`u}nFZZUd
zGvH7Nz@O_Z)tZl?a8DQ={Xa9XESZY9jL1*N)y8;rZJmKSCj?C8JOM+YtwW>zO4}<)
zoOvyKZF8J7BqU@NaN^F8ydrEHuktWd#2ZoSrAAu2&qHhZT(NHM0oWmMi_<^kS4o7u
zJPZL>Go%}i)V6ZUca3RwKlPKd{E!RKGpAPP5ooiFa`S6NmR(_3UOV`%dSWVIH0R$B
zqE35itjC%D)6fBHK~G0_DsuTpa*oFb0PHe?>2J<~r|YQ?+yJ@%dAtA1^D3a(9eZ{y
zL5gt)j_?L@vzS4Eem`<tqPPGc<19J32=*zLYb3qEmgI%o(^x$X{ZTlfVwTCRdj*lU
zLnD9z@6pDWoQbvLwgBaOL4RQ9D_%S#CCsw%l>u&tu>U7Nh|7K%6gmiqCP*7XICbEo
zp$zKLL~hA8&!BWWe46cGI82n4LK)_rkx&S@;(#noIsnPsyjO)dYRamYC7Vs@_YFWg
zs6QlzpXV9~Mp33d>R%YJ3hDmnz+iA_&L$HWnnw3A<~@fGu-yoH?CY^GeY|U=Lwacf
za!A{GtpN5K+l=CnX8^`rOM<G=y?8k)ap-H*L7|+9$Oq<sx6uN)8p#2!1h6~3q;24_
z0b>PNA717o@<lvQhbY0=db}$ed=c*eHh%RLZVu<j*Xe2u)Mvx3Fs*B~LYW1v8vxxt
z8-p${vY_XP7ECj3?SM@1vdGcY@an66=x3BA$v23EX<O%Ab`0_X)ZqUxEatU{<4?rp
ziRfAfV@X*{a=fg|K1(h-*^&zN`Jmxg?*;Rs0qLDSSr%!mqTzG&g_3=-ut*IhBrsj|
z$^cZ?eR;c3yE;-|U!6QfZtJgT`3r`5A|%@vpp_#roSgx0>?_ZtCx!Y_?RcTK<f?<;
zF{;{haFNR&7_jsR(`tqToS|7sIti;S=hZAsEL>tgiC!0Bc+auG3he~DqX8g(Klk-d
z!Lk|_f+P+TvY(GZr9wzK4>4>t$EfsRyyJ+G^}WK_25bqsp)`Q5+s6IlTQEQ?Zv<6I
zNulSfhIhaA_0YIe@A9R1%l#R#kVn8Aq6c;M^Ve@g{aX$AWndO+5L>?ub~(LCx5%M)
ze>93agQqS+hsRNe!pi^%|A~O-Y2I(pNbFV_P~s7EE~BN#B4Ro1no;#|uv>ax9hggM
zk^;Kq%y^8@V8;f_sh&_L%ovG1#Q9MH=`seH7$SOrA0ZSE%Og6KCSjOn8Y|o&(Zois
zrV0DwI9B9-sMwTY?3MG_V7p+#2&$bB7~N6pnIp0^B7foAH3A?hNM5LU)8xwsZ5I=@
z$w=GpP-r&t0x4qo^7VbRvBChN7D$F;`WmoGGJM4-Wf%Y`)F>ZD{E?ZS)k*obALrf_
zVpCQE%tS!b)dU+Qa1H}Ya)+?RPQo4vFNF-4k~`T6JEqVNNRs+CfmyabKO9I8#9VKK
zP6arfBa||El&7bsZG+(Mz5oW<=XRf0d02y*5SxDu8V{E1_$!zAV*cA(C|d!kG_^2{
zjIp~?|0?b5i)K#%glbDU0Z>6K4%s9qs!2^z=6|gAeHZg}2hhRL$L<Hk_A4z~aTtNe
zT+S4;auBXJ7!uLMa;sm@aiW=RRad2{rVUSk>8RF~xo}clRy~YjV9zk3wg8|QJHH|>
zW&W6Gd+<u62(5Mc``h$Z>eY{kC}t4`r<)d@3snmvIIXVsF9W?KylfEOW$;iIswV`e
z4ajscj*2Pq3u^NS;QThEXGakox?PHPfy@Mjbn#*V2XZR5NPd@Tn1|$>8q#D8fBHA*
z8WU$h-A=>bD)bw#E59QfAaZPJs0y2-D4f9MD+qB*jALZuReLNqX+PH-1Nj*3bGbLu
z5mDmh7~el(4O!|9({FUO?$lMmu$1q*!$k=(Kt~C}GE#`f!(tfY6@ojvWKapL_bLb6
zepu;ASXGpC2XIAI6&6sVk<Ecf9YnAzTu^cPmH`L9!3L40NERuCV8FdlCOS~&4YY*R
z8_(c22^+>WY#7U^-t~eLGG8uR(4QAYM>+5Ab2P=g65PV5P?0FwD7tBe0ZiACxjojH
zZopsVzu-3uJbQl_C_Jd-!(PV$ypzTHU+p3X!pNOEU0pPyn*me~+SvaL4^^pETUAz7
z{zR>?`jK^mh9dxbxxsEJ#pVMU>(rj?iezCB;kf~j56Vsx_MKC%?{)QqU{LM{$m80}
zQ?4-S)%(?&^78WO3;wv9valu^oZs<2;=b6-;gW5RpU_NVd@v9ussWb9A}nc1K-hgW
z$2(OnH{w84<FGEMhHX$1qgc*%bG&M)WV1Gb7NC5x)*mqDecj61j%^hs8Qk;%6?Cn8
zg7SrEb8ru>;QL3vH<>;hrxU9Wsr{Lr#@mZV;?6)z$GG7KcBkxtZ=+Ml22y^WdF%*u
zDo;9Z%JC4mX_Z)R&tqO9Ag{m`mK6ZF*J<)0A!LqCI7xQ%1Z#x4WWq_%Nb;#WHXC1(
z>uF>mVnGNhim-T8c4%rZ9-ADbzb6|((eNQ1-mL@-yhHSnP+v~Q7+_)k`9l&`-jU2-
z-BTdW{hQE!ogC)*bce6EzGuRXr8(_#5RN>99W5lD79TNt?x{Z#cggo{VWJF<9zT1A
z6$q4JY&~)fN3Q$BiU6L?gYB`Wo-d5>sPFU58_)1L&nViG_~=k*iLwIxODY`8p&ukJ
z^Yv2SKd2`7H?z9mvhNlbW#jl-09pKWqj(Oo9fNY9p-j2b;8EQ^Gl5`1@b)I{aMaae
zY3uw2B4mbq+rs%NZ5GfEz<gmxu%4|!%eRsIRk@pTaNX%gq@~y`x|W>p_I1S6smgs}
zjuR3A0Rnm;Dwh(M&D_||mZg~4hA}h&>&jpRH0s+Up~3yGdJR%RSBB_oDJNqWb@y|k
zg)j>kfP3CtkDU$aa*^0G)(~)`o=o=M(dnwTbSxt3!Vf|j8ERSPg^YU2GI<<W{`f8p
zqM;D{5eJ3YLdiq)PWGbv);SN8y!!*4AZ)HA03uS3B0>wm8F#al3>}o~)A=g-rGT3f
zk4gcPyC>q$2L^VFZkKP2nJM2tFh{}bm{biTS4$uv;J<UATt)h{U`AB3zPaE;TwWrU
z|E5=2#j?1d59%%i-J*y39qwE!<J)N=PkJ5mJ^^<<7=~yg6oM0l)U}l-Id*Vqm1XzW
zU16M_7^qR~DC6@tt2PoF@YThJ(DMa!XWw{Zv^?&s|9C~~kWEu!d6Wiilj0EU6P3P4
zr}=gU7K0*|d2cej;?Pp#sASP6+w-&30iPXhD~64o0CC09{&#?l%enAf;t*<-)o<Yx
zujuohmAy@L>sf_^#;z0i>eZkGoj;CY!qRwVMxLs27r=+5ou_KHxQJvE|3<RUfUX!=
zuI_`Vv7m`)GkLZ~m=lXTF^)}P8zF&zI4osCM2Azgj4v6>85c#B<JUjo-?b;5;*6G@
z$7fSPF<f;<^DUI>LzT<#1Jgq3&(T&@Z+rzdR^U009BIoFU}*>xtC~GkJyt#7PTZxq
z-nrU+Ows3#e|^!KOir8icLQ9Jjn*}tHIx3rm@v$JK<DjyPT-dV1t4vhhYuLl+00M0
zK;&=p^VVPt#3hFKa^Cw#;Rdp)AZzpthC8_R=Arf33U0hL)7fmbTB-QGkBnumFLr2a
z)M474_m!|)lL!1>PrGa#KD|3p<>il~uP!#QI_XcU$8*GE;6jbddG|2chiH{*i5esM
zf)k~ILqNqZ5*_?;U)ilEv`hp3R{`tjdUw$A6M5h}Y5Y)%d9hpURj%Fptl#kP%{s8c
z-Gl+%;U8S*Q$0x8=~e<$VV20Mrnu+4KVYKu!I8Ijk9Z4-2X{dQqAX#mZ3cgAAtbiO
z{Gr%`j<7~0N}d!bj`clpGArca9V}CKuMCJ{0_L0+h0qo%T#cf}!wr=5wbx0I4qb<j
z6ol$Pc}>GZvx2j1B#XHb$|j@fB9aAl;)P>LhH@RFW9hMyvp^m~gqSNO21<P2Nr-?+
zi%oz4?~p;LcP(dBC-O%Eeca9L9VS}^FFXft-FdKcv_=4sVkNCC*d)Z8wifteaVHO<
z+7PJoQJ0_E1eP}tkR!+PebbW_zBm)?7pktNydqc#R6gIOF$MAtfQfIY;E7QGY!nrr
zx|~moT3$H)eKwa4(*tDbL>t=&mCxp(Hl$(rEb6`c7Ek&t<czib53s&=hlc*)9$!4I
z3Eg+!sBH^h5KfGajnyAWCJnRy(F@Z4-U~@yuJYxM#(Q|%nY)=R3{oO#e{+3(?RIlK
z#tRwG?0ilL&52_k6NF1c;1<k=LdG@C9)iUXR@w<M{Qx3QO_Wy!Er`FBj-?ztsH*LQ
z_QySZPXZO&e}3yRCggU(t`K9CE#vSu#7L{1AO-Y*61XdlF~AeWNkW`hYGDE2j}!tY
zEJb6yg<Nh3ax)|RLp1lkycZO(MW>d+dD;PflDB{f(a<{ae`aZ*wvs%0G=7-#xsA3W
zA{KUv0vC{((jpiyB~Jbd4u*cxZHTn$M22uh=W~WV7<*@2vGg){<}9R8Kav>G3Va!B
zPky0(PLzmACasm4R_yD}pJNd9%Eu!y4&+60AX&9Na$*;U+G--YR(Fh}m1O<25Chwf
zI19b9QqE+z630an%2b0{qr|Bgl-Fbeie|=>9TTs9U4c)8OHJnSM_Cl}Mhlabb6I6^
zNyX5bI7HBoMTN?VTLkXLnWlhB*>J;snyTR?R7?-@(goe^VKa+M0-{B))4uR3GMJw8
zuO>z-T&(&pGJEsj`<b&QftDkC&YZ*0NO@K<g3FB$h>Uu}jUl@GL`Id|hwm5R2WO{x
z%?v=M6W*Cu8J@n|_8^QA_HX4SAl)Iaw5s@lmc+p>(`q3R5>CS8Iz@{<Cc@x4(Z&32
zzV0KFLtgkCIKlh{oSJft+YAyt8JYA&YoCTSU{!ygKGA?J9m!~%9Go4EWnEr5HrUP)
z7?BNW)84NiLA9mLNXTS+X!a~Eq(*kByR=W=70h!q^^UdEa&y$1t9%qj4go)4nHI;X
zxiwAWi)U`{DoG@`*4Izqg=RYIi>xW}(<u$wDTJFW?9liWv$uSU`7W6L@*|G&EA@r=
zgeRYe>2%0g<aoq4UVfA~iI}A{lFPBA{#B%6P{|uJRWi}RQ2c%jMl9`yt??lV2zKMv
z4JTC){^?Qoz)yN*roo?1FHx>v<ls=sfEd+4N_u`bTPum^=#hwz^47Y@Yf`AM$BQ3t
zV9z`j9wb@eqLYm&vh*XDoP=P#o%b?$QNk^@`!D*K#FauuFNS*Ju)NMaMCd{nOot~=
z#ic19DzEe4PX+-tV=#PHW%Lax<m2kr5u0(5O-3ipuTzl{;#%^dD)dE|&M*|KFJ0hI
zk}-V+!;w7-kv!(3z2br!Z=6KJbnXxXxs`x7DQp+S;&z@c>AhaNqQxFfZmi1N3#Ht?
zvvVq_Q|Y8M_L}aCGozQ7O(<F-{0j3>g2!?}&UB^Ql#Tm-Y=iv*gl}M_5FvKk3Szgq
zuOK=cFq0$Fmc_eQ_5RA6(iwnav~e9Y=@i<Vu>^%AiMpUB%Rb?TncH113$owfHh+?6
z-4xIu3}+il;OU|N0#bvNzR%2WDbM7aNXXtJy6<VgU&?-6uGzn@i5|Je5mv8rX(+5g
z6x><Z=tVbw=2^{5>0Hsw!XF&>J>})N%piW+*$renL+B?wPLP=Rdi1!0HB7VgL*rX#
zqALS2Ny@2<Z7t_7!9(SfQKAOV^iLrgT#g6tG`v%G6E-vEH)CYzO@+Oi1o%n#CH>tZ
zIp(lj-xt$4B^NNjf4<dQL8dQJ&7G(do=mo`DxzaAYTRBU^}8jfW?<MFdz`Q@k<u6e
zAVui~$b3Z~@+IggWn+}oWGX~!?N<caQC<LMxTrMu&a}wj6J3zfQs+-Tbs**Xanfii
zYl)p{W$6gJ^vV(a!NC05xGx=fN&!QQ(!;uY8C>Qws{P<a?^=z+nuxIX3y_Co=K2-6
z2-G00fMBep6z;>w_3_eG`SWt9|L%BZs(PdS=ci}DbiwH}pRGA&u@@U2{%5<>+CrBe
zh=;ipKdiPn3a{<D6}z@|0_(kG4uI%qr^*D#lZMO$Uro|1JJ)i8oQ;ASY9@>`4c^yB
z@(-x1gKfJX`+wVYJ-U2u`6}P+b!ELMpt0ff>KMO&&<rVrpp2Rp5TApZUw~v50@5GB
z>DQFo6Lp-coNewNXz6ncEuzZi0BYo3ceO;7)EAO=6qH-7-6Sf~3&bkY3kcM-ZpJa+
zzfU{|qMQ2yyM5o^Yc;ZC%Qo0d^62RJpRevhbb&}v1g6z}t0(eYgT+Sm1#hX0BjOTw
z9koqo>Q$<>rG5n?F>$J=(>CwvqGNX9g|lF);P8Z!>$m!cp#DdKGu?I|#GC|#-wrL<
zbMF+o%q{2BQeTrKt%WT$*o{`LW+;>LJ4-DwcDfvas=czy?7rNKtcp6coym*<>Aa6l
zMpwNwoGx3(BP({JJU1Yu0KrQ{#b)W^3CJnHTLc7x=v&<(6Q?<zOY@pxEIL(Wueol#
zvN5ai8+IWMz%@pIfKgfjv{mShPLpVZq4Ke2gN5$kG%9mrdv;`V_8I7oY0I@%yR8XB
zl6@9o-ZX7?1ev}$L?6xDlCO-?I{;Km>p^p5TnHD|E?*p{X<lk#5x6eXHvvt)iy`<v
zIBRfuf-gzoyZSFR_?<@!Nk#{_w5ND`?~7(DJQ)dc3uc%KGWfuMe%@h_->~*ndjd~n
zX3K_eBJ}JJ5DZHovEc;LyxLlT-b0I{zy54YNChu<!q-Np(>QN~2Z+ii5ES^?DH3V;
zoJ$b@tEj-!wSSHYA>b5#1}q~2(oQ-$X(}$j{Vvtvef_U;fZvKz%>TN0Aejdq1^8cA
z5Jwk6#&G^uQDpEWq450w^At46;H-bwI53P{Lgk-20srpqMJK2IcWLX!;c@@2ulOmO
z)DXhIOZ$J|G92*ytgNhC$z<>+T^Y*Xvk?Nl|9RB1M@s0<B-(ZgLjTcrGBo70?Tmrz
zBu5Jgpmee(xN1)EA<uvM$8ZF55duHEl=u+-n4RM5$*}+ZPpAF{j_7z(c_k&TIrzXh
zY)eTR?;0uKpyeQ-7_bn~&EhUVwVd8&r$f~q-rie)k0CrZmUk5cx*OY4u#S8&p-3&_
zwQlII-SEHa=IyovUs+mOiUk9tLIAcrf<Yji;IJ@CCMM+FiEKqqC3{NvKvyN1T?ix=
zAjMMpKR*tovXZ`og4)>HDvVAH&qk1#`;U9II2ywjPo)9tSSE3GrKRCX$;uvfezLHS
zGD!=Ji)oHO<3?enql546?!IS5nO(-~pMdUm{f~D8Taa^Y4#!F&=9JZ28?R)zXFdg%
z4wfE%Uxxrcbs=%{@bDLsp}sw(8x8z#a^M$#>_7MZ8<f7?F!x6yhE*waptE=t>%ujV
z3^cfWsx#)zqU;Y`h1~nKsuQm#^2c(z?SylY%Z{OZi28tD6EjSwTsaQh1oREre;0Xu
zG)977O#1K6ERJsPUmmKJ(aH0|2p?$2<gH$Q_4~ER>;Gs?;O;Tf6SC7H%-d=aM$N@<
zSxv1hX(+iS`OKQ5hkxq;H`>jf!eds$2s~Ct(*NZz8@Dg_HnCjf77SccUzCgyl}<F#
zq382Z*kL$p-O~wa{?{5d9F3XTT+7w+C5m(SGSL>kH^mAr7hlRpnMzS|Gne?^dxoq>
zrvEpEI6>pd2r_uxFdixW0%eTrhAQ;e+MxZ@fBtDCQ!9%J#1>={eu6I&5`on+=)p%~
zz@h1c1+V=T_rJSj^%sknp>?=jIAw(oz(L~rHD97$M>~=Kc%muU3zR{!z@TtrYOnu#
z4bJ~~5R47UR%So(ih;MlyMYR5{1fqiUYUO`QDdT*U?TVL2hiO+#3l1ujPZE!Ydg08
zM=`&6s7d~B8jwySqN0@4$mr=2&d$!lBO)Z4owuVJVZ?_REGDwVe}8IThd|IAAhRNg
zV6fS1<5RvwWK&bWTNU{Ud&Qvz-;+Iltdk2Qi<8x9^S~%5ER5#<-MxlML=+_vf|xJ$
z9g|MKfysKd^gF}HpLt>d?}oAkc^@Aiqbjm#si>fD*4EH$^_O}Tg+*U;;4XVRsG2HI
zpuc|~#AA2j<RNqtylX$4P;^g}DHq-p9CrsHd+ehr4Pu`YknI*&`B1zhcMHAEJI9CS
z{{rkxMgz<md>f+PhXVaZdylKX^HJQ5ur7iZXJ=D4@R7oOl>ZFz6X?W2ecU$aH!E0u
zlbIn&NV~Hm`ty94(B#RgqEA>rrr+V3a7wIvV`F1Jr%h<!J1Z8S^UFFAw(S9Yq^FBE
zl_o$gaR9=NL_tXztqQ-;&FbKUWhL;YBfv9?d*Z#D2X+r?1zSp1ST+h0?sGJ-|3+Yw
z6?F4=B$=_ks$~V^zh;^kIeGETf#`5Qh;X0EJtli@wDdBoMjS=7ka*KJlYYaj$F?8c
zvKwSIdVj{hFO9vB-R(#xwT(Z}?p)J4gaXA|@RCC}AqzcH(WX;+`ATDeV3*JLz7EqF
z$#piL@z3G%@6X;?%zCprE)NvIL?@Kg*Ch3c-w(#(Cd>6_){-}BzJ+8wdZy7)x5%*k
zaMRbLgulM&sf4=@dv?d&aj5o*S(feKuR~$&5Vx;KG3&dY%$tQ<7`XUhpv>x)Q|@`{
z0a^;`h5oe?9#sXU7V41QE}~}=o26~3;gjahA}T5}t6h7l+|E2-M|vkGx#emEdJN+-
z2vVTq=uiypgsu+j=VoUZU~{|=z`*8*5DN=Q%$=Q|NuOP_l9kFyn?_&sg&FqUg%rjm
z;8~PctD2ZlJl|QM#1QeDlso097bz6#H~xl4MC6!T?ij-37at;bc2;^T-P2QzRPEJc
zO*KG6U_<;qPV~*kC2i@T2ZB74+6X$me7ZK@@|)e$<|_YfKPG}!cnh9vap$%x#4*+(
zG~w0%!O>ZTwbd?BxQ1JChXTdj9f~^?ch}<XkQR4Zyl8>q?heJ>-6gms0Rq8K{&SJ*
zJUjXJ-ZN{~dgtPYYK<q?lGWi;_w@=l04oWn@-F6pITuR>kx$~Q$(<OFU$>fzXuK3N
zrZo-=UC0$e{sp(wF7U{*(hVIjTtt)ZdOu=qB;u)#c3OLF9)%JsA!!tH>2!m2CFj~}
zhR39fJXk>>H(M^F-YM(W<#?8z6@?n#ccqqIL1|VwB5{Q`9m=QH@*}Y(QB3ZU;IhAL
zvKs1dgTnsDDA~Pqy|7y82#!_Y^Oi$tfvJhf-P};7P?a5g$AhJ@{xHHV?yvB?UvT=c
zi+SKpO0!(9tz?}v#e2Ld^X1E%hue!-ctDrRWvg>&Z36y-omuWrM2j>LmmGG+gq41^
zjK|Otl{m`r-YMHVqm0rZH<5jLc-}d<$ZC(rfNw!dfV&_Nj;1(2Z29>Duqd8B{x>^^
zJ*>TA`Xr}vy2Gn4s>qSi7|<!CB4ZAKI(6ZG@z%Oe_#Y|lY1)^~fU|;rP4b@=W^BNm
z2_xYk3?(tcka$hLH9U{*x7`^;qO*Eq6Sw;7kZVhE8Zo9Vy<}|EthB_IflyPC<FEUz
zeMSB0H+Q)ko*DsdmFl&D5}h3&V;GWQjq5}{|2255e}&5ujqSYS`^?X;tmWhw$yZmm
zlCE!QPnifL4?36&2Y(VUXqTiwoNcOeJ7ezbyCVPkF84{)OOCeFLLQ2jcL|S6L}MFF
zbK{$mmq|c}pUR!>@AZ^6URR$DZfx-3<>b%nsGmyV3LF%^@MU_?1<j$%iKw3*Ec;2o
z0#~<1!q)mAK8*~KF_Q9xovLE5xR<Ox?Gf#IL`AP4%C|BVXOoo@D%>^`L^<fgi`$a{
zetlulLT{(4=$Iv^f>`O`FKpsK`MTL0?Id85TkyIcORx{8N!1M#-^7X;_fw7QeIL=y
z;HR74D@Y0^OW`u!2uM=r&66t;u}JJB4)s_7EScrsPPSyIthTLw2Xw_E0tnkT_lUwa
zm;d15RBWI5x+ofvw9+$m<W+(YZPV($){Y_bt+VTol$ORT|5e>Bl;|h$0h_*FgH2PN
z><H@C+qSQzZh#L2&Da)Y3(*8_EMknOys-vH?*~kTW5A);DQ_B$0o8^rn9MpXKKcXd
zBBIYX+zck2t#yaDa_}14_tU-NzSQ(xK*4LuODib(apq<5)=r?5ipi@q3<s+8$+RpG
zd<E4Y>F<?N?#jW-4I`GaHM<H;c5?)BZjRfdh$@m(oP95O2}HeTKG0>3<KK{XlxERU
zGl&l(IvN5CQePQ-UQ#i(<<6ehFL05zyhv&Ct+@?YdA$qHqRLb)oD!6QT_vUi=lSyX
z{>{D5<<(J!Hy`5YqDH{sL^)~cu>UUehYVA`on?f)LD*83h+Foq42poz-?_BZ((5Zl
zX;}9*wuf6f<VV~_tJ`}xR(^KNLif+n862l3k;Ugk=HKf~|6gl>)K_PP5x^^*wd*tk
z9jtBzY&;V|=8>OVwqC)o%3;XJjoi#@#fzO-r68&fV-y|~+<+u%|9E)O-zZnSTf+(1
zJ%nZr2@{tsH&Gx0c!~VR*C;4|YBwS|Et=<i86<biXnvcHX~LCOFYBq-ND&Jgb^zEV
zW_F;gVuui`P5z4j@e?`x*ZcTLOe;R#l5?fP{ZR#Qj5!=~9FfaCg~xV^esg8i6HE?x
zf<)FGPW|vW))zLdPFt!1xt(ScIKtkbF4*O<WizZXtSp$4E0%fZr#H#5Z8E1P;Uu1&
z4N-G)S>mZlO1>jt$*Ac!G%nJsv+xD?z6>QDeX{)jeSgA<fEf^%;%&JXzcQjnW(>d^
z?>36ibWrOR9(?IvT%RV@?$8;O!ljhV=L(N&I{E6Fvl&1D;t!zQf|Nb()hVlHSDG63
z&k_mZZm6aIGIj+8+zGt|c_VjxjNtSdwsXm(ag6^31i@GP{5G(%UamMori(ift!`0m
zm$Zt@zW)F7-%rFV&gk*>0&>|=7+0jF>|_$0YtjT)Lo6R}oigC28YPb*UWX+zxR)uo
zcz`}vMAmsA3R`G4QWzN>oyTOA{$)ro8wN!gFcGf`H?-%J&R%usny~q?TV%<ocPD{o
z_af98sL^wFJX=V_dup6>Jb*6%3Frxa1`pt>y;S^Wa*5@1?^L%V%g@h`(&oonZMR1A
z-wa?cPpSD{lIJT)DmEWNA>jHZf4Rm1;bH=VZbLtVqPUZA0&RKpc?U7&qQNb<#Fc{Q
zbP*Z8ycvVNGVm=gs3Lkp#)Fj9nof|4IP!>cNcRp9?(sNn-A;hsz+%|LVqyTnamR1U
z`Ao|U?pRVbDlFj{VN!OOj!T@15-{W_8x6T5FuQ1RAdeBS379Zt5?5Sej2^qwlD(j4
zQm=4e)v{B6J7|w5bxGYuPUmVaa5CqnROi!c!usj?-P6}*b^R14i=GvwnZ8KC3)g*B
z@Kk3?rM8Bt=DaOx8l#<;@O9s|eT_TH6lL5TRLStUvMj~H2oqtPK>5-6zY{4ns~}-+
zvhvlIE8)Lt5dK-?9!7j>{3ia#YBXtxo*Zso`$i`0y`b05ZI&iGex16w$MSNol<#|n
z4aPv``H&SJrKv4_TJK*qJYKf^RH1ltVjU&gq6(rH6jY$+7j)bEYvPUI54_YC8pJIo
ztsS_a5S+?=fet9FG&_;Pc@h+GLr=u}$6H{R&-P$*mEkO4=S>`iFabY}bAj#MrDL1<
zG1$BnKFHr6O&(r}by|6kM?sNb<<*(?0RibOc1OO;KoasV=t_?TMQ2QanJ}$O1m!f)
zU=Kw3=a#rKK;F08Bk2=Kvd}HK*3~PhA(to=OtHIaj`cQx&F=5h8R`ZS5~`exEVag_
z?Q*rrVE8qmr-+B6W84<tC)a*RIBJ1anT9{Ltcj+W>wVBv>xmz^AG=}b@OUn!?X8q=
zXB}r<l=|>cK>yR_4#d^;7Pc(v&i4n-`8j>lJhf6j95^Vq(v5YUHXe&47W4)K<dC5d
z_jRZ8*_`#TN072Ezs?yl^4pH96(u52{KHoFlrgi1q<6$l$B*sZ)4GZwl1DxNeTKi@
z&vj_rxf%H-Z)*_{<f9E}XKGEQw7wZ53|0`zUG^z95-(>SMdtQi2TTKZVnSCq9gwJN
z)-Ep&pq0KpGU$1nWcNykEq;_)*AmHCuI3;FOGX;y6SZlZNJ8P#98%`0e<0qTX0n>$
zfNcuh1moV72umk_bVeH_0JmPOUVvV(kIu)$>`zwvhm-PQTgWnP_15vctHYY2?r>J?
zz)Bq@ky65Hr(eVE@eJ`UvQ6!O1Z-_#-6rcvuvH>igBvFCRcW2)nS5M(0@}FP7ufE>
z>A>f*0OjXd;4W<WqnlC5%jq4F_c!EDBWuILf18%BS5GRU+j$k1gnr}<`;q9x1*WfA
zR)aK01XQ-|f-r0GR{`?EEk8N_OiHZ+r-kELe;cT}OHX{0!%Ht3dv{bx?lYzL<;oAF
zhueATll(}^p=<UoWN}mR&rH3&vh`K(Z3|x;)Wmu*m}QspHR(N1))tF$-?*RdGX~z@
zA1TgQtJcq4cbgn96gAZClLfY_<no|IVv%z#{nadS7QMMxRvQLit~U(pWpOuxX5L<C
z-NOnYt?q&Wmf?HAl{ab<Y_#Pu=?p(k7ajiCH<dFKGNGcf27tHP{SV47yVe@raGRMW
z)OuYmNHWc2)N8*|EmUhFsesziFi3f&2V+vdw#u7l-68haKDD}?c2pBNaA_1+DToM|
zy@6W{fioP)7v}f%)_QeuzY~*wRbF}oRa5{3@+Ro>0BP;{81D6Zu?bN_yG@Z^3}hvk
z3<k}mw|`wp-q=Q?B=+VO1#n!q{f_D)bEuqDzD#PHeT{vwTWC=)z9t|fkK8tS#4x0D
zajW=Q_&vaSlbK<Ap$U;|dbj|Kz<gD1JR38z5zjpv>#}55d>4Gx@69Y0liE!Qm6;ur
zAxBkJMGrQAMR*dpkuwtWT-D1Wj>$}@@s_T+3m|ql=PvFg>X58H%E<LTx*KOoPc#|I
z@V9hCZ4<g%>Xp)@l+2}05I(Aph3kPMBexgAwYDHJ5uyEEo~#kJSA44BnX&)czgPm+
z#>XdmM>}yi{NLXw_1=$wDxJ4LvUzu7yM~JDs$MAX^vMp(s69&Q*#TKF*O~UPQyT(7
z`|@RS7;>2M{NI}H(<!4r3HG+Z)jVIeI`89nUnyQ=eil+)Y`2*5IzDUncavCHT0+48
zBvvY<6_blqIQ_<2<dx+Igk8F#t(DUDfYV}9dEG&f1HwGZJwnq0*~v^_+XV(5=d}-{
z%P#+-fy1+P9`q?p#%r*MIHOKi3ABG6w%qj6zLJ~Y)tG-B%j@Y$stbaDAXSOsnFIha
zL?De9_mY(oSlb*$=Zr?ICksVrvfa-FVh=k~EGDfp@M9(dotS^fzPVi0%vr`AGPwJa
zKXta*upFd#z7=UP7;D(I%FyZKxSdcGo6aE7hz{?KhlH!uJEavGowij`plk*!`nYH4
zvlmNs$&)K~B!e{l5qqmgxl-MM2^eWuJqoyV7FPok*M*RlToDHVhm)#KV0PTYdhqtC
z6~zrcUD>O^RJm`}Ug=UZ$*E^~XcDjD=|}zq@%p<@QSI2_j8err16R0w^n`cJ$C0*#
zBC%tWOtx!;4FcVZVAi-z?j^lsV%>1EvgHbjh!?JfB8Vm)5pIXd09121o;RTm&wbs-
zxnE5RFjp``ax<Xqq>Hci0{Q$dWVOI2_sq5$cxGaHhBC+9>N|-%LjL_Op|gZu!6yt^
z5DVA6y!yb}M~f4U*ql^m>i4zdW;;f}o%)Dq!I`KJ*c98nbJ=D(H@>Dj^Y8kb+Z|Db
zS?)j>7#&-YdcR&)(H%#!B{*{YOgn3wnONCADH9eJlJIWo^PWaH`SO`H)J`5U9%>7?
z>Rojhn#v9{(T-_F#2_JYcrmW0-q)z&jIG`6G#2F;0>G%mDDHmUeo>bez1u@;&kurc
zaywTKVPg<L{t*|Xjnt2Fo3t=!(Ct}vJJe+7$A8N2_(xn=JGxtKC~^4nqH98|m*bzl
z9;BM*AJ31FR2-pgwH*7!4sV_4ixcsuD9*yI{!T%UdX~MBqF6%kI(v`V!aB%o&1W09
z#!FbG-8&WYyJza(#<uO*vJdLm#BRllQnW(v;00mbA&@s?#`@h*3wh$U2@E64)>-yg
zaD8jq2*KTa0E=d37mA6@U}l{0IgHM1F9^5J=fuQPMd(J>?J1{2u;Ce9)lncncV-Bc
zeuXCpS8`eaGrA6;A9WefvsD~LbC8+RUnnki3KwK<GH-obkgf=ta-hk$aj*AFc|%D%
zziNB&C?ofkN`7{_3+?p;P}BkaX>PkPqbS$0-AuO<!}M!t{MK3ihB2z=WspFAvhKOO
zA$*Y@D|7jY<M!oZs%yG+fB%C~cN^AlHG6OT%10>fCciZzz%K*`K|Q)$#qF7kj$$po
zgs)mQk@nhcIz)W-wC4U;1c05Z*M)6*AsiCA+>f2}GiqSpF_#<RtRC*8-d#AEhihRd
zLx3j~I@Jf%NK8ug9>Jj34d=zv#SD>q(!lErJjeBJW1qF8EPfY@`<T>~78h|iysW6j
z+IQLXn$T=R4hUI2GPSs9hkok|1@OCUg-rLra3})>FdMXTJn!B8KcnFCkqNrv!54!3
ztJLoS4xfmh1Z9!zEmxZkV~Hyd%K^oQR<MT5#}+`Ym!0j2VUGRY_V-r@Iqt|{f3Q&2
z<$J0CSCWtr6-uegIj_|3h2M7-`$<bovSI~dgl^UCuyWM60yX-;ipw0ZN1<2cohEoB
zgBw1Qa;k(V*VFF9IM$jR2ya2_kr<?WJL{vSz^fvq)(&f>M$ig&{<2h^&$2V?DZ}AY
zO5Z-L<oaj|xH4<WU|fvu5526i`~}d&IL}%4u}h>Fo<_-iOKH(Fomtf7e4fKCk$AG4
zOwUYp_g-=;dlYQqE#P(anNB6+d$ehA8MjKCqNf&K_wym?1$k#OMDeOG-jcuezGE`u
zenDCJR1_svOh0Vo>zQREQSK`ApqdB<{o;Gn{Gu1tLy)-2J~=e2@*BzLh&eO<=Lgap
zmEEKMy0dBHs6?-+Sf7i1-pZ_c)kL@Pu<DN*rfZ?O^~t1;SSiIVl~CNOUoCn|#ioDw
zYh;H>N>}EFK9?fO=~6D@{zS`_Q(WQwT>XkxmnR;b(E^fMh`Yn?aiYSRK9}2T;+F4p
zCy6du2<mdev^eUFZjBcG$pNzdOsQWMH*p{5M=T#BHTH7%v+-H*^ZDaP6hjJ$EBq#L
zeu>#;VJlRE3jhIgLR^1bP`}&pgFA5Z92=W579(912<#%A@i}l5d76HwywUQ0TvBz{
z?AR+C6Rp3j*7dyGpWGuM_n+ZeOX2Fopq+34g4l}t$1HXeISsxK<fOa936yYm@Kbo;
z{k8dVq123GlX>v#vyY=Ll!Ww*Z2kEn1=M&{=CZxj+9Kz3z72SD#_zb`MH;>lgU?*+
zaObR5W*~F-_u4Dlej1%r2rm@zb{V*MJi7Oi#;Uts-mE^1Ne%`}g{uGg68t0<z*kMw
z|BkKPdqG26&S|?2*XwjiVlW0De9hjoSBsPWnd8$NR>Q4$`0~Qtx?L%3Jfg4@#dC;2
z*trz;@L$TQpP7J9XLor&vDvL@2G=?<UpogiWU|UzGzYVG!?~ri6&BP7{L1<U=<#D{
z3j+FyV5QKhf98B-+w!1v`earrxgSg2QUNzw7A(5196IGls~;Bs(P33Yn*T8v&ZQMI
zaz7dkeY(FonRZ-jRTrOGE1TAsU^M{qr?D8oc@6K8u;wVbzJFN`=m-ZyMuCx^IDJeE
z+zBHzU%<~iEHI#2SlRN@juf1c3Vy&MiCUKJ{(&_F2*c7QzQ0}fJ^m%9b)da`7|J87
zUG8B!-RrZhDq%&yRL!jTCir2rWTk_?!RI7FN_2X!59n$>j4%Ok5)-q(!%nCaF5nh6
zki?od#@Yzi)#-WnD!D}IxQuDsKbM$Jy;uH7Cs{R988Y=?he9fn1=y%W$fRM|dI`K%
zla4f))>`%bVwqz+hmBs*coc6*Fs8`LRE9c$qHP!Y-RP~|qt+?U`{t-loams~TVkaU
zxj&c7`~AY$kI<~BXJ<T|+iEFyGF)0>kfBATT0&d&Kv3*Q@mKgUCyPu0dz87feo@?0
z`D<h#A+?h<=anztUbn6{C8ogWCgARti^EzrjgdgV4e*K%&LM9$6%BRkNd2_lh6Bi6
zi;PvrUJ|yL_*$YLThgWLVs>8qOjPfRW@6ZYMd-YS721HbPQmN2@Fn(eQoYsHe4)k^
z>td~0JH(+4lYFBqAh8<0-Td@#!+I){HtJ@(_bl*CW!I;>JJ=5H^s!s0&A&Xk(_vNh
z_MR8k{VR{1!mO(kxm_pP2zU){blJvivR{-unJa?&aJDUvrB>W@`?^S%UR<8})rhg7
zDh0p%;k?Y1EL8qI*KWxrB!me65@{=|l$W@lLCU#mwqTp{JnpWrka-wkS;SM*<G<lV
zeHIl9)h_e6XeE5lr!1zO##&8_sbzkYO;YJX`{;>#HWu^8Bu!1N8k0>uV4Ib~2!!s)
zDk|ILPo@ycbPrgBB4E!g2N_r+{_>A74^XW(^^7I*C#5<sctwQE3c36FML5SbWUH-s
zGSmFuzaK)$2RD>}g)WOkap>n<|3hM}Y9sx-z{xCO{;J4R)jm1mmp_q+%)4A$&VE+M
zg$8d73k?$~h#qGJSid`_PANZG&;V^rXv_2bF7+p1L1~j$XO+J*Zs$D%Zx0BGUSP<0
z8u;0)OErd(dfMOWBsVI<9ySC$9H)l+wr?gprp^0wO?L;=O^5nVw~l>YpN}iXb|NX~
zBy*#sxBTzOwY2U+cr)(ZcS8{LT4x~NtsKb2z0vc^Jpv(;L!+NXqpg5FarZnIfA~7u
z@0jR3v%YU{!^jB}jT-$g$M_i%sWc-$xqLe_DS5uWvs_%34eiI6>Co;<_RlaVL;IZu
zfV2)`9%%~KK|Ul@*X1I%e;t&xyE4PA*jps)2Dsi!STb@uW~Wvgl7x@h<Qh(E3p9QC
z8oa*hD)>q{EP+Z9QeXHtbRC=f-s$tXUg-YHf@2BZlh_XlFrz+#82)KR&*E+lbB#$J
zw)Nnwx%`GGX>^5{q~c$PSws^?1+XHYn%>`Yq2KYucgTVX#tN#VE4{xmlAzy0aZ*2K
z`8jfnZFD1(vS&;C8u6%^lQcGMynS1$S&J-`!WubsFt%HbBcsltTYLGzrayt>?PA?b
zRz@w9<6(L|E=adebK4a=*c7ri2v@E$CSvtt8TTb6r%w5%>GOh%rKt3agYUX<zCpgt
z(*5}Hb|g=nm+Qx)!%XcD0$a^u)xjfE<$t}QkvGj`1Up3r1$c4A+r6^Z-)J!tv*NAU
zjY?}TVRRdm4gOiuG{Zd*BAqIeP;z0PJ@7A@hfXV~KQi$y$43(%x#LG3AGVfwDLf6x
z70Q<_k}hnm%82G)3+SnI>|{#qQS&v7hsUYcDiSBuD92k2BoZ&TPCs&(;X}9i=rfTV
z7L+#MMKjy_1bW%Hf0>IF&`Ow8B-U^S))2MnR$#`<GtcITO?|x2R)AV8zYTw-ZcleG
zhu7sCf~jg|X;z-8+JhqL#5H?&>77JfEezD-1^{(G2-{3Qh(eV>pX{D|7^&7vOieqA
zWX99l-D?pW&N&{7x-=@hFVNq+PC_*Ck!-;(OPy_#bDc718}r2&{x|1kHPi477t*~U
zOQdypPUk^ZcIN{qxF4M@MaVV}JJl#O`)Xz5N!Bt1R#JvX``nVV*NYSv%a3cqUJOB<
z@djR%!|dFRNtUdF1k>6YTH#~&7=D%KJYXJ%;P&|Qh%HK}Y)AT9zW4STgHa(z(qjMQ
zuUe%T#Xj-8*HFrc=kK8vST?6`ZD|@z?)?3?M@@c{hM0_+=)FMP9rJoN^^!#e%>5p1
z*UuK7msai{Rmt>|Ni?kDTT2Bl6k^+R?}gl-Ok(8>|I%@}%?#}>yvc{}B(J%?g3NhT
z-@|Ns=4_&D7Mr!>izdLCt`waEzQh5$D1VK_B3nVjR3{Uh&w+Q028eGjkUqgCw?Fc@
zA%$!ihyBsDrLo)JM)n7wh2-XieqX}sXBIyKa(Cn<;r#M+Bu?^v{_B0Z#p!%GtT-Ku
zJ3vQkHZ<!6yBI;-h%4d9H}~&VTg0$-Kj{C*)Wlh6jO7U8{Rr?n`2hlbi~Z&V>V0y~
zsRMj*VL6*eQCp};xVpdkH=WTQlAlT{S-tmLro$CxK+vh7_(nfrS1jn1Cs0dN;a4iX
z3&rN!`%`9i%YD+Cv#X^n8cvG=OPhH*Yh}0xK$i_3sZ?-TZr4^~F>W?m)`r)yNF{19
zamwPVsGVo|%jy%#%9$W$46kyALjpFVxCiLzN$o^;kUy{&>2ew6DD1p{>KGoF6~r$d
za~M#Lq~701bjj5iF}3{^`223c@375A9ntKGm-P%h#xqUU<<5Al$4;WcygoIck`;V4
zTH+};p7SLwY=d_3`^$BrxM$jE2_@j&^eadoIwM?!K++{}Q#z|DcL(SVL4YdI6eHo1
zo*7NcJ{9&ylnp%y``V4r2sP{#Vu^<w<pp6zVp7a}+ixiwYX0^eobzEC1?YZ(AHy27
zZ1BU}VoA)><XqIqmo=O5{Gjlk1woGc!Faw$OZ5;1SKoGjz0)|K*9DhahuPjPTy_fT
z^8W_0!3yRNP+V(tihUc3m|xqj%VK`e5^%)kxK=x>2PDDEQ6gaLi!%*YIn##XWyD`q
zzxBx<A2)V=ZGD97yQo<#RI9^|Z{&`}nuV{lCu~h;%JN9-IX`S~y|Q9fO$K&lKx*}0
zZI17(#s7kLzr7vY$tFBJj!Rz+N0DXNyL^2LW~F8|*O@>lBsj)<S!(qKQ{PT)SKL{m
z9QjoMOoM{N1*!Mk!zL?p!kWLfhB(zUGJ}2XJxk45^d)lKQI+V#u$^5rw4U+qfJgEo
z?O0pAZ9Ci5H8}RBeSi@LU)^sX*ePSDc$&`PWca{<?)ZOl-4QOE)!-}c@xF=yqbp~q
zvQW#Sp4-vS?4-Ch`X%5rZq`q`uy6GV%w#EO@5>j42MCNI-d%@wTBQtuRV$S}IL`60
z1bxF*Bo>!lRh*To)_BOugdC)*ri09}cb`O--0V^XJ*_S_nw04A;LgPI+uSO>I*mjr
z%Oj6|M5q^eZy4=OX&vxtZ!p&0@XUPsCeFCSo1Bo%Xa=f0nndeaydM@OElN?Xb)w-P
zxQWmNH%w+d1gPbSh2CDQ5eL*_++9@VCaceK`k$Nl`i7}?_AO7a|1?PVI|*~+<ZLEw
z2=Nl&u>x(0s`~A(OzlBClfyQC(a3SY9Zin-bSG{|68IgTT$*wpg4OU{St6ne=~bUq
zFq-xqUaUoP-ig2BiEPoL)1nywFdLmW4I;I-7~yq?8?~*!$JOCv#q@c7k<x6>_Op;M
z73nsB)gTHHgS_O*%CcK-Isi&$P8sJ%DMjh`!I0l&>CueLUu|)R@&+t5wA6R;MFKig
zKV&?ab#f+nu<oMaAj1q}k{A*Ekn>F|Yo|GT>$|3YEtL~WzcD6-zz&B5CS~K27yu~C
z5S%B7TcJ}uC+e2_VHyMNh@!Ce4tgi^V-8{FIzHDIu>D9R<|9ykSDLUxt;n-3Z~)=o
z<E%TAc7*wb<}z%MU3T;*lh4?3kQV;-`54SchBn0NnzdZ&eAmu*6c7Bc@JLu>>%?t4
z9><E@^HcC|%u$i|@*0B<ujNPz>+#zL!c6I3ze`G?UymyPIap)GSYf-jh%032QUyP2
z_#z<C{JC@eAaW(t$-HPD+qcq(*tnh4w6lJo4byTMC!O$>5|y;n4h?+vy|o~6xI`z&
z*$P2W9H9N}m5r~yceg531@?swmHr&|f3}Fv48O8a9vAX$`-TA{Im$fjfQ`s>Hl6>$
z?C>NIJA)fx61b_TU_f~3H@ybmof_S`RAqQ_7emONFdTu^zxuNQ<~8I33tq#51?*ZF
zzkZDA>I+A;wwsBJj5NPkgJuYx`M}K~M)WL<+U0+%Bip?$%CAx3+YzzEp9G8(dmYw`
zF_^UEgI;DIhZCGVV({4F-XWkJCxF><gwF=t9|T@ZN@?<sjsFnsbEIk%DecLwVC4K9
zoEKtzb<(Le`T-Z*iIi>?ymYL@7hb$As@m_wpKp>K@olH`XgWKCe$_x?kr|9q6P21I
z#rd>3QCh6D<9;eNDPxzU2PLVOyS~dk#>r)jwXU-m<aFKnlr?Lg8x@*4Bot4=Ka>Yx
z|5w8e=tpDkisHLiD=5V=K`$Y@`*D5k^^q?dequ@+DBNd2&(D+taHTonz(EHX8;v_t
zr&%IQ9RXGSE~?GruEel|D0=ggFV0e~Dp8YKC6i|~i8(8uSvlB(nVcg)w#K+6?;|{=
zm|6O@u3#~jJ0bmXw*%yPyC_^V2h4U?@#pUa_J|}%Y@<u2s%lq8C?ut{;yppuC|AT>
z0*c+N@T4|P&l2DhY9fa@1qJ2aE%Cd(CyBeiT{f-evH7e!dCVSBA7z#j1ch%*J=iGP
zqpErwo0$GFv>Tv*%kPSvGGiZ&M$x4+@(KI4C1m({koKF)U)RT^zMogyB~-MGD8W8H
zp)S*Mi|=Q7>p-bBp99y$&_E)@dGVv^cT@-Gbe5ynf*#j6Y1vz0(m4n0_%WHfq@L1`
z8mZVjz;>qtEO7tXWI1rxKXvY*9QHY*=@H%A@MNP7X}A)4yoEtSL7G6H=S${v`7Qp<
zs}^Zd2D5258HL|ImB`)stEYMM2t2oSaz|`_v!9$;B9rO*sF=!>4|ql52weFp@^JjI
zOFV`{<FCB=hL5S*6X<uzVt4NBW0ht}vl!)H^?t<T`A4zmHR<JQ<C!i)-7XN^|GamQ
zZaUEL8h{vzi18&95enHuo33yYa$r|4OQQ9Xw{5Ul68;iCQA^_k&+)>ZuG!~?sD%pt
z3Qc?3ZzymGVgUf&F~TL5U`&?pf$=8a69He#;~lU|UXGV#)pcqfdmCHfjn#z2e4_#S
z+M-e-Ndc?p@yy%B@}jki9LY46Y}}<f`trXxCKDV2_v<cR)VMa3=i+AH>ry?y`3w8i
z#V{}slJRoJPW1fpXt#MtvG_-Mphzj*_3-9Im}_^m2Lt0`Zcs^xV8dqDh%XY$a(k;w
zug6)m8+OA@w$ie_A_br5w7G(U?(*w!zM)gqr4}V|^ncSS(`rk?1T`WKkLhO(Pi1E|
zIat64iKV3jWFSE$K9{vHQ^1Q%WZ%m5m@kQhU+7G&u9S%2wZhHQANtOB+^^m^evCLC
zrdhFQNL*@c#`XSicvB^9;CpWVMoG0Wi$|loxYWYo3e^@N|J-KfQYFd6jn&N)AeCRv
zVJFwHP4U<*d$ZiHvENeePQ4J%1lN*U_s?Wq&z)8NlHu!-e@nCE+lk-8(OkRRJ{k4w
zk5s^BFp~zcn3sR8B+n8!O-<tJTQThJ=}6s3LqsRSQN~WWz4b~xzTBMGFkj(U{ZMA*
zwb#*u-PZ!MwUZ8P(nQ8AHR>&t0Bc~vJSInYk1M^!GqGYX4t!J!H%6F4Pqm2z-9HZ{
zz9JV-R3?}ShbTO=h?ayrLfGftv@v;VLi1-CD?S)z{!w~#U@9B`tZRmP>4X!`6|PYj
z&!koQ=P15oK3hs3$6Z<S>YhH&>}nty7CfdkkqsfeJzuLrg79fz`0JWS(Iu-@8aK#J
z)AKu>`oQe>J_i<e1N-YN>vn2-E=z}?7jJL_Cvvz+L0#7glU%1cK9H#_mtCl?$>4mV
zctT$A6X&GmU?fq$X~5k@{4ji_E<Hnjt6J?mu4Z#~Dq?%G>+AWqMp<d!3{4cJwPu$Z
z?jKwtgE%kMJ%lX(vh^E8sP9qwiEH2%qH#HPp#O-Mbhzv)9&x8m?|sPZ#euUgtRwPQ
zPbCywG;;kbL&T1&kKOiswD_MrRGt~nMPozHGU8#(I*&!lv(=09fayPrOsEM)BmGPg
ztkkzMw^3-)!Pob|oXpsI^Sx1}Kd0z+7VoSxT7qz6i)fz2ujP7y>2&-)ww1h3;tZxg
zrD}3qN&h$#zfy$m7_Yr-eD{W2MW}_ol;>M4=@(qKq>z^Q0!f)g($RkD)*X%QHu`7l
zES2iX+`xH??6!mvI%3<6F%(jAL^^F}PZ-%+(=u|kCiyLK`H6iw!=uUc5ct;cjMN%V
zI3XwS3E~^4eKGE%WoUbIJo}$bNC{gIz2tz*s(OJ_atVw%l4!Ju_>W!fn^$*y>O)OF
z8f*<bq;C?RX$_ohQvds61gFw~1hAM!lYRoTU3%bwMNmrA;|(y8^Yo$xay#;iNcb>l
zvwxR~CpVQ@$Ks}=5PWIqvYQM6E_RogH9sUAD5Za!p2@+v^6vPzRZ0_?Xx85j<r^P@
zo_X2GB}f1Vw&{o@^}A#-M0IyGUmq#QbQx+WZL*&h=AtH0CS()oN#8X84J@!<5$5iW
z7p=+O`)>ZH_=CuyYjC?M;9h2PJJ#hfAr$T(djcP0!8)&xVC5iPnA<=i%wr*M1#*O$
z<<Y55wh7%a_BGpkKVrD_u<(u_bL-#q^h?}IX<X<&+%UnhpGBo3>Tlv^lELeqR+kb)
zggJV@R$2E7dKPHDCE9ZyGM`t=s!W)^M!S(3P8UA)gBVe{KsvqqIW&_rT%VOsGl+a#
z1E1Z1srU=uZ0zjJC>lN!^*KeW<y(L+Hgt`-1JuNaeeoI4Kz+m|h6>T0At_mxD8?`R
zg4h%xGb=aEKWY-O*3!y<6}c)JF0h-t!AC2OY`X06#0_AV7PzskzqGLXTlm3<F5^S!
z=+}Buv7ySS2HR;j2cpcMzmw_J%pPtiC&xS|G9b(^PuFU!TX4H&rLc=})pfwlu^T)b
zc*qDND2+GLMzRk$C`?l`yq2pr;p^h)U#3S0GL+3%s^B_aZvl~~W(-NEg&F1ed%4|h
zvd&h?`P@*7$Ol;E2EF)+ZhrA+GC4ATpS6mz?R%yY7x>8f1iC-E*|6yHzncYum~_hj
zwqQGB!Twp2@$|FYoo`i1F`>+Z*UoY*hUzQbpZ)FD!hmVDwiDaiZL-2G*2N23V}H0s
z5YVfQyNbJd*IPR1jW|loB8HC7W|4()i|HS?_+2Q|;teBADIf8115^jZ=U+y7w&K4|
z@E&iyp|MH$;)-&Hk=~DnW%;aj7GLD4sLdX}q}IZh##Mju=rmMuj{B4T&HEk-J8dn9
z&nxuSRG=FoknrrmQYAH&jXKHv$7VQlDCzpEzyYhd@eYnRottuQ(|Ja9-vC!++UnG$
zqHsav&vyo`LA$sEE#H`Z5ezAN&TzKGz)agLQc|C3U+6ww+)JV9Wa|&d^y|GV-)E1u
zFK<o0LI|0ySI%#oCrnQX<L0MM-E42sgwJ6-gs2pCgsLV{H1oDAPQQAWqY5IeGoK&x
zvah<KgIA5Z{4qMgeX(<F&`&G3b<1K<t9nnuU=C-pE%Iv$6H-AO_D&deZr}qWIx!#K
z#ahdJnV)fZo}kYyOAG;P#G^lS$TCs9I&9a&_a3SXITQvRdqRCq5x~+bp(7`B1ndT3
z)8hAdNVg?6xK^;jDwL5?D9Z*Z-Bb?7?A|QNlSz)mqJjWu_cb?wz^BFWTjl7;i6%Yg
z;ifvWAg)Cv<fGQ6w40Cjxc-E%BjS(`Ng`S#yA_TndTM2$uJz4pf%wz44d$j&oa(^s
zb~Es_tYRor%Pw^Ckw4O9YohCz5#`c}McY}OSbJT2a;B8m4Q@Dbd@`r$W4CdZ=ftZE
zu6Y<Sox4Bxq04|a*pohbbmVONP++H528bB{2$|Um_GJkl!>@mM3|~iH{CWLtrB81}
z%mjh8zL{?DE#wmI=$SuCDXu@&?wWJLgyDC`o0t!RuM^%8X5yxJ<K(4m3gxr4s%su%
zWYL&mWM>Vvwcy55%$Ea>w;pQ5>B&h+P%(YL&B><HE3ez+iWm<MPnBh|>n6PBfKwQ|
zkK(@nskID#6C8mu1k0g=WnOQRi@Uv}SIu3hQ2|}E)7YwJ3zW_GM3zr0eohWBCm(UU
zWiR)xX)2|lI+E|Kqg0o(J$}q?5drxFmzIyM=h3)vk~=wkg4kFLJY?w`I9Km;;I|$b
zedb}<r1f~(BL8)<N)~vlRGsKy<YOT0MbDD?$Xqg>aXT>dS4~$0RVb3>`7x(nk2h;4
zMj@ll#@GrOza+4c77rI^bKS7GXS~$zMa6eB0-ZeOq~|3j&t`>vzF2Ko`1FCGZbU#K
zRLJeAwL7>nFi=~@zAawxY^j>tqz#doMn^T9ewc(-G3`r!Uv<$NHu&hzB=EpmtJ+@F
zB$Jp$-1jv|T8NYJ_##R-hETOeFdjve&TKwT<##c`pD(4$RTP<64$i!z!@pT=Yh6ag
zEE#hOQez986WfP0rsiJmZ%4F0d4!No(YJU^sgj@Cp&Z3KtkjsNIaQ8+r}IwEH4<^f
zE@{bGx(n7ZPfCVvTi^O-;u>)fHMK8srG>5I*Sw)6Q`FDnqEY&AsQH?)*)gghuP!-~
zEQlRUCk2dAP(?*JoP~A>?{2vx!HQI5zIZcJ9Wm^}wIsvlz%~Mhu+avaiHzu2LiQ7D
z)CTyUd)DLl;c~Ts5b$!ZYcNc`Jo)5Azz3h(!i|_`)3_0;?)Kk#U!Ot?<q|C*Nk#sd
zZvzGf2Z1#`hd@egU3ak>@mGWIa225hg$uRw9_?iaZq|0wONFCfDu8O_NF%^kC8udd
zB>3|<g7wswTBPx%-Pqg)`uYsgU(j*d5ysaHSTW%=_jh_(ehTvWMMfA=O4CENd)Vd)
zDSjo{9$YS;Oirp)Jk8|6GH+yk^9JrDmX^@MjDT4}5;h(@;Md{4PUh2s3$MWfzq9Ih
z60wWEiuikhex&CC-g?+f%@Mzn@HtyK9*9i%!X_G~|MRUq{NQ()BI~>#lE2Y<4BlNm
z`sgmJ{Xr1<h`-@LLCEJe8#3nSNi%)r%r%nAOw{INW!jm_@%fK?>X_YXdC{%%D)U{V
zQ{mBd7UZ!~8AVR7*AWgxcS1LYhXQU)9ayyQ;_mf?ZOvhaFI@pusxza$M_krp_BGke
zU~_oYNiOo*7waDwM%x)dMgtq(Xd5D#mJ-##_U2MnW3BZ+wYPK<dfWDH2GW)=qRn*u
zERVGw6V1$pLSWNtrK6DdFFTRDX6Em^a-T})E{j@oQ*x8z@12A7Tl1!M`Z6h%4#NAf
z6w}b{8aYYjC1GDxXTj+AN|;1SON=HB0x?=Y9qn3#O?ulr-3U##f)BY=H;)|u1vKkg
z&xbpW8O>uXO|K*2UmqwRIkK_t&rR<XWkI4Ea4eiLhn?x^J{P0#M?ms~0hqRNQRopG
z^^>@Gc%{N$e&4f+P>W1j4%_axUE9`KpYe%qz7cM1OGx&dkCg)%4=0e{h`3cOKUs3u
zhryA9Xwiy9SKM$$ob+@+hvo28j=#H`<lEtIIB-=U1P(bWkc#vkNv7wmyc-zR97z=>
zZ1(|`mrxyVwBcSISq^{S!4#lyh7;H9;4UTdi+D?&VK?AV+10Pft64dq7iSmW>$pV?
zc?SMo&86f*nT&rXVLXnLL2F%E&h4_+Tza8JSFM>LTXt8jko1pemozrx(*-L~?*AV@
z1y9?_YK*<Sy5jpQ8KA`$tQS2FN~uVU{={vy`CJk?X*n5S31u<xT1|XDIpme=nm>;G
zE<ZOp0ZTYyFWjW3I^6?jN=>IO2^weF;d7zEAJ$G|^u5dGm-_F1^OaANaKYoM^&?_N
zGT4{lIGYjAHI-Sn**}-Bdiy)Lc7daXJ~yz>#$MM%)8E8cFwf1+P4Nv|?<|d694~R1
zQCmzd%Z84uQUJ#{rS;njjwc8<GV(;=L}lq}#p#K(aYA2b`l0z*XVvM6O(hS5Q8Ncc
z?nA9VolQ4gzMSO$9W0FaTih6zPT!x3jIoS8Q+M9=3MnUDj|;v>D83!i#|>8P`#F>*
zWw>^84A~N`g}ot2(9bBUbDv;q-@C>~7Ig|}M^C^wzjNzZ@AF{K*af}^MVtnoZ&Uub
ztC%Yhf9iln=n@QtuZN(h1(KqFcL~xy#SBxcdfZLjj&_U@lK?aEzuCe^>~HD&1WP4F
z2ED^rB-Jn9<KC_MU=v>8q^^+4B(wg<;1?f5UXKKwp^|)pocw^>d0YMa4mQw0!gz-(
zHMON2i?|!r72=YKKnP%pa`5zexK<<e;7HBvq%B#ma;%`Cq?TbHk02s@D1^I&^Yw=K
z$?FvTMZ^ygkd55C0i??CVENFHSndvEv7yvB*bzMM!i)Ia@Iafp`*X;_@I$$+IQ{9N
z)lMS_jgY-S0XVeM0_bsGH|h>Vz-KC^yP<x{TIK3;RXfuW+DHO%OIp8nzh}@Z{RGdP
z#ath;+fC=dnUa!{b0uFnBX@Izy)PxLwTG{S?}@zwHF&}zic#C(`s;3pL66IJuu*%U
zpj4WjD$x|EU>Q_kA3Czqao*4OZe0`}K>ytR?2W3J!tlH5VG!30&uG2*LhD=Bfkv@1
zv8Br@z2GfkpHE=IUs#5gAs-6w^BTk0i)S9;w$iPJcUF5RRQ{vRg#yT~P?X)YTUj{n
z@^k%73cE?c2O;m1xX$~leC@1&8!~rGG1wP1@B7;<7xRe#(C&Io&k`06U)bs=zDRr6
zX)3dSEjYhF5<Gr$BD5%abbUNuLzz}(5UpzhAlI(Y?uTotXCLsV{zY=Ir&JXE@rUts
zov~T66AK3LeHhQy0zU;u>~Ut590WRqVTHphK);*NKPnyGLlvF^VxP`sOJAPDWAP+9
zFfW{5hDMXS=g8ln+wR^r?|7L%y~5uuae_Us)d)GXB6%1jd|!gJ>ruH=p{Mgm@QOR)
zHAe)}$<-^!KG6%Q`Yo(y-&_`b<y09KSvJmno%;LqM+QU)pUDteLdIn>TM5IT|4TSQ
z0%`rk<;q#8iB1WtYieeWeSsscu!mvUeBD#>LB{J;;D$iD8+t4&!^0FvJdt>M;$Jty
zZz&y>8E29khwpP<N~8{9>xheg;CeWMMB%p7j&OM#S^Rf;6CKI%Cc8HQzW5a=c~h1<
z^N=7|qi)gSx_u7rG=ER$mmE%T`6E^5Ss?PZkmassLB(2rKv;I7&l>|Rif}&dEh!{u
zi_(G5Y{C9l;HD?0$@&k<JLNvLfRer8pm`1_kf%}NM8*j(9Vdl2xj{Tc_y`zEj>hT#
zXwli=L@euU9HHC7Ivi`LyL&%b0of?#g>6Rwz&&0LkOgrEP7A1zNVB_W1e<9hl@k9>
z{$Lgd7^HIC^CeYW`*#b9W(%d9W5}7M8N>dH;WT_TQ_=X^S)Wjh$BG6AKC{7ud6$Yy
z28tLiwX^I_0ZzChPd%A%F#5q*YJ*^IR!)y2caEr!*abJeT8_?M{)ek0HS$E=Hbs0M
z2X8{v9I+~rzs$9ctL}>lit+A@hRt!gvOj*QGwW0i><ud25DEA4`sNT9&DUEqi#*@V
z6N&nx!*#!3jiVdkhH{GT)O{_0h*k%=R24S_r`T|d1dGY_kIrRhjo+0t0Lh(hFPW6x
z`q@~JXyS{sgfvsYGjB2*#0q}WOYSUTmWjJf{3gR;3du9dqBm0v*thWB{LHVlT)U=v
ze$HjP9V0L3sqPkD{|m;72HYMCc+d^|+U<Ng-|9k>iV!OwFyIy~>Sci5tB~^9W6j{I
ziAMkSu(AC2<%ph09PpBzyRvG)6qIHC?9ckiZ(@lqJ0Ut@cAk17OPH`kHFxfP+KK+&
z`*i+yluydWaPpxxnV=_;W{Gm|UO2!IpW<LFeg0@H`J0=MlGq7+e%|WYPvlAdNBWL?
zTC^nC!5^zwFq<8m)3P<EpyBPjoex6uH(@;OUlhxLMJUKNmjxUF6Nk<eMpP^bCw4_O
z8gy81g1|pmtQC19@egPc#ma-%Hx0<P$~Hs*xZ^!W30ccr{=`9nK{LB$nC1EQl^irr
zz5;9%ZFVe0eHRWsZM>Erj&n|Vo71IgjH)i1zIN9I3^C!e=zjFUYFoeb(b%YdaK|sk
z%1KZWcOLUpoosVIM@@?}4Ecto(m$myycikJU-|W~J+`M`S8Uy9<lOiD^z72%pM<EI
zl2r+<>D%AhNIV1-CuPb@KfkX9j_B3!DF#f(o@qVji<h*_R`JJL1pa{Q0O^{R5M|(V
zVnwrJXZbqZ)*OP0KW4dnDeCX&?M66!(h$7Y*-opy=??GEQy5xFS+$?%nv@7Hv>On+
z{Lb}QOe*zGZ_67sih??t8FW~@&f9v*^X!+a!!I}6Pi7RCvf$@*0R(|n=+;D38+Yt%
zQEXzDtkJ<4HS?h7baUYWlU;Be9Ho4gPtX-`*V|Fvb+@RS41IEce#DxgXcOkN?xO$b
z;f_L`FroX7b+K7<GMifUxyVivklgeK=<rg6`wtBTVgmD42sUap?-FhCz7W^z61sN9
zRijmsOu#COh$k`M+37__NB$arS>+43A!0FY$CFu<vWFY?+uc-L#tv1o+@NFH%8xGr
zB4UmLY{s3XqJHobHNY<ZT!~=^v!wAX41{AP{2j?e`EG{5UGH>;*jM*J_h>ghA|iG_
z=dDL}B5wwO&@=vjqE2gl>oL710sxA_Z9l@A&KqjFKnQK{?-$&|CoxjML{>?c%sc?5
zj<Tc%erlMum`lgK0X2nn308r=g%Yq<*(%=PjWKhQn9;`l>e1J2KJ6Xh{D8N2vtdAE
zfU-^LD=u=Ikqh{l-?UwdbOg6R0vpuC2eWE=66oQC<ArBS=Egd!Q4OL$)u#Os=#*n(
zy!J~AIjG8e{g02TVexsPXjs%%K8KFk2PA1yW&TBRMV?8(<T}448+hwP@ZR7!V!i5*
zV`#mdP-|IT%I}SEk3#hxS?4T|c*&-Fno0GB*k?v`0oBWqF`5j>K6+fa7;2u+6B#@c
zSw$~z`m}2?;p>k$#77ZZ7e8dN8-V83tP;Qhv{|=ZE3i_zE4C)w_+W^^StqX^Z);4g
z6|FT*?~T+E`}f25>#E<1Mqvg=1HPEeW;hGCDCV;2HOX942){)&KIZx2y2(as(~2=#
zUNJ7R$W}?lfBmfM_TlkJl1)6#3@Xx8soC}M_^f0LZHAUVR{ZthY}rriIZ3x`%Hfuu
z()8NpO)TYAMW5IGBA4kARwK&`0~y)(5<?y7tuMpC-z$Q~&C0}Zs&$P}<1cc5pBB4n
zl0XOOS>XLdr=0<z&E*77WI;^ckw6=bT;eIFqyLm@o><YDyyOoBbN_3)5_x%9HrhRS
z)f}N+vAnZVXBnk_M<T^M4X-|@QWe=u+d|-?*~!%Y-vMb+S~asTryhyqxp)P5xf`aH
zYljI5Ty9D5?i(H#2XUo<dh#ORRBQx~qp5k5NqEP2;sqzcMBt2Uel63W-F)fPTz{AQ
zWpDBtd7b}#G>ThUp9-P1@Q~@ed%4^c>FCWsS<i$<Tc8?8GeyfL#UC1*tNoh#6zf;-
z&xSsXlmu{6%o24(G?l>8d09!G?7WT7H<xjeBH!^8)<RKr_42;L;qWeLboQO|(@0FR
z5A>$a3$2_H=%mHVc^HDBiNir**GEQbTuJ1<+0PV#U$E?&Y&rv7FISq$=Sx)PSJ8i)
zUoR4xY7_u5ygGp#9z*CkUB8Y7aBdHIdLEr`C<SJK0p0?pq(`w0VJ1472D&jzs>bbh
z=32#$tBo<^WHDm?doTJ8vS=?ZaNXDYY8j>?YP1aZnQ;hHQN{Hfwl<pt`FB7i*6X#I
zFMb2fu7p#=vQYH6z2fUqRDa+geBG@^{cZU9)Z7o6fpc<>&0T2rJ4F7<O*D+Lq7l$6
zimF{p(Wg_VEu-aN$a-C8`+ob4Df){yhrMUDIe#pzf}*_SO}rhMu@_#2m}yb+_yoW~
zY$>P#^T<0#Dp0Evv#JqEG?6u;>ECAJ7ON%DSy$^IkHeC#AQDsR23Rg4isuIE3QKV)
z(?{Ku7;C$b&wwK1*i>+V%s;b{<u9#HyOu}y{77L<{Ny+P>9aFM9IOh9iqR?pUY$7d
zs7Lo{N1F8EXupWvaJ`5;q3T2`b8V@VIBty*&F<if%z_0)7GVJ2=21<CZ@*RswucFW
z%A5aX`9OO+CNKLlY~agoZA8HevCgngKSE`@Sz=s6?*ZK^A+sw{a<J3-ZN^$RYd2_x
zK-}9oHm{?(mtAzt%w~EC)}_llaapJ+u1)dP`is&8#R858MLX#@MGumTcPXMceSa0G
z{fup)T$>bnwmd(6(+AH-qGOQq%=Ar2Z;ANLl&bePQc+fhl9W0LNcVp@>lE^`FO+(`
zg=bs{4;gyFcYWakL3CbZ6!}OA6ph;DIaZnV-}8Tw3pWnmk_&T!62-kD#ypOuE4*b+
zz2XvM`!7X1>b_n$z#Z>G0T#5D8$zTdkGGUxw;PaS3D_kT%NM^kL@4V`eqDm6_G}nL
zT(m267WElUJZqO^v0p?rlJ-8j9b*j`iKVcuXqjId2=ATz;2y{db_&y^qxaw4&=zOe
zz>drCeOyifeuXn(9C-bw=107lGqAA4!d42QUtW=B8+6$Rq~(Capq}M|i1+l2U<*qN
zD2nC#9E*tL=>LZ7CrPx(>U&j|B?&}NBmE5O^`F}}*Nts!J5#|K1{<4_)26!Hd%Zd2
zu%Dp0s*2qGY<+4`^4B*2ts4D)Jw@C3Aq-;lkko}|EV0rf2jIP@+z53Ic7t8Fko#vu
zbP_x6GDV%;7f@GCNjny$+d8y|T(I+;gSy66c}QwkGJhYK*H%dNV9!&~&wblH?~vNE
zU!}CBSsF;?3Fr`xS(K-&*dyF8>9X0B^IlH2?I6AMnAw*o-R|gC4N20R!Ci(NB(%Cr
z1U>Mhv@n*-{ObFR^LR_O`<*HBmI^X}-CP}yQLpB%ERe*0DR)&AB;Q>*mKrJ_jlsRu
zB&A5L;e4)Z&7P#K(4H?w+W+%7j+{fE7D1sk_!_xdhHMBQdRNc1Y&lgNjpK0zJCZsP
zIrvKFL!2_IiO7p;mAx09`y|r+wp6zk{gjX;9x+@oUp+s`u2ONnk9MZg)s2Te$-n@Q
zwRn7UFeYCcQedZDz+jNh62nIu`$wgW+Rm^Yc`a4FsFLXbwb0m4n+flK?7ekRTwS-u
znS>DB9U2HBxVuAe0t5mC3r;6UfZ%Sy9TEs`0RkkryIbQD+}+(8C$o9K@7De9otm1O
z|E6lDs;djCn&zB+_St)#wbt|eWM)GHREnkapXP0TFK73#9scon22b)bW!u#nYkX<Q
z<<_Iw`B2~d9BcPd)YC2NSLc|J8@jXKHF3dC-aVEmaEHFO_-5kf#S#8sb|#1(U5v75
zF;ju^EYFIvrkU~?2INazo}TeFtCKNY!>ZtB+#TVad;aT~F7uEkdSUn9*J0_a?gPn-
z>u^C;?9YhDno-X!{|djP>BvFl2^D|`sP>z)LE=X{A?*wdYq)rxB{rJuk^0~fgO--o
zurqYsjX5tD^q<3!h$We)wY+z>O&xxbfsCd>m15BA6Ccei6PEdws0cM8DZPj=BG#>D
z7M<z|tU|w=i{1UJ;oZ?(wgs;XJrM$*s}IT5sG?^xj$2>8fxg66EHVEV(q(D)$<2it
zsWUEj1ANCP`Nf%Dch_A)X&BO1jUtBwFZ%~mL}PlwdM#Y6htgc!$LfjUQ&4)HBHg$o
z0(>o=anG|kG`6LP>IErh_2Z8;3fWx4$=w!<${}wrxP3m0tx&I&ZdbqQQe|u~=;NU`
zRmYyMu|b%W<cw7NIT<{PFvm#Gos4N2N~F>7r5fH4z~|f;Qti|nVAPC0nh`I6NVk3<
zmLc{Y-Ffln(8eXwRo3Q>$jE--=g(*u1ovSs49VO?ES6U<Nek2WV!jvn8xv9nzhQ~4
zd`U~Us_IZv>fmmKt4+%s|E5HVGl<-~s%qUG{}Uw8xE5oV%mR9<-E5clqUv=4Io@Km
z@H1n#L#2aY`~yqk&mOJ={NqZsDU1^p!C7WpC^ciYHoq5Y-#w0(uuj2s$Ys4TULJgY
z-ca^AEY9EG-*6-|06YqcSBcnpyqX`aVxb@rlu|D9;Ck@{uQ}z1h$A8zfrJQ+VP+Qe
zj|9?SIyB)`+2=9Md=*L9mty@|c~jSZgXdy`P5{}VJk_Smvy`7dG+wAV#FDMlSVwO_
z)hON@^c<!qQrNLJsg}DGJE<?3?|v{cO-$Z;wblFEbAB?PuPfp&@v7L>Q!J4ao4~vJ
zCz4Ng#tQR0%RVw}w0%d%AB{`5{^12(ZYx4lPooSy3Zf+Um3fl{T-+xU{bX582R3g#
zZi{e~MSVg*)Httlm;udIqSw!?nJ<ykP;E%xqH_P+j>UdIs(b`m6x^7z3sGZsEq+}L
zc<289u5v*ZHBUnSD&6}S@$6s}w$r3V%((m8rtE0n+lnvl4}<D&<XbyaQCXGAENm|Z
z`}TZe=&qX68a}#Mzosyy`~>6gazQ&gJ0+E+!LPlFzdSb?l4Qebjktm`KBPu}K2NgE
zgvWs90eFrOj0@$jK!eBE6{`(Tqt&3-veEM~zV{~mBri>I0|puDIb9V@5`HH4D7;w7
ztoTu{w^YN~*Ol^7Zv$D0a%smDS*WTIqX*mnFDXOMYvg%Bf^VB}Mx(gs8P^XkOZ;{1
zFBcSzBR5QN?O*F(9UrgUCYHUM@vEx-(1QyRb{W=MO>~i#k;z+(&ZK;mgB}jak5eB^
z70IVNUI03#I*0vGHo7+*tq4dC8?gP^Dx;RARu}7jv%94vVe{xTo2%Bx2*ZayNGZ5<
z!h^e$MfuGK^5i?&?TA)y=m(HI)&}Ix`Q))^bFkM_Sn+dThrE_=xY|Jx2*YOyjiyof
zg!9TaSxA}U^5$wc&`=|TE@l-qra*X&;iTYvP1}!z2aiRyQ1#4)GUqF%=2GP<H<~cg
z1`6924Rl6dM<R^918(y3x}a`|fB3AR&Ol=PJbXk#3{hOXl|J*S$#z-xxmKm&IoUSi
zTGRf1{E_H}D2q4K$fVRh>^)l!i$rnIi$R$we54#M^C*$Q6oIT_oT&Mi?_Ce(j7&9^
zbvwuI)oh(|&jmst--?Yz!-aTR+XW(k=o0Jj9(xkg6!K^<ei92y*`4^!H8;n^&oV1G
zugBk|i)$W9PP)zy6kJf_n}YqztJ3sy7Y>EIibCHEF$`&7z|6-t|7~3MNIHluJUbkp
z*#P_Sz+E$lU+@L@OZ@u#sQ|w|%$2i=(O}F0V!I@Qt$9mfAinr}=C#HU4b$3%KuN<(
z6Qu&$s)3+)>r9>w8gWzOamLv-M&|d^Wfx=e->P1_tBVf-DHY}Dzcy`e>C&m)uCX5R
zsJ%k<=!<(*gfhCUJ|jY+7K6zqzLGxlD=arHzlq@pwHITtDr-rvFsd{F1DE<+LynMY
zrc&Osh)+wuR`k|BUrydg?NitH*sGT8o;}?Y;v+)YvBf!DFAOCLUCR%6%tI^a!5UMl
z)7-UeVfrm$CG4rX{3b)lCfP#JiR=B{>%-}sOVfS9@60N?QQznwV&&?Unu3md!H>p8
zZJu6m+~ua*8VtExRncgr2L&#?LOB=NND*;Y8iMuNItvh(uV8*oOci*Yuff8FzF8tI
z>xco1ylc^_;~5@pKhmsvy(d2F3J@1Ihm9f;JOT*z!WHKBQSGZ#$|C3Zf#|NlOrcUL
zz_Puz*+5ca?W1E<>F0Mtuf{)|ZWvUiT=R9hD0{XjG-`Ch*d9IRp@b!?!J!~l`YmMt
zSO&xm+osclUk)()_rv$q7pNoCN=1S1D5R5!mP1xKi9tq~>21r|o+G88<3YHf<L*}L
z&s^oywmfwv+x;D$)QxKUc-HIZ0!!afUL{qVEuVc+6Me;RfL86SSxRTJ)wz7JuK6S)
zt&LGV-%ObTuY%kBkJO#j&(R5uJY8+H5sy4u%AfFDM8DN^C{dv7n(9jX3`hM*%jO%S
zE7wp`Q;~OZM}oFVK6COdNry3;BO^RNkJgWmC|0l5zChhhN%_nR<niw6fFj<l;Dcx-
z#)UbXWhdk_bicaH#;{HV>j#gouV2AISU~e4jv|KD=kN|arwyW|+;(!ws-Wjgcxv`d
zmGGg;N$9*{iR%64jW@PEPnT8<YJki<g`-NHQ&;VP#880oZ1L>bji=Z_auRdrGaStq
z*iNkISAA=ftMmN|iPs+;%5Y8n-)R_eM)$#3O|L)48IOl%7##*;#w30H;UP}HSRwq$
zso?j}>yRXxC9<`!%PE(aGIS<&g!i?#Di!Zl$Vo}bjM{f|b|%7*SU(k;o>z?nf1(Dx
zv}s!z%9zT<PL3$`*>}>vXAUT>Q?OINl)DdR8czFaG}l|Um;L&a7pl-qlfU*e_2a93
zHuF`JmwVHN6dyim6C8T%XwOyhHJsFK3aJ*6%bqMte}aATi69UgH!B)`rXRo+r7##=
z7gA&;Bfp07^Ot=~CML0Hr(g@y@YNQEMF-P2m!r9%;PCpu50wTnZ~F<f@Et=P?02U3
zcGtu9<FTj&I>~x=f&%C|GK`y|^YGQjf36N~zHCr-+MkKg1*I}m=8%w(uGdNm?+V|4
z(mQx`00q`=zw&3E6i^n}ZYUya6zKkj4J5W-ZAe)AXNG@b5;)cSOIQu!n6)6vob5xW
zr(AEVfF=>#2}imEX?OLdAIQ>RpnrDvhMCT26CNdYZ^k?wwEy13wt4)ej2<lZ+<^L8
zc+*hBH-hO3t1?$m`L}b*wsyw7ohNF1Blf#I8dPqNXFQ+yCRP_xa4t7&n&bwLb(Jgk
zQ(e>6Uv4;c@iIlhMmTEcfpMG!qrkjhk76`Fo$a?GI-R4FV~NDNj}^#Vt#^9-4v#}h
z6C0~7Q#KhBMi3x)I9Z!=eLBKysNd_(q3^Bvh-rIXa-IpEG(6itlS*s*P5zFKH?0LL
z>h{Yn9oJBZnJ+~JoULN=wVs{o*n)YmtkSn=|L@f4)Z5+h=Eh9ZbdP*6=vs4KoP^65
z_&r^5s?w9bY;~Bu{B>v2N$t#gg4;AFfUs#DXdVSoxL@*37;{5DxxA}~N-{{?-&%+=
zerJmU_Pyq|cB?W86qvy3A*3RQ`jA1US{KNaT~9VZwy-!Y?#q+Fn@d2o;cz*6elDm#
zl>fd-*A<^tS8g$8j;>sjb9AlcG1g8uUXv`pxr?D8HY$F1@es|S&OsD;Ar7DQj*R)^
zEVa?67qH*1N~wZ{LJZpc3z4Ub>-a0*>?w}BYmu!!FFr}p;U?{QU7B~#?YcB3uwwDi
ze<Mwx(X!xTNF6)iePhrMJM!_n1pg2`I^(YJ#W7aB-KCt-Z9q2}Y5TkQl8K`Body2W
z*3}<c^!kPTy*!MYAQj$RaNi)H%c`FLZo`T!Zs6sL+}nlccTK@H@3uTwYUwp96DXSF
ztK}9dkZiIyVvm;Mv0iwa&o)k^bQuo(<k8}>RgY^ZX!9l>uRHG5Q{i#y@9LqUd%oQ7
zaL1S5(V+*l`Apxw5`Qe{A*Hb==$xk^Ynv1jL4j5NLiVV%#%No$E<Vn{R{;vy7NEL*
zO*X4MaeEr2wC}zB=i_a;2@9=aivJxFrWuFil$L(^1|`UfYoaj^s#SUaeLU}yGrU~2
zXSG=7NQMa!PN1FI=uZrC^}hWE*Gr-M+-1#myqSJ_ertk8cpJf>*4$ocv7Pz~xE!@t
zQ0DCZMu8v92Zh8mwFO3yo!>tl^1Z;Ym#)dT@g79x)p-linYGpkE$;|Qg=JT&eeM1k
z+?(<T)=E-5KEx3bHmZhkRGDVig4Vh&L{;Lz)JQp~JzdGP`Pjkp_I|xDKDKaRa&u_(
z!I)(YyIKb;=jgC)a}OZ$j%P=ksctj(3p^jn>-U}Ttz)GqMPr@raF;do&lEBzk(=L$
zZHJM)&t%G?#Gd3gBmYH>A|n$}wv&GWPrt>7v9U-!X}}J`Kj*W6u62(muzS?4@<Fib
z^F-_*Z1D*zLGbk*W4X%ux9{#Iyb(ev?V}(9PJ8@KsVC%J;a-~Vb=}F<jebP>E|>vm
z1b-qLODV(KgJFSLmeZVWl-A9sMQMsjcs}mG?JG<MNOkMI$Q#|~a~myy?|kc@k9|jT
z<$p(SxJunGC`4@ea~YPL$psyd%QyQw0a@-#o_gMv?X}q76JR7U3`nkbkVM{nq*RA|
z@jEv`%gD@VDz^wS9&Y%KR|oZ`@Sxioayz5mYyC~+!5E;2#QHg3?SGM9PEm_Wz_?}3
zT86d>0W`CUcJw@k*CG5|hw5|uHNxkYW2^osE>~JTONpnP7hT!+7@#?q=HQO7sdg0a
z<sU-t8>?qIN~t?zcSe{&Hvn2F6Qp&o1C|9~Va6MP1*Ltw4R_)baT+AFRk3|glNBUY
z^!^Uj#OAJLe1!N8!M_lac|dMb7%nP^f<m-mBfaBBCnq1-*_4%KyV16oSDNM@+9~+E
z>-$}oKXvHSb(q@u_0Cr*JjJ+5?zQKlQ<bQjnJI^@L$*^|6_>TGt>QCTG9cNR`BYIi
z&}!;3?`=i{M-{*vgyBzMn8m;b2|@{20;BU(dQ6{P^gU%%T$8S0<>x1Q$hW6sDGCx2
z66O?FFS!E0f~OXfMUFZG4qGMT{?>zbJ7Wy_H;$D6)YtRdUCd$gu*TNxqnlptz0JKl
z>rO~Z<;9!E34OsBzDTE~D2c*_{^u*mLT`xkdOaXTcH4hwKo7*I)(PanhVA*>i>a9T
z+ix3NN5)9?t4sU~pF8Q8N}zB|XvA~=79mhBr|_wkj_Y0PAeMq$VvoR@rBu&Ie!k=w
z&8w904*m2<<~g$moztQB<4rcUN&Q-nTBaX%$D)eZ@m9SX455?BXR`M#bMKjPOg=8S
z2|UFrOpIWVbzXh<f&T^W$|#wWQ?RmpSL_x)q=D2YikG5QPfasp>@wieWuC)v?=!z4
z*Qbes5CiX2pVtO&x_}8iXtjI)lSY;bUrRJFH+1U^U7P=zmv7Ine)r|8Kerxl*^6|V
zALG#~woNX2<Tr+#?~L;-5Ih}v4&Oo1ofefv;ST%Bv!jt~sK9~357kDid&zq9!XOn1
z`(2xxDxpb~_g*Q^O#V7*1&2~r<>&bsbhuMJ^@HK{GGSY(L#n}k_`Sio?0Mw48jLdO
zd@mVUh%(T%c&L4$`GVBqY3pOxy!QI*(<69a{LD3<oP6-Bju2}b&x_F-+YgWz5?(TE
z>z%=M_gF6^CtvLKs_F{g;3+}K!w``4VdrE*?<8qSd>#j=<oCZ_L$xv37!?fYUG1sU
zQ(-N3?drv!3lD48ys0*vs*`rH-lX+#K6^yypVAem5MQ!8<G6>`=rcblQM$dN&Mab&
zGm4eg;hqeKc4xBG+-%0W*pE|Kig}aAzsTe;y5o?VO(1P2geL(%zB0!?4F|JDib9>r
zwjbgM4+@2SPkTbQZGJdUJ%*H<0DNyg%>8|}#H^FmaPEcY*5Y6B{SfS@%2WcZsFT%0
zQhSv#U8HFAbMzY(KeFgp=gEk)Y8X$(_Yl}9XC(?_%SJKD1x>cFZJ*5u;^o-%r{q5W
z*LX@=>y`&4E%&%88=sYq5IdUT%N#%we+ys1Vgy}7ZQQ@sq8z-ZZpEn^F{)M2oVn9q
zIAR?~YN}lDS2pZI55&hl#)0A3I`I~f-{Hq&Mzmh}W!Y%Na>;hRSnH6|ZESdAdz|6s
z?hPziiY}Q$i7bCd_IbWRq^vBam1bIw4HOx!bKnW{%v~~m_mVJ0AW|7_2@}%cTUn3T
zJ#MbtJ?dao*C>L;F!8Ny$0i^DNg}^=*`;C@amB$YJzLt2+8x!bmhyVa&yk8elEzKb
z4d0v2+H@V=WoCUMvfZhG4+}seI-3J|+JklwVEF^}()Ha~SLS+vnnk~`1A3IAYT<Ba
zSp1f&lR#CtmRH<js(I>rCB69TI<s#t1RG+YrNr`lcew^7)|8`FXuhV{o<@`s7i{Cu
zHOtIB?Jz!;4f>Vu7?)2oUZr{+0Yb@Cw_wF~s~=L<JBCT23^UEsje!A;;GMz_Pi4~w
zr*`wH=UG3{fa9F8dT54XBd10fa_#I>Hv?3B!~6xUjPByNy8%vV6TD)>N--{@)$ES%
znqR|C?Om_ha{CdG%`RzfPu{(&SGXWe<TkRi^M(JR>ClsGU<}!DMAeZ;wPvGS>r+E?
z=1YiFl@`oP-^HR-Z)hA47TsAmKR;2%BPx>M(4<l}YMA~-*2U9|6^n)+P<-X8&c4Pe
zEQ(3=y+nur$7gkUnV*6%aunxV4L*V2FaHko1s1mk)b$fszSXCd;-I9~Az1AOMhpkH
z`5ZD$D3NmVZ8C~tAQolb=ylB3ujRmoStebyair0m??H$oa^7-%PUe6mdU3q<RhIK*
zq2We@ySsxj+F|Bb_sfjd9Ve9GQH+G>S0JuN)T?)pzhU)qo@CWK6V9PM_&o9i*yfKE
zOG--GF0V+Vlkz_jsdJVcO1sE(+JCQA;Zpjc5MS8$`<hgW&Mg&grm9fHUZ$e3RZ0qD
zZRn)LeO=?Y{eTxPfohY7>l~o(VB!^NNfck?sps2V9vMLg_9On~uW#f33H`Ft8m$>~
ze@U$l-6MZo>Q(XMBgVH=Q_pW)2!20XyvAzFH$<#l3_U9HqHK`o=2>t=WOE_1jQp`-
z|FwQ18JXD)>()-wx7hnJdF1z<<q<bU0?PAIJ8Jr0%&3HQXzKaGQGt%l5+(0>#W^Nx
z+RD2rbt!5QLVuVQ$(w0>`g;5sUXyR8?J--1)y20PDoS9Hq4EuvKAn#8gsFA4SjsF(
ziyJe+m!*3MDc}jh+uyG!=G4ou4~n%34IC}PQ<WXsrZ6j)c7FzQwK99+n<+9&&^P6&
z7bd~yQ<xJnazK370j6asbW<rs)c#qveB3)&%(Y&&d~84VrykcuehrxJ9$3@y$!Dr1
zBS^<bMPYlsDeb)#<Wb^>eTq|rnlH-bzKz{5ze*(vR4LLb#YMt|jHPuny2E#5@|iNy
z(ju=K+<Z4*Va;X7rc-a^MSPVPy4+;)QJt4<?VGF}<U*}+KQNEUMwtZl#~?s$GfnVa
zbCL$W@BR>phxe4txS7N}Cbc4c@srx}quykqg^zvZ<JJw^w8rh<aGl<d4>BAv{v>_g
zw4Ih%5FJe%@H1glF1*(NX9t<wyS|>DW4Vp1lO9CM<|jvJ%#n|1#ymVvNz!}e?BsDj
zOHs-y8b7tWA`<s`exX=vh<RtB3C*$<6-3p{%)%)uS{|r*dY;;|G!`l<@9>P?mS}G`
zNZ9M19v2d<TKgTl7w@SxSL=7Y6k$hHkTICR5DH~=TLfF;w~g2JvAPWoD6&xy>7RAY
zSNROY+p&G1uDbI~pk-ep9i$|3l|B+T%;!?t@a@k!w{(A_=9)t*QWRTawc8wwpizk1
zx#>&^&dRsFj^Z0H@2+I7T=4RtA3b-(|1>qEwF+sFC86!P2@r%4pb|0%GpOa5Fd&#{
z^z@z~{9{}EKz;z;{Ms6CeYt70qhP9qPb*W(U(gGccxG~^$WStn@%|30hN$U;z>cil
zn^mKv^&}lRKX;bSMLNMpWGRh~SESvFM0A3`)_eNvN_5cp_j@BP)A1fZ<NXht@*2`l
zMN5a?8;QB+QrCkLQEZz5z{c<&hjJV{UmG$i48{LKEBwPKpko4*cv3{L;{EFtCI0_;
zbj3em-oGFn{}aR!F7&=Tf`seYe4S%EsAK}~$W@?oyaenJ(9eni)+q6Jz%=Uby}QHV
z0{PYH=43!I$asa-)bK;|Q`P~oMFK=u9M%iMKY#utwot3p8%kcf8t(G+@E8WvMBN&j
zR;}c3fDrLu{Ol(5+4{AV)YGQ>YYRY@Q?iDZr2~6-vsunf0MQwr<!8L5(5xbO>TFzw
z)(51NICB8zaxIXWknpRTXBr93V7ARpXtZqm-qA_yz@K-b#7Gjv#T~%$x@hxePO0X!
zr`OY)DZM#owNJ@2mf&+hVPH@Y6g253?u_V6W8M|Npb#L^s<I>j?Y5!7k{>$IJmiCb
zG+b$+=>hH(v-!G8UQ&(s-~kR+V$^;F#=!JbcwI7pmo5Xxrw+iS^#jD3_P)M87rNNl
z;|3IGLW_xlkt)N8uV1l2AU>_t+dY^nG?FEY1yo<KnwtZVX6eDT5(8E^ZNOqD2o#x?
zK_^7l#PF`z<&s~;Nk7-n*7>bPQ(A=VKPRr1DTUH-y^~LJP-&X(slithhzYa<qq(L#
z4tP(MVR#2O1rwUpOz~_6U0G40p#W5j^&nK(yv>f+mjE`yz+(Gx!*LgxzyjtS8@0rs
zU&fbAACQM22421}i{i!{ceg3Dwlo}GxxbgISPyV#f$Jqk;5~hx1_r_e*Vgyz=vclc
z7U+xq6AM*UjV|R)-Jh+d2G-{fmdK^Fv7@=lt5=)plkf+W(!9=eDOA=<420S(VAeRU
zE0bkuCIY^s;%wsYlIRx?XLe)rQAgZ{PdbdS^DC7k=bQ!LD)Vj(r56j$J=kXH1@SAq
zeTz-2oK^scA1hm);xz;{H%-ob+J(h)A@x@0z;n3kV7{KibPzKC+gfmQk2sarDv33X
zR!Jn}Z^PH}b^U7Hp=KcAE`c*2i?@Cp<vo+~gio8!cn(qAo^B3VELWHgQJ_<b>K2<W
z?*hY>?K_;&P0(r;0V?F?t2qjF;Ls8T>`mSye_;U*7{5eN!K@Yp;@*!1ZvuStB!e)C
zxy`;hEz1JN$@v^zJKB`X&)wujS<RaDi-5qum3ikyX+W|ct-1o31J2s+X=1OH5IW0y
z8H>#bes_rP60+0FVJ9t>64PM_{K=mxVvY7R3A_sB1@CX)fOOBvlJ<tG(qgg;pk0eE
z`eeR5pF=761}u1W<^@_yjPb$~rYg*1st7F3zy5AAUbawRUaT%yoT)I+N<Y85xtKhU
zbKT4=KY!!IK_2s)g44*NG3)xx5yW!-B96aq1OGkXr*{NVdL__yxUkt@B2QXRm53LY
zOKa>f;SJ732@{^ee|~(2Qd*%Noytoxn*e#=E;tIYwzi%ZnE9nby*FP!sg)d>wP-9T
z(+58Xlb|3lN59_b4W&>ukBCm$HMwQGJ1)pl;p*$^@6{jFp#c#`u0Ah^<IcNzvg6Ys
zG3e1!Yh94{{!C@vMG%HG{H+Z-rnNBX7T{k&Jva#tqA+AaS$T=C*p~)OYf}Y)U*DMi
z(@-8A5x&QdA3x;O9C~7z;+tx{FPD*o?=M%co!QZ2MCcJw@rM^>jpSVHo6je7w3@vd
z;WF&pW#F2+ob=Eg_pvo^Ss_zuLHx|FBbZA|OG&)4Do;1|Y>|D-D=eo~7pvoz89lzH
z3)nvc>|~yapKYO_>cLW%X^i}nMe8xgnjH`fm=mNucj5H7F9SwdyZK}mwETmKLwBN{
z*IQ|4z|K#gASlIW=nl9`7`J?RYz`gQ!m^yJQE7InYN{RQS8GN|^DDerfS4A!0)6*c
zV~^7!==D1joWR1jLx-6n1yk!576+GKP|tp?Fmh7*6RGVzBII<or2-A>|4{u#1Jp1e
zq8?gdYJF7PV59B;2#n)4G&W)j-Ex*bI8TtoTQlu6p~xT+hb`=b+D(f6?(RY75{$#w
zz+4|UG#h+;Hw~3Euhzvl0G6=Cs#o_zvsi!ThR1eAYM$CyUZO2DtOt)javw0ldhywA
z$ALZ7H(832S+C<xqM1uYf>a(CNqxX2Az*y>3JB*SD&ZVF`U0O1fV6e2krY1}z5VBL
zlAEF;$IKp;=T=70nUe>0$>s{cbzV<tZ?SG}zG?mz(A9>mFlt@lw2&f#DQ-ik@xH;C
zxn13bQnT<q0qEZ40s}ux3RgX`zrVi+aD@`0c>O0DCA7s=&s`V+R7I=A(8p_n_wyXT
zA6*>)xm^MjxPwTCzQcrh1p_KG?^&5V;9DIiexUy5*zuh~%>f#Aj<kzxx<ls9e$u(t
zQ_ieAbfMQd4eki^z4;5;l*WyQV&b}<(gDlr<NLd{809mO0bAoibT-?{COi04!rMq=
zf2WQ`d7+#ynJqtu%cPxVKcosdPrydXO_ld!S4HVxXb-&?6-v2dp=F+(w3ICKwDdZ-
zB!{53!g+=aoSOZVu$3kJQ^*WIw#pB804Z>#I3U7ly0<C5?LU$+Rc;08+3?C&F|=<u
ziCM+kKce#J=sya~WS9C<WMK-+278ZkR6O*SlTwD(MfXhpE#wdTluLG4A_43CutR=1
zQ5c_KOy1GuV46t2JJ4STt`7gw(%Qbcy4p_51MP7ni(hd}qa?9Y0<iSqE29UBV3xlv
z=knLL&{=&%d|-h!LueIFfa`9L*s4}SYh#9FIajjW_AISduf(=Pi4QMllVd+*Xsw{F
z>sxJ*&pDb^v?JOs|HF7{DSS3!d>Bm{q4VUU!<8LR>$X^n!JF8dWhZ>=GpA$y)y6&a
zq6y7G3bN|iW*}EEO`64+O_(cYDVwETb>{F-+DubI!_3aG$u#`PzYE+D{|Gx4&BT4Y
zNm`W7{c=?cHE}x#+vHYG6P~`uRF?GD#cWAV(=Rjb3?;Gnvn=TLgqYOaH*mFpm`i+;
z4O(apDT2q9Wv;#HCreI~tc%`e`CyIdUQ}aHH!2XB0k+z@ASiIDcXJ<2KqSRv(hF=W
zx#(&b{=r~E{)TU9iyz>!h);|uirNNo7YuZ*!<aOSh>Hs(XEP&4XoXqkBrdI?Z<dY(
z>K%7A$Yr#%<q4pD@5)rM1Q4Ry%Zz#pdM(XLsWucnp;#6@EQ$fU<c<?s?c@(bv-B`D
z;}(M-G|u!-ML%jNM&j5bYVI(ZMh%P=Ram;K@(ZeYf6RT;g{kbe>LLu0l`}<<WCT%0
zVJA6krc~51$clqIb~a&<yq%HcbB~3@y#*Y=E+kPfF(+b|f2-v>pqH?0X_%DZ1O|=-
zC+L*{xzfxkY?y$Q*<FEz(9kzlz`a;&<PSE9T7Mcksn5E@GYDFD8fjmf4QY&u_R7N|
z$iz;#D6AVGV!LO)>ZUMN14V2vA$E_D4oqQh2&<Aw2GK@qQp<9A+^Xn)0Nx_wl>KMi
zsKE)o>ZF1(>$YYEr$cwsVu3@2(HgGETF%21=V#yq@w6K$%}wELi1wLpWUPttAhx$8
zh-?9ns8J~=%dm5fsIVcFP~|}NlWV(1fxiSd1XQLi+^ZWRm}hyVrZ1SDK701b_M_cR
zQoEz$&R9<sS-eO}gY9<N#v^5m5}aPLRH(bzJAC^TwlD;vX|mR5j7XJ#%mjXo<9Z=s
zJnS@+9DncwxShJ*m*biY5zb#0t?Ym^<w|H%pCapCc%$|!pb|Blin^g%z$)nKLmR%M
zVuN9C0<4QhGQ-?rqRsZHcuCa9d3i0Q5(;iC_X^F$8Zy~)Pp2KeRxaE1B(=*2)fnh_
z9+e32uI)b`T#@%&-z&8hGM<J5P^~^~*Iq@B%N}~)J$%+Wf`=qSFP=e728~swx~9EK
zaWir`R#+pg9@O))17hwc_b$ZP)~3{2)0`H%H$=R{Gd}*3)i9iRo##pFdnG!J))Foe
z>86(w0L9uGq~!aoOG}+S(w`=xgT<e%libx^kO&#>eaXC0I){F~*52`ytcV*o@I}H>
z0;0XM$ZC#xL%+!fQ988dcoKe+EUVqt<%esY&(0hWOE^l;Wf}dXpY-gBFbF{f3&OU;
z-&`=dTWTlTvY*{uEEbDAsra}s<omn^Q!S&s)YO+svj8d4!56-%=()<8Qg~SYu(O3Q
zO{N&R_p4q_L8HtMDgW9f?t)^^b?-DZOE&bi7V~UcL-lqlSUzVyR2WrB3XBLM7G@`8
z7ao(e9EU*c)>vm(YK_eph1|C)mTCm>{_@;3t_DCPa}!))-vbvLj%Yu&N5mqF=m6L&
zg&Q3C#8YX`3{zdy9d-Lf)jA?I{+quyi+>$R*Nxf(x06GD>n5@Ub#?6N4Pu5?A|XZF
zEs&9-mg5~KkR-K{C6^9BOj}T*7}gYLPd7|%`Q4*xSj2{`2HLp<WPD=jSDOT^p6KoM
z4|XU@DK1G+^`>D3T9(25w7qJ??v+erSE^E&x?gIr3?-MtsE~`=+#@lh7V;aE_h|%|
zJo&wDT~a&zhaSy{r6VY|v@8fO40#O5C;81HirbLWzA}_R<~^fdx=iXp))Ff<olm^(
z(&N`ztXFMDaeg+aZy)+>feW9_a@5w6ICsJ@we{R%Q9GruI7NiFifOv(!)%OqEr!U)
z6jq;6(y13hR-6=MLc%CQB8jch>Dg~o);LE{Q;BKao#U;Jbve#rpbIXT17fEJ<%>f4
z-75S?6802iea7+*Eqj8B_5J#2@V0VOU%rfw=vE*zcn%@t790$=W{V*j{h|7{6v>y5
z3*)!tbQv&UF4ckg9491D1@o~my@vl9HbiF1*VK2FzW9s_pHNI-7yq06t=y+KjTpXg
z^vtxF0V6U(Bh<izY~_n)r25jp5WguVgf}4J*xBZY9jQk2w4ck3LO4`Dr#U+#;n|>0
z8XtU#-yVd<j5%M?B*+}NX?+{xD_+l_h#;KVor-RVbieClYr4#Aq7z7u<q9EsD&CEp
zez-UF!7rLT+42jAkFPl@rRgr2^RlJqq2(RzoB#-Q{r8!j9<lEYkF~;VT`gBj2VfVe
z7^M!`U|;uI^V;J7aS<|Gg0K#rl>Y3$)#0Dztw6L&gmp!#v{{y0Le=Ds>#x#!7D9w9
z!!|=Rza34t+M5r0_jomapV7F`4W)S0onTuB9Z*|F^Zm**h0v8oC2BKeAxlin>a-zn
zG~Hdo6ginO1(l|o(+^IaPh;N;iQsPwObzEaxBLf_$5G9t#z`-mc^Y}~P;&V|Ao}VJ
zUUO*hKeWtcClE9I-_SL6mJ|4CtBt=M+XCv4fZzX*=KOyS8kKoQwYk3ju>tpKE4fxV
z!vm6!?Z0`5t9fri9_obuN*VzW;mft3&eDLpDWZe>zo?PgrP<t6bb<8XI@DSL5U9l9
ze;_4`rZjRm?EY!kAQi_hdt%l7K{*a!4F4CA(<BO@7YV_W7EpDPfLOOn2&}nJtR8rc
zW!&-)$M&DB#-TyTG64ZWZ00LFJI?6nXlydFu%jaz|A2sHAZauY?6CsdS7guD?cJS9
zorC$>P&y^BdXzLYeD2}lQDKYwfLav%k8=%A6`<bw;}1cLh<-*4n1NecW_R(s{ShkY
zYS@1`_&A$#mJtyVsrdP|{$~3mk!3v~AtS-tzX7B*#-<yyGcVqB<J^BdL|x_9zMT2<
zsPj1@6}bZSDyO@uho`6GvPGZO1HzH#KQNI5LgjN4r0=_r1A2xpC&!YFo4|nIB#=>r
zxejG-7lVKB0qfyV{{>35?HYu9qg@NGeH@B|Dlx%9lD%Ci08V0w;y=DJ4Ui|>&k(>(
z@vm3ge$@D|{ORO|H==k+#7E`O$_0XlucY{&Xa7R23{R~z+?m&?mKpi)M`nzH5B?h@
zLCn=0(*$AQIQYB%1S&ewJisd3|L0F12L=_r_rY-~1_XA8GbsA>q@wyRkycRU7@ELY
z?b~1e?<0&$O|%@KFefzSl*@@)|KMO84wjIAp;~*26#vgNV*}ax6zeUD9o?Qb&Qq%0
z;XpHm3Wl}?Lc$mS3!TZl->)qm-%2gpt|%wR{(y@mfB2}qB>uyEefadh!Ko8_{Wv7I
zOSeB>qoDeJ6){to2v>OkhX(!k6`d;k2Puo<yX5tM0i{(WXvcqgVK@}`a8X76o6D)J
zVfk7W{XamziCtiYTM{jp{tmwWKS83l<2nBZNj_Yv{}-@=^+5`T0re#f4V9=Ug^G#_
zy=GC$ag}Lwro4wSXC_`WPJZutG73K#VOZZclp4~+U-D0iq-mZjSwSP4i;3hnFkW>^
z_|&~)BMooxS?JzBM4+^0gEEQRL~@x9KG!VLJ-WCEs&@XA{@Tv&gdA$>Ya{eu>x6r-
zf}GR`Md24&t2P_P^iB|gAjw>|<&G;0es>y329Ao{yG_S~8_D6O*F39gZQh9gusD6<
z<KtQ3HCg^C*CaLA6hR2_S+-m9=#lyn;@uJaQk`_!c|u59pK-S~BR)y{4*#?SbsAni
zMj}j3fCq-jb^epB|JO~dO~=83LdWKBe0Y31<)|2&wOM-DfvAfaF}68ewokHj7=}oR
zfk$;gxhBT4_QE)(Wa};cy#P*RxsmM*S-cN3@S|DSc^St>{Ke}WHOs#8)=e85LPe1N
zUn@j#YOPY{Cr#@qcT%qS!wxvLsqOWO<6I31T2<{nmBc}_f`-3t9*-OUa%l*0^9H18
z*6ua1ZDf|#<$bLo<9y=A+%)fik~}0uYVq$(J{37?Wg(IzAjL7{d4&B&<2Bm+W{(^Z
zY)ilLv{hYLzk5Dhq3v>fvvm8NKM*C!vU*c~@1gu6AAq1zDTkd$FrXE>0q3f!P;yax
z5l3F@vvv6{L(V7u>xbr-w2Rs@eT#U>36|Abks=k(s!QS8r_l~UsY3s31_z3n#;c`Q
zEhNI%_ts{An|rhxR}(1kO!KYel@-#K9(`wtqpW1YZ`Z5r&2YSvH=8WsL|yN*vZO(V
zA8!rISDofs)&)<y7lG&4(h9Oj$6f7u>^yoLqJpQX>3s9CUCF4~Wb7uc$e8+}yc1e<
z)<3YCyh~7ldy3)J2zyEmA+!OSn$+n#^85W`XixdFz#Vka5z=UU!)Kp7L*r{}J>dJB
zso)jI<$B#|u(D#KeB5N{(Vw$?9b)w{$J2K&@udNt`roVSp8d5kea_&bzd`(d%XFU+
zNV?dVDQ#z74VQc}AIsBv5-U$2Lqf9_O4Og<p;JQW^w_KE=(v{p$;MNn9(l8DS+v^I
z<zpA~``4)fe{k(>Umc%&?5ft}8aJV@45a97zBvBltA^R=)_T8iY5Ph&72`C~6^28R
z{GlPM<dPY~$*#Hgm;TwHT(rdU+y%_9#(3Q$d5OWLQP2FWK%L$(fItI+)?TlmOx<ZS
zr!BO;j{0=v?(@1z?b{!vU8i3|m9g#8K*uuJ`2ZFkfsF5SM;wce%}NIf01Zk8$*%Xr
zAc2IZ6mYQ|&Xh*uHXD8fij171iKi)I-V`?%hm){s`%NW?m=^@xog>Yu7+*koAp<1y
z%;suJMXLlIcYc_SWOn7wqYWWN1uWXqwK|P}B#!Og6bA$V=s+J-7pUoD6A|SeRsw-;
z7J#Y#(yQlX)GP{5;<0#P(B#pTYfmxm)!yWJQ{s8!r00?Xn0D80M^i4|yp~gM7ZE_Y
zM6%VlN=S>G>O9eE;!q%fs3|4LFPDv)S9C-x87I;`t9pN#_08~484~<%vT%>uC(E0=
zP)jQ0zv5IE7p-qS8kaLM+siw2A;U~?nVUcKv1~1%1mbm{SW9QK;#N@F#hZPKWGHbM
z%I_dRYR#%@+@V!m`u3%?-Hp-vzj@twHm1ZoM(4>vL!u^{dmCscou(p8Gx)`~dq>l<
z=T}^*Y!>yq8d=Zv>vAgy2h&2|c>sdvnty7k=<QB^@zFmiNJx9H6UcccaGT=-Tio4}
zlL6F%{9=RVNGi!dLtw?;^?s9`e9<2T&w+X|6$U$<FVYh_JD4XM61hkzo$wR}Y8pAY
z?l~VVP~-_G<5S{unR{(){6eEZ%ZJFo0|#_X159xstQC3(62NC7iFg-}mhXT^W%Kew
z?SUL^@T8Zi*l_HZ3NpGwqu;xo@_wnV4x?U!WcHKf0697C-7&tU3UycCyOA<|nsASr
z7($x`&e&MPpaQTXTG5mrdoywq+P;p2K}SAYeoXpVGUdY9RutdkpB_b2H$ndcHgi{Z
znpZj@c*`63?%9=V5q9ZXpCi#;DF2@dOA4BVJ7651Qil_^irJ>(I!NE-C#=;0w!Fjw
zfc13Top5~JffHKrP}*Wc?nEc6IrwmKFb}L_xa8wm_W_S5XO&4UC!A3CY^gSxZ^1<}
z4ACoIXxaAf?decrFJOygbovWjf#l>{kOKyuf$cOnvh*cQR~uyC=~X1sS0AXzhc!_k
zaczz@3+g~S(ZST<uQ+<+*`g-(-7z>g;JZexqC~~gOQ{TD9Bwmz6kkN{H)kT+K@DLt
zmv?s=m#gg=HTfFaIJOdzOSKY8>iCKY%Zi@1JdEN*7VN9*J9x{%;eB(-#+Z(VeyX)2
zMRs1PXHxFp_gVzD@|ZYQ+=GY@*1KIGb;f`BZkRIqUUK5sRcX00qldhPv%POMG~i~4
z(Gf#O+%#NC?JKqA$0Z5STjnbrrLTYS*;rZXWu&z9YTYNTx@uP%WM{(BoHvpnvEqoV
zr-6coHo71js0-|%0Kv_XSQPfmWp|>m)NWlqKaD}t5Wx%Bguh4kxI3Q=j&Ov8tT3Ys
z+=5}NeA|6syYPuPbEd)7-g6JXb{Z#ZWMt$TQf)T!B;6C9*c3P&;Cp_t=q(E_MXB@Q
z0yEkakl%kQ?Y&#`AHHR?5jdkwyR$5Ucg=@Ka!uq&9HPX;dEE8hU9Ma$m@U@GCn1dz
z9;1Zt+iySPJrIg$=7V(Xsn^?XyZIj%0R}x(lIbe^&ST$|S(x}!K~L}%HGj*bPSk8&
zuIk3r`I}tp6+Z&1V9k{_ICK`RGHkO=D7E*w_g5@Y9rwp*a0AU<7Ex%`+u{$b;d^fh
zJ5Q#D{I?lA%C}#5EqHZmm0@!EdQBAyGfojtc7;sKvI<66nOf%*u@A<tyIO0oVu|i+
zkF0L)_HDoYUjD6g%8Lb^iB+$Ckc<x}CK{QZBQ>*$QMx`ut$I~^O#ps!A|vW;z|HiP
zk{p#tYW3DtW%zKgMl9ej>p>Y8Z#0S9mXeDwC~UTZ%9d4uEe3`6Vv}d7$CVwBk+>eU
zA|1D5i9LVbay^V-x^!h?M$?!`djP<oKq}65bbih=@qt|5z{0m25G(mXY2vo*7y`Wk
zKy-oY8B^JY=`2u!OAvA{qR>oKud<pAbU&z-4g2WaIy?p}yv(sOsb5xb5`@;=EZ*WI
z!_z0oe{Hb8?iuyeF&-;nMaDcUC&7i?giBa$$7SU~x+Wy4Kmee+$t9hxCN^%8$^}=c
zu+Sa9^glA7V#<gG-Jb{X<eCmfN!|*~lV^oG#MDUylgr;WI_PMH(wvo2I~pD@Tz|0J
z@safs3bmMNH`VJd#qcypZF4D7?-?VYz!XWhW)_ZMp$dFd{<zU>)&1R<W;bMQX!-Rm
z64M}H()dG2Hv3NAdpa4=aBCV5j`sWxJ=_~0R-CiP%^xz5ESo7L{M=Cd#@n+-2}Mv)
zX5Yo1r1|gL#6QFuIWJFsqsn0m<2X-|R71;9Y0+%?z}C|BCtE?K*~=3eVLzNT4#Xkz
zUxv)-u|Z)n_{Wa|nvF8rHRP!DwBLKk#L;RJkyh!TM6T)nYmVV-LF)%<GG5E>KLR;H
zCFY|!f>)9C%Pu{js1J5S(k{~7;I9aS1{g2}y!(y@Z#dQKJ12YofEEkCS*YAaJ@!_5
zt!Vb-ep@B-9=``+GxKaDk5Cjfry!4fR~dph?bE-9(F9cSpCTww+b^G31wT{Z#~e&D
z%-S;a#jK}cKmQQDzxje3Rm|-X8Iy{AZpoo>1x3_WFe?x048CoO-O|)Jv45x5(AI}c
z8&<I~rf+>(xcx?li@vMzHh&uiv^?c`_+wk(^71N(zrDw;dYai;xZWrHma%Gaz4j8e
zC$!@I+}@PTInsEv=%1Q!*2Pb~qJ>qmtn=$_MnzYF$ksZkpaXrLx_zN|*HJYnL-x_&
zz@WwxQYH)q`i*r;H7m?uamBhm+g^3bcx80af`GN97*$OTT4lN}k5~0@ZSjrqH;ZU1
zj1pKnA>|Quy`U<#Ggi5LT!PcRf?jJ6xXH`yDrIY_Biku4Qk@;p+aV^$t*qMMRr0rm
zx;Tn18Ek>`v;BF&LS6@qqIXsp&myl;?iR05PAAD<Nxe%c8fB4Ad0rC=!Y5Qjgi15J
zz&<`an<drnrQdn@t1S8wlY^B?#L|jheWgr_JNAgeU8dgXI49PPs$y&Lc!&5Wvtd6F
zA=g~XnNt@{(0$l`rzeroKKd$%&3JDILGJZWIR&E6`a3C~Mog*(g{0NDK%b<E=ju{Q
zxhiRcCl;oR$&jf3ED|r~)R8@GDn*Dq$1<0%m(UWz)p;9l;1vQ|i?<8D;d=^KgYqq%
zpwnIg!@97TBo<}+gy`*&WPAfVew=mVd0hLkBYt#>@zODjauS;3qNpgXd<=VcrRXoN
ztN?JnDe^+ipp)FQ;2`j3e>^{)LdZqv7_@dQPn)JY^2bGqte){Uo(>ARDNYY0^VwdV
zeDw6vP7sN{c;)j15rQhDWN-{RB5Jt3zAj#jDvOaQBrbcln6tU;gWd*g9GncaLp54|
zBdubdDEgQab}l+DU<1H_CyXPiOgEqxMM-ESt?1EZF<Ihf-cQQ)ROFy2(VWp|s^+O;
zTJ__`#e~4p(RgT|&sW9egpH&7WtN|ZFGiewV_Y1<YHe5zzV})SNoERgr)|%c5%Jp7
zFg`YIU-_;=#$-ACQ};lTDkTk7#7sx>Mdd{o#l-}xe}hwwc5v{Uj#3MFqnjf7XqCJd
z^fmh?uu2Fw@MKr7+0N7;^=6q=?~6gp7n}<?Cr^Uvp9a#e?B-6+`W<2}loF}r;V^uN
znEh?)2w0+KV?U@!C-S4bCki+wP$#AUv-Oyf%TlWmG<g7XHr|%)2rhB!S@3H<mz*6Y
zPmV7ihuxYS%P-yY4jj}j&?d0}_K8kJyu7z1Bl#W7VH1WYk%?TUJNv{ETgbE<hO3M7
z?BC=-;jwHjq8BCMX#IC?8fTp4Xn{7Dh!|PS;?eD~TT&Pg>Pt5W5ooVIe4GP!<|b$Z
zQEnx@1ECU;5gXM2D61E=D;Rj5kE5sYEaW2~VRk(9eRQcQX}3Z*6FRFGy>$Mv>f^PF
zdK9tExnNr>Yy1D2I#a9=j(21vfbkUze-R{5_)x>9MPTE56a>2sC2G1vKvLF|3CDGe
zJK}<9!d|y9eP<3%W3gE7M4C0+1ESnYT@LU}T@odWCO|5lE*KC&150V32vbw)Cosj}
z9l<n+Y&AkkBx6_#<Y}}=KdQqmzG0oY7_%u*PZzABdqn5Gk2Mb6-)sGi7P=9=MmCX9
z0jK{VVw)I{te*>bCCI9@B8`p+9*#%&JIq>PdKt@?+2)>Kau<9O6cm-Zu&9axUX?~e
z4>=`cwD?U{n!7=N4~Kc16qJE{)YZ1i=AN<b)U0WNU@Ih9@LXs<X~|2}#k=4*(hwt*
zTjwouX+}Rbw@wL#F+$R6K^(JoCYM4ag~&|yunZo=LW;_?SnbQQE4~c1O!%<=bJ?Ry
zejNTz1ia#@<cgGq77l3Fn%pNhrGkM{7h$+J-b{ximWV$n6_QwRw!2P<UM?YcvM@w5
zr0k0qYk|1Ck}h_234(G7iiCpXpL@5TS+)Z2;*M#w?4KoT-x2m8KYnc{NQbH(w+X;}
zV&aeBW73IT9kEPtlu+5TSAKaU`ysQ1Q%+C)7H+hovZ0NiUjJyO+|gk4X-y?H`lH}k
zdOW*nvotx$ap5O>JTiK*a)op#PEzSDZu2ZE%E&^CkqGsvQ2Z|+BhOQ~_K@8+vfIxD
zuf_Mq1oofs(PS1{gy?KL>eO(8BdZUyjbw&YU9B`7gq@k9rqd4tQ?=iG|7y?s4&L1H
zdsk)3<5pf~Ip=SlF*0{df`wkg^Fi58!-J48WVw?zS$E|ha$hJJkq~n{7xcTkE=&hs
z{CQmrfsKz9s*_dbO}^SE<r&p)zVFRv4Z~6nz&N;&jS|0L)#D1_C{xkRHDU4moWf$=
z#G&f#G;|<hzMI6;s8DPlH^%I<O*EZYs{8jNaij@aD5Yt+fGRsjM-d3MyGSRO8l^^D
z4(0@)Z%nqtGmh4@Y6@D`sL`iCv@Z~%|LsF5h6p(B;MX~9XSQqqW!WP5i8MfnFTLn_
zk(w3=B$B45q$3Eu+jN$#Ulj+zjagHQm`;VhT7k&rSNEjdQpj=V=b?Hl2aZbVrB|y7
zL?Fg?VQJ}=cR1++L29!4kPWLq`dn04l~IRmd5P=aL`8B=4??~o{b!USj%+#2BFJj`
z-j5{R=K_)lLM?_M7c`SHHUv?{%YJvB!?Qko-6?+YS!cpFMQxU`3Rpsf)L1;MpP1)K
zZatUk+TZSL5S-2^+DT}nBFp~r<8Y!meYW+wW&fr2?NHhB<+#JLOhk60wTbcIUL)5s
z2R#l1nP&E^PWAD9opB95dAewv(p{yaO3b@Xd*MG#lZhfTp6=&eB~(#6Y3{7$ZN^!C
z3j#SOMFzR7ajg2r9xH?E>f=ZE$3c{5hssKW%wK93#*IBVJyT;T^xR9RuJ3Q4j#)lw
zKdqaSKQ&Nmwd=QsT)+!g_NL3-NFV92t9xkBKB@NC7k0zkZ)G1zAvGW&CC$StW3M<|
z{IUYmm&J=o|KLoDx}YWuga5f!`)Y3qb^0pfJy^E8C2(71Ot*sjXO>NLr}p~K7hdAw
zQYsJ6KeHld9d~6PkQVNps3cfTDX`wNTeB}vj$nj|k^9iOSHGI?rCDa1&P3$1=?j1~
za`j(#uMt_!8AK?y5U8_Ccj&MXS>e;Nh44t6C&l-L>3R*`eP`OSPad37&a#fk(uCTK
zw*FiW@V$F09A!0x58Jk7<+=ZSZ*uRXBzHjnIGE*9K<UGv)JtK?iNR6ec<RRK;1!kj
zYW}i-3(^FM==%moByP-5M@5>!H@r3X-nVCd#zcrZ)?-o7y4v)^tFQKmzrJ(kJmZlV
z<-uin18X%?{)OJylYV^BMAnr*e{<Z{za!T>;mxwmx2#YM9P0$hTvl39-;&s&aM*@)
zRdvA|*m+rdi#ORwiwjQA*Lp~BIo`6fhb7l;2KgQ&=B9TEjPp0f80}c$g@g!~CK^He
z8WNy~jeElY3J(#s1AT0V64QV^Ug^sNIM~pJUUWnZF1ZENy*KaycRv)i6qIa-Kt`Yt
zyPH0lP~Dn3egBb2DPiCx1pT}|mR&nU<ndKEb<|QRjDjn!3<<A)xZHdfQXcaqRUdAs
z^)kJ%YKi}K;410?oxaFWbHX`RQDAnu!JyS_jyTP2u|Yoii^x0cFh26OUx!4iClTS~
z&+)z9Sj3H=wlxa=VnN;L%Pz{d-&Ys63Dn_?f&`F}^_hO|Ft-(dY)?(A!jl$*lfR&#
z8lj-nZS^|zen?Q-?#jreeC3xuH}bbUocrmd)5}dMH@5}FbS{lyY)Z?w#FEK`qtN;k
zAN^3%zx*M7@ysFw+c22OyiPuLnx)DUal*{#<pfw!=T@l+Y<$sT=R!7baRAEj%YRt!
z<LM92*xPO>opvw^s!?bXm>jl(+#2bVyDA~?J38nRFDP;;5{un}pXRMHV6XY%x+1(S
zHGK35e<%PI@d@PqUh}VL1V-*&t}uXE@Cn&uI_!!lK;kdw(&=KSxUGK>n+{yA?yd&F
zl44ty0;SdcXdF`ok_V-h!n|2OTzcl5e#WHpw9b)gMEMjOfK&{<hBFDS{;C{GAf7Eu
z!NhT-J4kTeG_ibgaN~f1tM*;)?IwfOAiuZuJI3#5LF(Ddg_C5bUB4&w^P+$03A5!m
zSboW!IOrU-Se9Fx+#Skm+dbF~=ymn8eZ}+ZmRZS7-&?GpxgHp&eXE($8Y|S36=Rcw
zDWx7w6Q%DKc%b7McaPn9Uhh2H+^e;JJGb?1q3$D^D6x=+L5;BM=}NAFmlAm&&qq|u
zz1d<fKddBfD_@vQcsIKbJJ<Z1R89i-@6V6naPfV?N)E%NSM?FrZKgB*izdeBdFnG3
zTA&mre6)mMHe5m#2@wsS)OS_4jt9uJ+uPmZ>+{kq+2dXXw%y6v)3q)(E|V?x^24T7
zUxj$KU4i_-C>KS(VG2@yIlj7WX^w2fO*geNpUDLmgaI9H*g!CHV@fDdk#3_eE4k42
z>ZwZeX}Y7uHzxim7Xv*)F)>e8=l!{m8vyv*-BO=zv@X8yj~dM}x~n^{yQsFymM88V
zTh8Xp#q{0|AhKDxM7g_eT4+4l8qT=p5&4~CanZAxzTF?%z{@ozfQ*GUxj0_v?(DEV
zF`iNK0rXTg-}<h?BJ=+b>dvaGtu|cOr9h#$yK8ZGD-?<rcc)l!cXzkqUfc=p?(XjH
z?h;_lZ>@v<8+MKu8A-+<Gjrz6^WOJ$?LN$t8V3-&OKjc($Q55e|8N8dKP^HIbtUAW
zL|hU6Qv+8{nj0hAov<}e7u%8F5i@`4HaYCYt+6l#qP&YrP)q(LV4lLTSqN?^qc{~;
zE8M@s^zjQ@1dbRa7Z|S?76c#tr+*Ns25l1@`=KTq>)rMUx;J|zdygq=U98=_)>&fK
z*o}o60`bt3s{%&W&xwOqD-|b6Eo?JZfS$#9@TEQfDiCe8p*IF<nY=6Uqn#Hip6gg2
zzze-=x5~+3cG*gz7liNi=O@4946fHZzK-{8>|!%NwP)LHDGx9&Y5TpqR(V0TUE9Ja
z)9-ukrCe=N(%la27suwon9m-kt9!G{L~y(3{zm)j2+0TOthuz1wh8`~S&G@k#vo0m
z9)@vYbJ1au(sKM4i<e@yXiOCtPQd^swSq%>hhS+gbmptWUe#!dw<E9$WZK}YQW%c|
zu-|d@wjjMkWUu70En|2#_9zDNsGDs6M;VZXIAjEZ9|acgIv7j5#AvRQj<pNp-h-d1
zFjDXEHgrgth>G=~psiq;y%(o61|4hAwU|fuwg$vl%FXQ3iJJACb-P{CnKWRXcI&UH
z8(-d(E<pY_&}<%iuE<_ZC-m=vv38bu&l?im`a)xF`N18;GV*KDO4&Lk7eeSs+2jQ`
z&|8h&^J{{FlZ2R?C3JB(ylw)2+vnpfst<w=Tj%<`?xpDL()z&Eu3p2smO|L2n{2;6
z6aAYwPV=jnevXN1%7F^;jtTkqE~r>RpH-@<nynQc5fh!=lL3DW`N5OeSGX{a@`s?7
zN2YV5?O3N}dpKk?Cr_M}g9y+}`G!jwrNl59<h@@H{ybX5aFgSI&Yy@CH~51j#Abf?
z=dQJr_-FfVrS;jZXKL@|=~_VqR%}v~P}XMSraQdPq~5>DYDx@73`0=oOPcOMwdHDR
znx|hBHymM4q)q$log{oZJB$9*_cj)LiZpghBQE~avf=pYW(#a!xgG6dw%jI`7Q0#k
zMSF=VO&_#&sOOn8C>NxaZ+qw^#v*WCCTaGF1Hb|0GR0yLd@Q+j)dpqEl`E1C?o)3E
z`6F@fvyd)XKcWJ^vUH^0Fqo_3M)cPm{2+2yH@&F^Yco2P#|T|>PXuT51K#Gv)0c<S
zkQhNCkY2n7o9S_R+QLD9ZfIVLhu0oHu6K+8a0<yN@BU#mf`g{2`iD}*^*~c*d-BQo
zvwD_Xk4Hur%bA-=&B)FE=zR)@1+xbHpgqqI)gXdxj@PH%7&fD~y1>OnLXqfM66WO)
zj+F+hvdIv>epUl<w8N*``!;=h43oh+XuevUT(f5a_K-(!fJ0gr5>1HA)9oV(>~)2U
zt$ug~>4?wPme=gQ##R2hUUq1AOp-*$28sV$Jl!;W&54|*?uK}Geehdwb<@Yk7~0i5
zO!)8Y5t(RCV}+mEeYp5ri&M`Ig)b|1RQHCUSIfYk5#XQWJ-qg$H;QH!<y={&z>d>z
z&-ci@c$+J>jEu{Eh)jt_Na%bFhWH~-s}pMHK_!lRv#LO-Idmnc6{d*uE<Onas2f27
zsL>%N1pPLxl_%`)T#4VuMw;_o65>5RYIflUK)j~6DM<t!IYkIn;v3(I9V>LZ3PXX6
zD8+nQ<2(UelRb2Kh|0{@#$y0Rw}hl}V~k=KxSiiR$wzcJou}Hp$~i2ho?i-%vP?wC
zrdMMIX7-F>?@Eg~<Je_a3q9>mb-*#YQTYPeICK_Mp}e)slUjMoF4uw(gM^&fpd%&-
z94t%dhqQ3X9iEB8>F^@ns4NssUKZQS#IZI+>1ek7op+l)tcNdsJXfwVYW?>U=hmRt
zQx2A05;6_v!iMWP2O#(|t!oU4f*Q3tde(Y++{6(uPpGECtn_glLK%CpDpUbBF^Gkt
zT{hfT)oTzBt!pss_=EidzTiqXA_eC}xt_5w?9fj_<10!h)7zbJY1{y~)EN;YhR4xB
zlo&x)idk+NRo2Hk%L4q#1yS*YmG@dX!G5&bYVCtpWB-iCgkpAVyMxQyMp*#R$}nJq
ziSc+RxE!#P+>@fqV-mFoHoU9h255QoZAc3f)Y}BkJfu6Y{Soy%1F%-Ugj_s`m)ps@
z?q7kXLrXQ);Zl=b9x!ZBDwG+vWom(UUvisV^52;zI#d2VR3z|}Vf0hc8~JkjzpV;6
zAA{2@w6j{zR|NrMU!LXwKyL)X!WAWGcOnKi5%3|K%E<Rg6bonK8|;x2W}w-fDI5QJ
zPn=3uue&Cpn$U=OocA95Xu<1b*bMqn#7N312qB4^TM0#q+1RN2y!7wU>g=X=$#a0e
ze2B#}m=z(`z+Eii8(U&-X;t;)-QDD-5+3pWw-}PnNP+8u+vmL4cp}bVM-zk}b-(gg
zoFCl=S&oboW9jnXa%?r6_r7c>k#VyBkjQcR7Ha>jg-n;DX13BWijy}%{X@b^n5*+x
zI~ChUZxwU)MQQuV@Wbi9tBfR{oq%cxIUuob-)Xjr`N|#?&-+^<e&=OH%W!!bAIl&$
zH7B{+IB2XSp)qjHkvq<f(Z|NI(*3pYIcmE}h@nSWq0ut+b9a+78&)!CuuF~RnPCuA
z8Y~YNzOVTESNmqYl-=0HA_8Lg{$TEI@>Giv>Da{r#&|fEzLg*Ev%{{v483fKc(3^R
zt#2teEIHS>2OXJJ<m2zWT>C`Y0ko@qY`v`<;ecxWch7YahcDKHMC-|VUeFOqjNO0t
zN13g`;$~6bI0dnowDUhUKDr>E9NzWR8|<Mw(m5G^NbF}UJJh)h^|>e&{_btLn_@47
z^!W%tzPbf<>aW}pvsgaau8$gnOjiSGz*RBZzuZ+gSaDy5tHm%%I^Q4CkqE8qT;SUY
zkLQ|ebqDR$999L+Du45WVxn9TZWDd?n^SJqU3i~^!hmb$4wz^0;3#*gB42duQAlR?
zHRJV=_-M8fbZuSwcn^G{S&|eRiw0-b*k;OHm^}HM;Jj0>=>$J+;O8oJv_M6$%-hBb
z9v^^+tbl`2tKK?_!{Vd61M;={_q-GWA*akHHU^gQXhJ=Y?%4VjU+3F{R?&gyZne_X
z^lf)+Gtjd=`I?woz|5CBv7zlOg8)DtTYt%=aXe|A|4O)kBS1ccb>Gi&-TB&@b#d19
zI=qm-S4ZjoXb<PH7@K7!&Ky8R?c)M<DIC1TY5?EvStq_xcE_83CR-(^EW>|Lu~9ZS
zl&U{ISLUR{UardP6y$&CFg~%X-gNvAb^R~gtLQ3MrT=5F%XCiS!`7O;7E(rcQ?}~A
zdnI&*%1fNJ$CXFd>rtVrjYsLL11_K4?di^c8?kco7E|2(Z^H!O_V-VWW$s#xMfyGG
zBGFj|&99Y)K84pt4LPnB+EiR$IB3H^>S@EUR}*?ht)#axb)<IZoeQpihOpw4NHyX?
zmq(xfcKr>3Z{VLK-={Ff)9WX#z&kdDT0vI;_tib8kZ${ad?12XBZ6&?Ar(Fm&h$Z_
zS;n#3PkwCOVLaz5AIG!Z3)d@4QCdT1WcA81^~qb>oxAjFIQK;I;SmqZKLZ7Uj&VHC
zAzOrO@=5A`lK{nq!NTh*@6B`W>F*W4yHn8$u3oW;9Qo%(duN(6FFrkS{gW{Ki(bqd
z^!sA0ApKh!7Yb&?o~l0|^y55T9*oil`#+vSfhi17U1{|4>%>mZvKF(+J;YsV?blYu
zknQ;a71ZLfxBD+DPO75=&^P8e8x0cObMeNZV&~CHqhC2iar+knCDH3i$jZk!t=2-Z
zvpk6I%@@08<phIN=+sW`A6(!2u^nA0gHQ#K-Yz<cv%TM&udQhb*zVq83>Bm~el87}
z&o)G%F)PjX6fbS<y2MrCb6o|YB~k%EF26(z)w~bi_ZKG%O1uqM2MUw1G_zSq10<hA
zm|ej>SIQk6Jno%q)Rde)H2e<Br$rliy_m+)Y$1VXsNSL`MyK|u2?&_Oeym5KCto5L
z0APve#kf*$6Qbc@QWR8w<H@AeqTK0>rQ54f>mz;&fDT`Q-Q-HFio2PVrB-TzGNQrS
zRIb3z_X7*hn`P5Kn>mna95&G&9uIwOo$tznFaN5=XbWZWIP=HRL$2HtRD1ZDzrpU$
zKgM{T{-T5{`h6aJ?+PBK&Ol0bfZ>nHDCVKtz2$y(`LCNqU`D`}ifI|8ME<}^+yZ$k
zna=?HmXElm^_bG?<9R0Nt`5yG5}+1^fsQW;7(jEcw(r3=?b&cdKFB0$_~&I0&S#rs
zba+ZX7AX8@vDxuv#YXyiJ~kaox8wl!4EA95*&u0SEzWd*DB2|F$^#I{&w%;l@4L)i
z>-;EYZ!*)JaJ7E}2s3E210DN%VA1Ea+Ue4>5lRawm_(3Nq0?03D2&hR`b&pa74DCL
zi7xbApF!~bt3#hG)MPJkZVbyT5WtU8AeDEgcWB0uw5>LmW0?iuV+j|TKTqd+5((%=
zMp?ca5B=U3@zX3{cAryRQ;}cgzNJ5C_v!sAhwIb30k6O-Ge>j>TVEP0M{S4O4%j&o
zo|?A1(2e{D$E##4!eo04|6bibjLI4-@1=AXN+EEm9*!zY9o)OP5;ob4mZxHqDaOw%
z1$?g5;8`_%`37~A=$H^!D_$Xz`nNY_OBr<#w4B|Qx!C&idP~OPD^WXE6uH{DocX9J
zD_hpf<^XQK;@opy(Ky8cqjp$NOj(TK$`zLSy`l&Id<)$B<EpA-cH0-Ni9oWUMVQV@
zIAJ)L$&&Nk<LPxLIO7{AQuViq28RVhaI|5{S`8zoQ!BpVsao&oDl(}bAwGr!nCM<e
z$QnKzv|py0Yw>8UIhV4fjuQMFh4YT{MR18&4W!MoF${}8e%GNs)A7S&SA}+xrrMO}
zOQqC&DX#m39Dz{fM-~sdzi4m34?ouPLE>^M6#W@kDrGyUcI&$@EyUDB@>o}%QEyw2
zz*neAUj7ircB%MLL9*Ti1^05w`BX_>bpkKZL|-3MC%Z0$z-22kA)gCp(#-Z7v&q;I
zLY9&eWa?spW9l8zIm8Lk^G@xgQ&Tz(MuQdKYK}J#;tzVBXV!MhN)f;BAiQ>W<l@(X
zjWx%GszWbF744Srf~ZtOI?-hnS3PEoc?DQHgNF&AYsb6K;{xr0Zp)^iAlqXk-B88(
zFo_4mgXRvY=nS`9eOSv=hpmE3OhK)cr<q^GW+(CvZC?}zs&jPGQ~|F3?wn$XtX_Ba
z>BI4aDtfd3=c{gDH=;nsy)KYr;fy;v<mZRK&HGwtJhn(CmeJbDz&H-Qy3eOxAuu9H
zPS>~Tsa*UstjiZ`o9+vK;*xg733-jMro}{Gi(DvJ0VxcaN-l>PgD(=MLX&$G(XbJ}
zdcIOp5G<>gg+JFl9>49AE;;h{7)a#uJkMQT7?IaqW)_&DobNRg%uw8doe0u#&`gA6
zDE0m^fyV+7iX7D)$a=Q>4&P3&epZROmho^I#@ii*dy)1xAEqT$Z~WQiG5t#*sD9o}
zLyVU+l7|jTxgC6$4<So(SX7~N_n6c<Eb#U>^VB-IgX|jPpq_F%v*&;7#d3T#qC^S~
zB&B5~T9qr}Y*D&<DEv$NMFaB6>2FK+JHI<b#X;p@6-Vo|{ri`4ULOW4t@A`BAHwH`
zR0PAFe95H_2kM_OxOwfS2Z$oklSf{9Ol9rkG8+cttG}1YCnBvEm~9-z#EUizO(zSv
zGNem>#tYcUZoa-6qx@T_T5K98dbpIdkpHxNgSE$jF7R+$V^XLPKQK@hxi_Bfz3~eX
zAzbu!!_$4zvU@C@khd^G*akG`FuuwB9xpdji8u)UmBx*VjO-1Ic(U+=*r`+I-MnV~
zYpesFJ0rL5`EgP9gkq_Wdl=AZFKc(O<ZjIy1j`;f9eRz?r~GeNX@HY}Yrs}!f(LNI
zyX}TDyxL<wWN}q$>rt)bkPz{^RuOp|(z>4kyzY|;IA={*T!Z4>O7cTg^z@BtG>okf
z2ts`0CUMts?<Wto9<te0Zh{T_%XJPrm-g_1G+@fvVE@W;=Wz;i9>+ErdJNZQC_?`G
zYX7X>)omvmmWvN*yE9}pXsty4aCa4kgWjL@`D_rSRisUT%OCg7A5;Ew7pkdEpTQ&a
ztAN@X_K(*TLc5sd1`MpEogO?fhgYrf1qsxhu_x9Zo+a)D03~l0tx`ul^rY?HpfZdg
z5=>e&7H)?8UdfqQ!Bz7M+;Ld?Ls$<8p#XcKZ!dZZJ$<R0dh{{G<{0DZIYB=u^~(aT
zQ^CWD@GvLUQH^Y4Bqd@=q&o>U^F%FADWVz8uIL+Qk!+$%-Ld#mPt>_`nXFfm3fW4l
zUy3d6CDw1a@lh+#i}Ej|y(k2~Qj3t!ie+n>qS=8nvLSN}$!t2>aJXGjI$jO%9*CA=
zU|t{P%C3OgYX0npR?njqN5<IVDw@*5#h+Uu6LCaacW#2OJX(XMVKFwcUppMn2<eKf
zhG;o`^33y7HrT>`aB9mS%61$MNsCrn_kb;qP<5JItF!)+jNcYMj9xp8Weu~qov+r-
zlO1jP`y2KIe&x2`3q`tR3j(!#7O=mgJ>O_wRW~H|E@DXeL`7YFILy%*ec``sJ{V1S
zIyJy;z{y6M<`GmI&>qMVNi`&S<|Mn+`mnI2&Lf-JU80~_Z=P5nOD&kb{A^-ldEgT_
zbi{qe?aq1LEMA(x7+Gq}=DsAtxY<>%f@{?(;irAH*w8V)N0q^P0CIYrz8oh`xU!#s
zKMf7+GxGd+RL#WLmp#5h)gC+-nOF`>1h#bUz-AZzjaSF`(e0*zxr3_Q**q8~GGGH_
zyve$X<hv$TbZ<f3Kjq%(dDUwCa~=2PYK+uBGP&+T@UiFHg%51WY&{&h>~1IK;c~6^
zBR9IlLPQ94dwD7C&1{kwzKLKHS7$e*ZR(Y|GM=N>4RlCPa}!@QDf!+GXj~YsHcNzT
zSGE0gl0NvE)s<lrY8vwI(<XadqUM~`kFf$cX2bTEbd>bK5{sDa9IAa5t1cbadMiY*
zU=?&)0&Qr_B4IF>SQ_KWMD1G9Mu=Iac^41Lw*@>O1Y?5NZ5a2p_^=F{Z~1thak<At
z$H15q7gXLG8S&tL7*;ECp&Gqg6Z~v&AS!0PLWv(yD6ir@c>(mQEwtpJQmgorG3iwL
z<}N#XCj?rZey4m0JQ8R&Sf*0p;g~;k7G*W?3UpTl8WR17Kk(}OFGl8$kW>2zo#PU{
zi|BGDk-5=BL_lp0??LK!v9;D5iTgNaM<pq#>bjq(=7@$QF5hQwnaPK?Ikjqj`l7&o
zK=CjjYobE0G{rLaHxruJ`zdGi_~RMjauKu%6kr?C)kMhcC@8!Lm@boF5~Xt_-Xj-7
z{UenyQk3MZT;zV3L~yFAf0x^Q6=M7m*nbGiW{||B&~>ax;Zs@X!NW+3if#hxe}rd^
zHWhF<4yMh)m0B9VlO^@O0MA#|Jyr&bnZTm&HymUeSA)kF$#Yq)dPR_P%;aQ<diUs)
zE7Ox<1&F2y-09?W5c(_g4p|fOOhG3@<@hJAtQX`C+mB#uyVlrI;yvx&U_>a+!gVnt
zZM2D?{?z-tr9#CVk`S_p-$oBagC)ssn;~Ui>b@ccdleef)K$h}tC61%KD~N)<mE+G
zNy)NoA>6@g-X7^hgg2(1$CW$~TPq_l)iI{h^f@k6(jiTf4l*7zlKXOIbd*pFG>W4-
zy`LtJSj}5VGVR3CO}mFfi#wKUlJo||FGg**BcRs9)MnY!7rtI}Wsv%0eR}I!Ad`OM
zjDKRqc0l+xfqhhTLCs{z>x>!mR)?G1V<_J&Ph^SwwZBV`i-et3Rt~JwwW*Dh_FmY*
z8OB3g{|a_)Ly=MM{#w1ba+Q{!LMe59Z?~SlopQ%FN1;2COqZ=%&EgrFx>_y_@i^Zn
zX^k#$(7v6q^s-4|8mN6Dc#LW3C1>%LW=oJ?!6Flqcr|<!HvY$0CdFR=1H!941?Qmo
zFdG1TeFR2naTY}_8$xcRS^{g9m^j1upOH8@_^&eW-Fs9R*b||y8xa{(%cu`%eoHOM
z>k!a?D~3zm#*bCTzW(6?2w_b~^^SWAxrf6FgAr^~5tbI<{g2gFWNzxxO2jQw59lPu
zc5)K2e6?p3D0&P`<=EU}-*9$Jc~leyR5w&;VQA&jZ=r~Zv@`jzGpKR2?aAk7)*Wsa
z6KAqDlAP(kk`VcQe8`856uG#7dP4i*Z%s5g<M%V+hUK`kvCiPhO5mdPU#_-zB;n}q
z%bLSvNww|3We!_PB-jsz=<3N;RIgqS9&fB$O8(!&b5iTCo1gi_hNPb~s_a|m?a%~|
z7w&CbQlcWF<Px4o8%8Dqitad$Mf^V-t3RsN*lgHZxpB-IAC{}-|MEI@W4eA~l6%*c
z&6GnPX^qMvS845_Rv{uzZisyQ-Q0I-c(QQGPllM4n&7;uWaCqC&<7dWivQXVSN6Jl
z(?WPGmRe=wx>azz@-nwNw%X3?6xK(rz1V>IY%-5jvhgNBv*~8JqL?$f%6eNg{z6{R
z<M2qtr-vuNH*Ee49*nZ*;MH~S;MhE2Ib)dj$XC<oNOa|oEmNhKD+n-@L5RM&%JS7W
zk9bfQ*FIVZBPon?{uHD5HgP6^eroTGiV~AB{kgRIe57<FZ4g9Jytpqjd*epvtt#yN
znZP5>BF1kdra^Gs^(O}zWFc3vHn)sQ8W>zXawF5b$|~bl_2^ixE!^xuEstBLn8S?u
zeI?*Ak_M7hiQcAaO?Sy#Y}|om(YQCREcHtQEY#?dV^m+tcPPJ9!dX=sJQJ=@6IO5s
z(ujLqx}Q{=)pehr>lbd$w?5(vKy^uvEpu@{iYkZ8Ho=r{CN>F;KFbU&)sn1l+b=aO
zj~~NwI_V=j6H)mnBV=V=C^6|T>gp(C`_uj)6c}jz2FnflaXc;s!>z32w>nP_n+{&)
zK|d^38X?ce_Icd*FNpX@$!)zjFd7Zo%XK5ROWphn%@O5VtqUig_IH9JSdFIiH98r+
z!u`)!vEi2+oYK?2FXKZb(#@$$AhW^j2ZSI<zB2CEtUu?3s5A_@c0M7UZPZJ*2u-ik
z2K-(-T$wMs*=Vv;?3C3x24D5scTqWnUsxT`(4S$JHOE~EGXFFiOJOPXC(#vZ(dA8N
zL-l^%PnIbY;vIb;1TaA7<|(Pqz=Dpzd0ljBcY?dS{QEV!(fK;=<vtRIx&0oHW}Rnp
z!&(Wd)nC)5<m8)z#!-^=yqgxMwe54*@km_$3WI@vb1?i_uV%N)u59y?f4f_;P#wa_
zf5VqH@dL=e4}>AW$1yTo&Vh{UUZ>1bYdtN3y&zXSckzr1KFG%@)Zvf(13qUk3a?zB
zmR5X>lv?2y#fL^qCrq;?N&VH`2Jcrg)cs>q(5ftAfRW3R8->-42#beT`#od5>hYwk
z2|W?9_<R4-(6r8%@$H>d_+3>;2AN2R$pa~KrlHxoo{-U}(K<N|9a+fCsVpJkZ_rKY
z{ekiHC12h7Yz)_=PIywgxNMnSz<~dC*m?TA{Gk6I@rD?KR_#UI(Tbs6KkfV9s3#=r
zn70j?O6WnRi*XTlHGdY}I3OHNm>EYjK0yy_LO%bFcnkN4CXv1$`AAIoVtiA4wp9u_
z(MtD^Zg01qP>r)?mvfOe5px_p!+`ZPV#V)ONRt^dh|FJEOgb_Mf3~6g&O&)?yQvDA
zZ^BFn=>-!;>JT{&ub-f^;iK-;Lsdz73AvN>eqtL^i%PC7LQPq!n+Uwr_@JQcampEg
z?PsQ`lAol>vfOKb;tYfpOGC}t;vT4?^pS?=VzMpBY`ofk;B~%7jA?kthwpt&l=txw
zIFerH)U%B(9eZAx$yD??p?yBz(s<tKZaHx_^wcqDPHyqa6L~=1sM<{tuyEFFlmb_E
zysua&Ha_Sm*58~f=B*I2x=xWe;Rgl>mDYT07)|R!l6d$w?~LuHy5Aqs%$KRgH&k3E
zn*2(WcyqtsD<bxpv$;n|VBqQq{6?yGS;>1+TgAmWwQA{dmZgtWE|p9}j{L_j0>Yt)
z!)8SWZ{CLFvEGW>WucgcaMdMN8k?yU5<{zKG9xZE`<Gfw*r(SHPOE}9NaT^%-NDf*
zHQl(vD7;Y>h9$@`h|qEo>)?@$P4CwVUf7>@=%!;zhD!m~8PP~b*KYMztE%-}aw4C&
zhURpw1pZ^=P+s;BSe!0`)Uy2=?~kT`bS6v#p_%zG9668Uf@FdTy^zhafvDNT+&hX$
zaj`~S&xZTDO+izjcafvlQh(0lxpD$!zi}l`qcYdBIOq2<A+7EC@>K*pqXJUb4#&3F
z=F;yDx6+(SxDJ%e@!V#84|+gDJy>C3gUgW7X#=k2lj@M4){z^*UNR5FzXY7c)F!Xw
zPYv4zLMDfvN2ks8{6rWIz7ELggeI}M|3rcyYGF?!-J-`x6jYgO+xIEiVSe}*adL7F
zGrzw&iZSiqJfk|OwWPA`l*Mk0!gaw@x{<BX%Qqs?uNXbpphz17V=sontvr0G*f`ud
zIWQbzAy*ofw;nXyvZ?u=eKwgdLiVrkqwAo5UUXrC1ZBl#pdR9YTi-0NeRG>ZTAG95
zsV6L*Z4qrAhr^V4|9dy=;QB<k8ED!QYbY-9Lpw=_6Iz1`Bt%1b6C^>*@F#t$qYc4Y
zSLC8h@w--$Kw&^gU*A5ge@3^C!v6Gh6K7YGNZZF)U1`ds0m=R7X7XJA$%MW+^y>4W
zJcHwG^`EeDqv-D$fBq<Pg1i}AdarG7TgwmQSpA!UGkNI#bhyS~kS1(Sa6EPNLE3T6
zsYG6)HF#1^3nhbtMoa#+RXeHPCp%G|K|j6uITiyJKCbVS#=Lb-%tPj!)8{<Tqb=jj
zt>8*$kidSj^U5>$GYa<-I}935SLdJW-^1~Xt=SHLkGHU379u)7CQ4mD>0-D1%H(yE
zIv2Pi!sD3m6r@u#K;m4r--2~SN4M`N`&)#3*ZsgkEd$PG|MMs9O#4-a#qLSy^|1&u
za;WTWoTV|NJM=gfF_r&*x9$?DgUO>_-*#nC$#|LzezwMSsPEUC(F!85&k<b4_hQZZ
zQhKchnJ6xMD8nzAn3#SS>#e8BtUYf&cUO|3K)#xnkod4fCXNoMVinW`9GKCyL3Brv
zAF;jzSM-xben<_iNRT5`cTL_&TbA!7b<GeEf^>4PF#xzyF9lkw5b0Z#a-yLabbs2B
zdvRL09fp0*VJ@<G6A6ew25n1c1-P|#LtRmxr4@6PXRhXOY->IXAT6W$><{j<io40`
z+ndApxihbhA?D`R1}Et8-NVXK5xS${n%gePGIXaSR`+XKcQy?QUQQ?Nbm4QZvgG;C
zy>ni4M253j<g~8rDRLVIZ!mflsu~=Qa_5dNzwUL|e;D98sLf_`p4GbxH0j;)(Mcpc
z2%dExu78KWBeK*Kuriw^IaP94L*OSGGJx}YEjv(4rCI$<&~Rn^A`Rac?CByZxa|5<
zMMHF6bWGsjC?fpzA08PiC$SATWv`E5?uF^>&+>m>+gopM561NYYh$gp^s7ERv)@0<
zdv~g>ld+Z?(_kjxUOtRgq?+FPL%u05T>4nZF6mHJ3gq}Fj{U>mkrpCI+JHwI3e}4l
zP9J@b&!je-f8}>1)jjwFGVZyg-W%T^G`eFQ90eX=l1A81?N5W4HIsEZ3_4?)<;rq4
z8wreCad>z+AwK5#AU-y$Ef+BQI4jU_6bZl6x&zI>xidY9C%tF7HJcmBWG`&|h;jQ^
zqJOPO%DR0Vy^#rT*bAiMu$g5l_`&RQEm9sB3{~2^ixO#&q3y>{YA*(#uVNc46!36a
zB5kMG@ek|Hd=a$@bJkf_as}Chx$<-sr(649zZ~tJLlz<tyHO~(+AW?pKAHvZ34#j?
ztiD*GCilM9pfX-!YyzG#<)orw`^n=`wa%kjq58}Dd_9i}dC-ETvobUkHkHpOD@|zJ
zmElFmx_8&R^+^kJDpd$xt?6aXTUPey3HbXKR5-7*<`aKFNVoO2!v?+xA*NuBdBC@M
ziM)QlZ2j<FZZfXjb75w}E~H$__2MdBR~QGBK(>U22UqK*3blpa$eX@f7`q7s-Re9T
zLUv!!Ir-ppuLXQgN{Z%EsN|OFEt2ofSGj(?vixiPHXys7|8jp?_V+1;cv?$d8Ahpy
z+y6Qyi*_oo)8*s+jS$Pc+hs%J>OvA0OQ8wIYAmaK!o#(e*Qfs_pL<{cb11z-NOXUA
z*OW;z*KFkz!C^z?sq2L#ly`F7r(vv?3ZEw1sJWBttoa%9E@h3z-PjHlUZ~U*u8S6$
z2opNS>lOkb2aU|Fv7+_|kJI(a4%07=0isw8k{)jsb%#~vZ!0#!<_4(K{^d_~)s84c
zX!QHxm~+vWN}WRZ&V<wcRd-}EuF-ImaGK|4&V;-S9?03M$y)Pe6vo!0HclQgp1I)a
z+p3j8eFTEfKW<L}-vJ1Y2z6>1eT(?t0G78vq3H^%%N+VkMI%-xfF?L^Bw!n)F1uj-
zwwsyR-57kU>C2)8PqskZQX1~Jw5fA+hfn_Fvoeia<UVZ@le!8<AA#x9F1c@SN`3A0
z;(oS+UCpa<dD`6S^e*11!dRYl3BP%10IgV0JyV8v0A%%I{Qh{shcT}=vPL`g309J7
zVs3d0XRf?>x}39CW=YlHbHzgehhFkUMn@dqWa99s%TRsmv6jY1Jf%iUjM?tkWpamD
z^XHtK&Iio=rgSAE0s(hrXyhRjTt0d5pXS*K-L$)qGw@=|6O!(66E?ndl>M`c@wW_t
zjOHEU)W(;8>b6&)w|9-Y%TBS^$BK#i0)?O+w7j>_!>@=*CHNnSZmZFyt5po|VtV7=
z_q0-=lS*O<(ox@jZjwHTiuM2na0d=d&C|3CJJkTcF3f6*i^v!LXrS6Y=O3aEV_0(4
zVv7>A7)$~Qg7<*|BSuQPj)VHm+aL7QT^9z<x1Rr2|A`4G|J&}13F_BASyRCEQPYcf
zuI+kd6ZBZaj)6G1ceUQN2?}uXfN`#TuejoV^1SY-&V07nd`vZ+ZLzpNUKQ6R;#Jn+
z?De|VcFk;foP?8Ws6Bf+1$E|I>%2PL=IFy66ghBOK?=$__Bt#eq9<Z2IQDP2jH-MK
zim7xyaDy1XYE0p>$x2&VQ+aA*5gb)88GMRv`7iWqewIVG+W-LVib}&x_B{eRAvZen
z7M07@S(bs81O}6Jr>TEgmiLPphiUrshNXd^43s<%hak#1;(6c3>(6;*3~4*6#yU2e
z5tmEbL>ksQGuu{r@1n*)sG<k5?+YhEUpGg_DAQ-&D{+V$Kgzc4(|;XITupOX7*B<4
z@_>m_NWT#*?OVGYHp9&LQ#BPsr^LjhG0-(~PYmGla+Hx~x4)$9O`=o5`>c{qMr*}#
zu0yOmB!gTMqsJ{Ij%D*I4!R+Yn!MA&ZJsr*0SM1WuXVdinwSy8*yJse<8b{dNme~=
zn?`eE1t+X~5eSK>?JGl7B|?Q!)4f@(K8FW4@9^jKn{+<7T&9U_@55+ihPz1P7qQ2^
z#L0SG?+gMP8HV>8D;%Yzx)$A@sOD&Q)ITRxS8+erU2c&xm`gpQmFO_lH&We~k^V?-
zZQU~+Sev-wi|ND#fD{+~0#HDkjf0WuEj=-oPg=Bzy$Fw$QtLe;sd;r`7^X8j;Zc<^
z`BC+qdRMi85nVMq78hS`G%Y|)S?#;2-k&xbsKB>$%w&l12<iFUR2VzkD^HQa)Dqei
zL&Vmi{JM4lpWth=Au1EBK81^rTt>M$8ZxmPqqnXZ{H)82YDdmW#5dajNBnk0D*7eL
zm`GhxcvWyZ?*?MgeBGux`7EdXwzJ#WtkcZ|lgFN)S()*BVXs*u?#CC4n5U!jXyD%r
z2(W>I;>PLTIr4=QlHd;Q-#P2*U-}Y}N6Z8zIlvk1&)sFw>tMjQHpu<d)j9C?-v{3d
zD}cM}Ht0__^u^`Tcl^s?S0wuArWehz?4ak`!Ogx6_ubpB-=0Jb8E+LRTLN>{@t!Zk
zMJMh0riPZL{Bk0m{$PU<b7cqS1;kZ+(5&aj+9%5O>3e26EPC~SdLyv7sGh15Tep2+
z^9iI-ple#uazi#XL`xvSs8J(oK3BZW911CewGIGs-!v!a6!SyzJeEf01Qz4v_fgjo
z356HX8}sTq`Iy7pCbI<QwQZu2Hm-HPt7)u0?9*a&b-zW$bEI~ky6#4C5daydi<uKT
zW@U);DfJ#e(ZnhOtGI)XHs7(hL`B9f0nHkHCg?g+K!Oyb(JsQ`_8gX^Doh}YFrm}%
zi}OM-%?t(RH99j+{hSNuM3sheegsP;8S~r;-29j#I!IX@-|34UG?3*ynKN6uvUk$?
zUe6oNpu^so(9Tew3|*7dX}yw+)hC>}Dm@GtRaU|h^kvOse!*vk)<E!0AF_<a*_Q?_
zI=j6bI%C1%CMSicCJb6;SzhZ&yJO+=yvt#^jE)H?VPEr~2i=V(2~3V=Y(Qyv-fBY9
zqE@mZG(YR9wb}RY81b>W2av58=@e?a@$(thm7Z~<d&qIgnh6Ql$Vp0uy>O<i^S%&+
z9mTG$C#7%K46r{hOE4&HHr0OqGO+^L8~%yy#G`Ye>sj-3k~)a<T+T>G+<nDM8jd$G
z56nO-fVN|5q^Gp1zl^4%Cb1W3-f^qbuMGBPAk<yoAHA-z8#HSUc6*H6Lg>z*k(Zuc
znP`Tuz8QRXK&xocHFr^NEVAAOSqas832-n##tGaWj>o$@os3Kn@)-fOt`j|vtkYGs
z-?@47#u*x8!_}(GL6Q4FsY)e|2xH)AsX7nuFt9w2e8`a`fWx2%%Inup*bbB<(=UDu
zEq``CO~(QJ*^=jVd1KV&vN0qyJHCM)L?V85=sy!9XS*CbcGtYt6plXHb${Y$p?1z{
zZDC|_JOKczY`{0qN}xFg0t_gcQJD_=-w9G_j40yH)7eac#9<lmsxwpk3v*&mm0q{w
zT%{-|9qDf`_e#RewTmII|AB$c$(+SAe)-tZxxMEUvR2)$o59mkr44rEzznq`mX($-
zlXxAkmXzRKnjpV3Hmq|Henp0lW774|4&Q6cszvIakIa6T?@p$x9H7WI?nm)W{Fb0w
z4!!wzv*Ck|4=F>wmyU#$OgZ=FXR%?JIzwoyBZq7QvBV{3n5AYSs~9CK|9w79se<)t
zwl^8<Il9AnB0U;qfn^IohjbBJxa7@c#2&&XtvJ<0Zsc~n@weRYP;NNmGjXyc#zLbf
zOykLVoc5jMvQnU%JJE_>Y*Aea`2KabVS%uNmQSvKs6T}wOjahTsSiWa5hb68h#?tI
zoz(R^?i+a9&)y@B*-VoWq}<8SQ^9kB!vFqoiMb5Uc50N$0CnMT%jiezv)7hD$Xo6n
zb~2ToMB!K*4qqm@(<c)#drY33+-`|8vM%Y3U?Rl76p*VV&!Q)z*G)2OVtP(@XgaHd
z`Zzpui+^i~z2Yy>C#7|-JMgN$^Lzh#jm{scmcBL*|7v)adkxz}>~9J_pQ@N=^m1xy
z(CMvlJ6Bcdzl0v(PohI1Lmb&tSv5&6YR%n3SRP@Z-h1O&okvb)sQ;ovQ(vU0Lwn8C
zQKo5WqAk}hP)|p>u&o@O{m@b5R6^&1ali4;<Hid>#wV&WXcc-rUS(RZl&3l#EeQkS
z9SV=fGnMWvx3+Pmc8}3AP-mguEs|V4n@oZ4MIa8jv9+G7rK6eW`}3g+iP4x?@I~1W
zfdQ)t2D#mW7YZ)BsRjvKo3mqe2D{ncjsZb}J$-jyfa_n}+-nJG0+kIge`{&QW30n}
zipiS-qj6H8nLGn(MglD>>ClZ|asBtn-7f=m%q*%q6j>BN=%|Mv;O6Q%Ak4F0y9`ns
zH0*jqY_gEY4!WR%mT@B?M7gwY=O`<uqUV*bf+v?ee?#;Kp@bq5aLXIBL~;5~f`Puj
zTKBiY&hOt3VQXTZh<F_694qP?_3E)@QW{6;myQJ8E621tR60%cZ1sH+JnMoY-CLs4
z)FymO|AJ&+C8@^rtQ_X{)R%ul8{O|679?Qxbw9avmM}*Bv<>ksH=I3Tb>X4vZj^W}
z<e<=fwWj~kD0bdHhBy5y%i{yTxzL4WJP$f$l<GeIWnM8v8{tL2+Oy8qYUNaT>Rz_<
zb$|IG%E;Gw^^W*mq7^YLU62s3Y$U@VN4Uo5P4}4B+*wmTe6mvEo>f!dmETH+NO1hZ
z>U4N)kx2U0^4zJSkHL-n2Ze-?aPX<z#-@BbmY#{VX{B?;*!1@adX{0bqB_X8cr|pD
z#NJy?c^67X$x6yUe(O$9&Pj6t4(B1CV>7$E1rBP4@}RAsdp;NVVrd&7)$h3d$PaY3
zjV?_{ZPU9E7Oy9{Xy1$;eL$Z+G5UlfgmEvmKmB=#|K{xi-uyza^)3qasMEoRCpmn`
z)xXi%vxu>8<WT4$d0vZ6HABf}9-~+T+Tq2$+uiCK`7T-ZencYX%RWVC1p4!A1){^P
zfhLR^`y3J@Fb><kKa+J{#cfCj@ZNAkv0-*!_>>*v`;f2-POkY=g?$6U(rQ1)M3o~-
z4YtCqTLNk5Qb8D~R2=xjv#oWkQfq)PpyZXLv0w2WX)4nNC{nPx*$;$)Yte_c?etu?
zp`QUv#5qydYR@Lo?x9o?eMt-Ct^m1Kh?#igbvf{L+Sg|F4I>fLH7Z4YLnB$e%jFZb
z$|ohFt=DYavCgm^E?2pUH8{TvuE?xvnE}stixrj#27+47mb`LLPB2S}Vqs_pC+6o2
z@y#dJ3jBOh!RwYZ<cW>Nhl>?VqwbU}MQ&P-+K|eK`Ie|zBJ%quSf5t1XY#$fpgFgV
z4x(9UMhid1?8gpO>IfED>(h>p8CK_hxc$VpTRjye3jLkyk(hQXjyH&fJba_)9cbgl
z9Hml^yk-H8sn1<Z^;=bnB|WA3(lIO!#<v@H@m@HmNxy~XPv=fyjTSxE^iEWNU0zr)
z#r|-<+qeEmR=r0LdB<ULF)cWYvTC39q>W|k-DDEu6A^EKhX2fs!PQN?a8wjAq)QlM
z{_#T|yPPnPj4f&Sm7F-F%L;t*u{S7yJ@3-vEB&FyBK3>fFKla5(_2R6s!qBSpX$fu
zW?jnYx<~EkDqqJe_Kwk@9}wDDr8$SW%RN~|IyoovorUBi51qw}*$YERTET>uye$>W
zS55t_$V_JHui4^8a?9zt#JpVV(bzG7=M3<+9}+tz??>;prqF9vC#KF(be>rGB%EP%
z(WN?D*|NzVhGC<bpPRV0NpgUqW+`1>t8gP1Bu^I$oIfK}+EXiD4f+rS(JA1YP}v>V
z{(4XY+5bzmLbG7tn+dePyOG1Sz$1IzB!gCiQHn@E(HHxR=lvM3nfJtFv%#7Md3xiU
ztq_2^%DZxC?q%u!ye7Lch8t3CX5&+>-J#XU*Xx4yNCZAwzd5LVl>8%Mp5DcY*&3Nh
zUNl4JzKa425CrSrMw9CvQpub=<do+b?L3XzR4pIX%UX>d@a2j}_2`$$COcje^(*%y
zug-864Ha#So{7vAl+e)Pm7TA8gDhrQ0~^v7Xqo7!@8iGEE{S^h5wP0T{^Vz@s3T#`
z6ocCve+esI>obMqArp_SIA^uc3qB^H#Xv}u<5g4DA?XC)mdk89N@aeklGchMF5+3Y
zPQF=rvzL;+c)IHKxR&zQ4wFo1ay|d|RcLO-z0SvnAq5`(Yr;2%KfQm4Z9d0I%exvL
zMrYce9idvmWPXc7jZ=Jn7%|V{!6+Z1$@*h@<gDK6Y^5UFqjaT-;>v#wh2+0x;W%`6
zcQP*>6>)>5tS0&>?dg4S<4j`u{ZZ_e9eEjX?~{gfBrzChLfms?g;*yHRkol3ha7N{
zb39DUk7buJulJ;|7~WY-u*5nb*Qo6i5s@#R*Fk`30l>$v4xdJsV3LVACq5+U%f`du
zPHNU@C$X#B?Jn7wqP%;GTP3WDWC(O@W7ygeEG80*!aFr;zf*n=(h9x;2%kg5S=|Kp
zd8snP$U-lw63esza)#Ec$}ErKFwbJ>va9jZ^BeH%<`8uHa3@4(JTbx%1rz^_vR?d%
zE(uJ@X7Al3Vzxog$>W_x$8hF%#(EC4eZ_1Wo_NyG-jt?Fr|@Eu3(qsBW#^r=T@u;F
z7JKFBFLpeLe&#vF@+I<QAZ1XjUTN|q6GvTvdox(E!Z9I%4v*%U9Jep-P=8H*(OSni
z#hIDge(oFY&5iVp{_6t|UY{G>04}MPf6Zf-Z`V#m<Ax%-U@Lwf1gp_E10my)1p1hg
z5d>Dz0Yg*j&{0X8Sx&=&NK0tzhAZ`NQ2rMYI1WdWPpgL7_=S<ST%nN4E259$a#)nY
zC?VGldpct9qEclq4~MB`+AUQw#1AQ>!UO|JUd=}F1pH<`9cyUNw^9X)xIR7}8)}QR
zy~clZifYnq`N;T1ts9RQzQ!4JD^_Tq@Hec!3o?et=?<avag=DtCW5(=cvYkTCbZ?p
zd76%y!*PANlH^G8w{pf+S_iEJJhnsH3<(LzZVbQ?)Bo2^?J*q{l4oR|lVF(pd#8w>
ze2Ff*2sC98h-##x7MFUJ{RifVDZX)ra(d{HZ{%lIJ=m_b%pV9a7dQ@pae|(sax_sY
zF;$p3WVltK>a(!0b88tH$=OPM88AT*|0QuAwwMwhn<Hwh0F|5NuaBf6r4X2}R3uAu
zj23A|!2Dpp-4jGKtZKm6!(p*71o%H@2D+Oecfs=Y!1{P|jty_`*HfedY+pl-v9eKd
z<i)xyw0osy^**si)rfaBTc9is)51T^ff9--uUd1SS#~Hqu&N4!NBYT2XxJqFrh;k(
zTdO$yUC)nzxm$+iDmBK_+i6^ss$Y0Zk_XYAWLCI0>hNK(^@1#p2R$4bEpDAHk#5^7
z?8U!F@(OyCxJp%3M=+U+L8NNc7L@y%YCC@m)=gVkGay=8MFidMvu6Hsv40oT^ie#p
zcGCU#&vZR^rqQk%L5N^upz@pO#$l9m@P+g4K$H?Rm3GkKpfKlWZW~P3?hd?pco;;y
zJ;xd|50vt@r=5sWR%0V0qu_vsv#!(cL1REk@YJIG{$gFLbB_myFVs3`NZF0`N64eU
z2q|n9i#0P{HVYsaQFodZEs(Z>9$u`}QQEAX6nvHOTGW{@k|R##w#%gr|Esc=-F&%L
z@P~utG3NB4co(;>?W0UdGV6Rw0u$b@|NfL_=J5@*f7U-&qGK5Z1wG`l>pt3ke{SkM
zU0`>zbGIWoG==DXXKl7vRr(Um^zN1H+2)%0!1e2+1NnSGYK6T`$En%V=Ym$F^)}X9
znl`ffb-TSth566ZY1db`!%<B8_wf7Ea>bTn8KRpixOdr%Rx-J$Oq=<2b_tKS_Lcc$
z@yn8m2A1C2m}k#a@Ux41T!P~g=RL>n%t%a@TC!InpAH@FkV2)?jZ`i_cwhHTv&Cfp
zcG#jqs^s`HQD&{2)uBXzh{27?Dyw4U_X(G>z37;s*S6(}O6}&{8uo9SF_BAv18bh&
za><meHi-T!oPxeAb7b|XY#~M26>qG{50ZedV=SA53;hEFrIU~l&^02(v7v5*RsVJl
z#-V(WBjefLFS3GZXq?ZMa%g|;T*jDDF&i|o93bo1$NBYdc4*X^C<4H+`F)(t{tVwm
z|4V9rO$>(aL%Vmdn-l)~!jt96A_%05-%(WDbj)}$xfZes=$y}Ev8lc3OzZz4tqH{W
z57q>A9mZkR8@;Cs+;Qxc;S5dxf}T~Vd$n{4j?^wGMscfGG2>7GmkAM1*tSC4!V7NZ
zr_^=kZ6T&ErSl1rT^jO{-v|Pezpya51iM<b5x!bg?sENOq{tW>9Z-!YTSKyFps~QL
zRC5HMUxtgUA*$OaD%eIYx5qHLuRwfB^4W9LEJT=~t}IP&Wh3h&7|Ko*xcS-{1`ykj
zGV!m?87h%%E8s1Dqq`J*BnfEJ!U3)s^;cSpIqm~~prfAlH={_f1Eb-yX4jjh$^=sY
zFxI--icCxk1#b3ew4(1e-^Fn^@>GI%s5OH0t5o;VSkDs~6>D>C<otgv_kNGIuOBqW
zj(aJ%DGuAxQDEkm$Z9GpZd;jYfnr2OMV*ccw6=%cE9|%noh#Fn@;{S--$H<kOlW54
zygQ#0O?n5#C^J~-ME$iEK-l}H29I<V4>2w+Q8uE0hIYiwJc^D+!ayfBT$Mz*Kl~y_
z$)BQUMMg$8q9eawE#B&W^K7BBpDFk#9kGI`yeJ`Vm{Wzdi$<4BOaDe}O*I=LL<M{q
zeGAIf&TjFw)}?nAX(k%z?x&&}?Y_OfS<RuVIoPDLKhew$gnpH*_#HP>=P6jC!!H6S
z4ka0gHMO6_PXH5w?nb<d8~0e9=_YP(AFkboMK>s`_m_hYX?VfwV|aTOd(T{^4eHC2
zGysJxaDU#p@o`a6UT*yC)>|?1Hap&*`I{PHFSIT2{~c{MvP%c*SZS-M{|=R?=pGx+
z5b#a`w`ZI$_WyUEV6Y#-|K1fBV?ZMQe-AVK|L$QK$vVyUVt}iN9#||U`1*bd3lIMb
zl$-T{-?$WCzkYpDC9|`$yZJ9V1)W?w>e>eITb9~o_pGoY#5k>1D1ggic><H3uGdGI
zN*NVXC%6XiSBY+(3`k2##sR_fAwXmFuTrncR`{azQt;kAlfh-(Dfw)(Q*QyJ2ddoF
zGu2*?{>EkRr1w*FVmy<N%SveHkDYUQuovvg=6^*~j^oJH|6DLUZa4WIoGSMAHEj<o
z7NzT1LO!~B(jC!$8Q7UT4nMOzZzOR6w{8NXj^>8<s}(M*QK)vOw+?_VA?po6P6m8k
zUKY9OF#b(|@k<)`LG~ZNaHc|w>;7#}m;?!o@~uLn_UGoy0Puvr;7gy6cmN<(a!N`{
zIs?o!@7=5ay>SA1dWI&;^2_PGkUc<ZvL13|GT?<9cc@$}PiiIfyOsatC=XaGcYVJH
z2>>RTcmTaQl<o6g0+MWWyEOqgfbs1fk0aZ?A@}tqxp{f}n?4_jfPqWt=5P`OqSPDw
zJ`v&la!kG4;nhm7-6Y@fv;&XJ>sTC#!y@AScCVJo<4~vxh+~RvemUN9d%e_o0%4(<
zb`KY)1r$^4>$gE%FDP5!Emv+l90=$f8j?6(Z3MZutJ`*Iw!1skZ`D+LZ*}?EuUa)8
zKaLQ57S2HJ?-cC_d}Sq@G+U}NSn=*_+kzr<FLOMa63%izq47G6{BL{$h@4afL?Y2+
zeMkA6&td^xm^6UeSCKb$27J9v*<TRxipz=D32=toP-j+DN|hC0d`$rL%}k4vh0{-C
zAcg+2qI`6459r2<gCm0%n(Pdp0q7<L4UOa`873Yd5+^)1V|fw)C)51TB(a&+3_vFH
zoe<f2k%d9J?R)<%3^g#xT(sNWRR9xNk+V8*(g80C_=A*n_>c&=|2053!gN2{0fEn(
zms3#LC3}mZY#O`dZ$rcR7eG;|Rv#u#<R(g}oc0FHPD)3qTC9KLuQlp*dYiX*yG8?z
z{<6UyPHxEXh9<{j#YBVJMPNDc2;T6xEE()S6@RVs@^0e<TWE7_upxyLt|K$*wiWA^
zRU-<LuOUBj5h0&D0ZhA-TCqjEUACcL>*~W?#!h;{mt;uas`YvvF(b|eM~*H&0LxG5
zWyN&I`T4E38~=AcTpoL)mCH}=pZ=MCYV}sM4k_nq5$IW}HY@;OttT}C-QC@awI;uK
zT(ADUOyEz+#)bWmOTptXFXl8vx&vla#PcSAb<EjGp!Ut=_wU@cyBX<|lqoQ`?;FK~
z(0}2YmT1lvt5r_7v+BW_2cag=c}FCF3~;in+YJ)l0B=MYTO#b#SU5HlBVdV15Dxfq
zY@z7&@%DH^a-I`Hq-e@Fy=ANa2wVwHwepER<yzp)>~ZsjQwj>|Li-$?XPB+jmAou+
za>OX}I}a_Dya2Wyi?EtPi8!*85Kkb%VAh?ckuB(@Y*Gp67hdq<YE36c0FTl<k|AJ}
zrA-eBU^VKVqOwi01K^jXbQBZ1J7;7W+;$>PuPe2tF}&ZjfrK+&a>y0}AC09A09}k-
z(EzY*y{y1IxxTko7TxsQrt2=^^Hndh%Btfaph^Mhj#6iP=E??cy+z@1jF>G<yH3J>
zw(C^W^Wx}zyJ8Xy;c+;S0JfP`Jj87ub|RQcU-7RrcDJzjo@_dve?liN+3nVH3eu6e
z82xwnY>Q+wXC4%Q)wBvs>cfM=f5;<qrX%ktyV9ris`bO>8&_C_Yrbkg88<(V-U$il
z(v)Bd|L}=6V|wpSf3fwy$#mKHpYFaos;e$)QyKv&NvR**4FZBlw{&-RgM>)8fP_d0
zA|)bS(%mJ9bf<uHNJ!4P_`Yktnf0w%Gqb)me+&y1;a+Z?d(PfZ>>UsJHEa089!Fk(
zJ6!EK;r-I4ks;)*KKXoS)R`sR10J=?nd}m!N=NH74V9=oR-&P1H!spmIz6{bmQ<vg
zoLCYuv-<``l(zo$-fWGMgScV#HHU5$0{sAJs>C`Y8Yp>brc*^>%SNt!P-lx`Nx%xL
zQ2(OZmA$La0s)hfcG^jxDPW<)*D(7L55pv&MA510POmn!vrn2R*3+v(5O_f$ls9oc
zW@aN;Qnicw@Zq@I@?(25p@q$noMQ6D+BcV&L>wZ;9S+^A)`Buo1QNPz`n7S@Z=!tA
zXK-imW|BNIOOo`yd?+?*S#aI>;He90=h}^6wvTz*f%2zG=jZ1bv$moUw)8WYlD^61
zdw}sw0xi(<d%k--wwbjRd~}im3Op)L`d=MN58Pfa-s)6+keqp}EquAd{ldts>5vpS
zxXW$*bu(>jW~+-O(aA?AQVDZ*r>gPIILQfjhrM>S53XhuN3(Yai>f<X*0<NsqLE5F
zuIj&%<w~M^u|f{ona9?vx=Yq4Ue>e2LdVfaEx)=w^lBWx^4N|sIHaYy)Q!*TD6;5G
zCz~t=`bKyt=kHk+wRjz7+YVP>GR&UFwCI5*H$P4_N`%R0(8=vUxnrBzOoIm`j|a1n
zQZ>(hCY*%s4ho_-=ja~fo!Ypq5B7b}AZ3*jZ1cmP1Qmo!W`05=UzorjRH}Qg-F&$x
z_@aRzMdyj<o_#8ZNl^XXmft8pGH(V-)QHF7D=)+2$Fj1rHnYo@P=~I82psA$kM5Uj
zZwTE4JDdnkH_jVBunvORc1Uwc0p-W+CT18n+2*M_=(9NTD6Sy!^Y!xYVl|U1jBYIA
zxa{s}{M`BY7vk@A1^Lw^3BDt#&DwlHvZ4$Q3R(kJ`HCmvtEgyCmh5SVsz?Pr6pF<)
zxZGVtCkceuRXnWWLM|K?(;h$f<#VYfwc0<p?y>Mmee!#o57~Ql5frTcqE(s!T9io@
z2ZN%~UlcN`JJcpe^5Sqf>Z6Fh<73QoIW%Z2N9THo6c`<onwNh=7(2Y$ya;7V8A@5?
zUTRwAL(u4|`p~!~7U}RkrYm5Pz~OU|k$h-n5m9V}qm2{+?kYoE+eWQW|C^$nfw+@y
zKO`L|w;J0WHRo>fMQQi@+*8#~b10MXl2S?E#(PFFLo|{Hl%8Fye-(89Cea|4Y;L~z
z?Togc)p;<KC7l58OS@>vUYc@|1bXclHqs9LL0W7yx!-ZTtavGrd#S`v>m6oJM+0=+
zT+nH2>!xIn?~tE}Dsc5VYnk%4^I$}Dx=I^CDcrPh6`%QV%&MyU*Us{FP^()4U17|(
z?-(qA#23_N`fwgW=PNnSxF2D4-!}=52xiM|d|-_?^oR=+t^BQ6j<mPVanMGq;_%$N
zL1Lcs@87>y?Y+yl4Cw*mqG7r?K?oj2Ym+;6UjTE^xixezcvCBu&7WV?By7Khb@p-K
zfVEcj_gyqWuINDU;-m<HJ~Xx{W;?GMzb)8#hE`rK8{?HyX!I+GbYU21L&`IwhcA?)
z7U3G5Fv5Bi3Cu7?Lc<su^yib8L#UezPrJ{JKRBcf+JD^GTFp98232v#+o&;}!IDr^
z|9Uwf!EB9zZ>GlS<Cavc2_k#rLD2a9roqhlrCgdf)6TdzV3x3@h+2A}T!(v!vh&bI
zAz}%Ao~@jnbHKhOa-&UFuF4xRC}04UhO-(o&Y0ilw8++}y5|#Az4!^^uPqlw3o?W{
z(l7;G0DX@oH&_j4*luhLrJ|*uwCWJdBE^PQ(wDSOtD7%=XDH9<Fk6vsO~;`*!rRG>
zb7<Fo54wmJzSiaM?PoJTEj#9%AqaxPJa5pa^n~>l;Y?K_N|w}P_|*+%GCV=o=wk8p
z`SJ2fiIStS=0P=V3{z6FmqoDF&U1m^>TPW_ui2LP0vT#JUZ*V~BEHO@8XzJ;lEhXZ
zj`A)siT)(s8hQ`2#|3mft>|~0EY{PS4|8#?B@5I7RX)cYbf%qF{CdYIE5kOV2uOQU
zdnbsoc}x}3cR3jecDB9se}TlQ5iu3T-AW7&!N)l+l+BcX)p54z2XVTJs9D#X{<H;#
zx#SiZg5iiSlGWizS!4wQ!Uvao78q648bA}mOYyFb>w{n%oQMe?Y2$L{108t8a0=hw
z@dNZcuH5eOq8<;d#9Y?LFsuaP1Nl1ISp2k<mrVK+XbYH0D;S>Loab!$i6?64%qM3h
zqcj&*PkXdJNoQfvF3W7DbJOt^Y9uF3QB3Dod2uEdYH3cQTolT{C(>$;$0y3k?lWzv
z1+x~P?O26Hg;fU_av;yW@nt<lKt(HO7xC%e@_RU+;_`!H{tBmvl7XjS5FY-CP&n|E
zxDp@;u(#<Dq_?%|>+naJ><QoJw|mn(viZ?|)}o&{I=Mp;AH`<#*uGT}?-|CX;k$Pw
z>ef@Jm?Ps4Q0Aq0<+FEMwfT2woU0d375JMAiPVBAeA5$qG2Ov?Z5&<MMl6~$KwF`m
z(R(^?%|9JP#!&HdlgqNz>KqjG%zfeCW_1PmJ_dsYg2Tx;dKy$cpk=+_Ek99b=k0oR
ztUXzQ>dKCvCR0cKg;y^w?9x6%5WVT%+*294BjhWOoE}7YM$*dVvw55<#;d)wN<C1G
zy!?p{9O?{qqseW+TY`Lbo4q{y=$wue%`ruhFwBbj#ZEKR-Uo_dnN+lj?T_Y};CkYV
zb-(t1mvSMV|G*jDKKJ3hP*h-gGVJ-AV5U|@)Kqqb*FvBVBriS6BWw?eO||ICy^{7-
z(`IJ#HBhfMzrUP1pM=9WKp~Y&GDf<;U-1V~@9TsiA~j<+6s0w`t4%Xf`tDI<>EN5&
z^F6a>9=yHQSV6g8Y3N?H2v&sUmEwX#AR*dP4dv=3;<-7QG%9?Hm)h1wUfm%|i7DeY
zi-{sA!_r#;G*>}%o4CP`?OONYg$ICewcr=VL0SOPD9=(fbA37`15JNH(cXabK}2<o
z>v{?o;-Yv}AMhd<ASjZKrB))Rx||%A-HLq=vk?IGunI6`%9VKXNH{6HaOW9%EwgIx
zH^=sR<eZ_+B<u$L!Q$SQ47`WFx+Z*k<u?8k$DtL2m>bb^iapyoWn}lqhikfeDN~^x
zP{EiRud}0%JjaK3hW7MRZ%|HC_Ppr_=1WsoEHJRdZY>7U!^6<>(?i9uP}}1admKsz
z2~E*a`fAySf|l+PAMY;}F$^8R;<!J?_hCGGf>|{30x4$H@R>t)k(hHxoi#R5Rsq>f
zmnQ{2i_2pyVu87gcWv{hOAT)jWDuh_g(LRJ`OPA|i8!C(2lWott1rW_SK+^(AfAcy
zt<E8q?gQ(E21?H6MbHPT?wS?f_a^lA(eWKl@C2mBW+=NmMeHh;74_^L!bm8tz27OP
zI%H9+e9Ys^Z~K|F$g#{M;(xX7KbW~+??D~c0Y}HA^+*RJo8-O@Z#qk790A@FQNfCu
zB7+7^hcseD^WFjzBP;SsOAbZnC69DGIM35GBG=$7zRiNj$5z5aofbRdhk^mVPZAz@
zz0wEa;%;(dkCI?OO=cltGe^WtJQ^=nQf?YWeQxGGBzVy)hB&Lt*Ld(|=3>z$=tSb}
z9#R<r_J|c&{OGSsoNAb=0rYW)c)5Yn!APPN0@l^x#C3Y<oD;~adsD>;3}>ReoaXq0
z$0J$uRB_^#m|e;MhT5Fy5ZNOLElJ#5KLw=ZK%@8Z7}5Kp=yB@o^uV_)o0h>YWkeJq
zoUWTgVUA{-!m01j0sv&<eryc$j1o&mRp1K<78~U4;^dL2d&m@Enht0DN*Uyi8c1B}
z0exF3+5l0i_jZmZjZbz~fP<hOoWGclOY~n5W`s51{|7kce}yO&iVO`8E44^SWEht|
zhi(ADU(gH3+emX3kNaO(icTEXQ^_omv0TIoZG`;qMjr(o7+K@Y#A%K&-DXVzriuCQ
zqi!y$D#kEdD+5h~e%VFzl39yO`BZdyNUaguTS+YRfG>Q9W>UE|i3M0dV(+>>NGz+M
zFy=&=T_iFqN+PCL;gAS6%xHr)g!b4d`e4z`PDj_B9iNzpC#$Fk@&K<o*PpLJTQD6^
zLMmf|qx=ERv=vYN(vmTwPDQSSq@--~t3Nb6JnHc9@an6G;LQH}Bd+@~q<??&2fP4_
zb)t0uK%;O;ND6Pdp17HrchJaLIhxl-hY=DG&@C@7ZyE6@gKrB`_dod$7sq{E{U#U5
zHNfA+1L>Nle5t|#!8{o3Ios<@W~B;`fG{33P(54r0mtIOzc<TBb0&4S`ZQkzlQ23g
z%s-}*as7qFT6*2G3FpT;o*N5r1oD56Agnc`V+l@=gHPnK7}XhZ%#T`oOGiC$C-%5&
zSfQ<qAq3fKcaRuhhlT-C);to@4-y=r87{eIm%%f#xI-^3CWdy(XTSb<HGRd7BVb8V
zyKFT3;tp2Kh|8LaET1~{d^}}HU;l0z*;?wrvV`B<-t0;q+91)uCNRAm79ie*m)P3b
z#sIEk23w9J=VfwJz#9l8W@z|9#Y-J0{zqFust5L__o=m@wYassR0DXcsHl!;{Eq|k
zI0;d?TqV|ot0>JB$&5wr0L={Zck$xMWx##2Z5P2H_&q7QJn!!rnUDM9qNGb92S!w&
zI7Eux4O{PE4XHN~pcZTtbW9;uamk0|?zmt8;zBT~H@dvq-4{52Qovw~=yjXQgSP7>
zNr|WadmfwsguVNCg#mcqyAKM0Uf+GIw}&I{zjCv4Noi?4o`m#tsHq*ud#M5AIO24g
znTS`e-$=)}Q7ZGJ+c%-gB$;OaT1gf2Bkn*QGgTiWl_=bb7XQcd<c8<)dkoB?&kN*5
z=4ZQFoD<LSdvls}pIx^_xVGd-$I3}agdn1z$cqG=&QCPAXy08##lU5tzG?-{?h~5H
z79)1q?NgbfQ~xekM%@-pH^0y$Ig#DhxKl-Wh@RFgaN|{z>-)2j60a6Oi=HH?Gs>aE
z>#+h2`MugzO!Bow^S@Yge2o`xT%~^0+gg*4ax3isVj!t-+(SDcyoL9s$=8#=Pckll
zbriDAcb);d!uNsbhy{e%8lnbXUbRs6zy*@v7vGimQ2yfDYpjO%*RWe@*$kNvOTWb>
zz9l^Qn(<IMD<$NOuUya*9-NjXm~XKY0II?PCj8X(*AH6X=S+xh59t*XKWDs@yK)lg
zhM>3#&|CxXN%{E<-ar9G>$K2#@yHeb?_)h804^uS&+M{ug&+G)y0*Jqc{Po;z5P_;
zaeq6j-6zRG5|%vPwsUKdrtnpV`qvv(dI0>T50u?!H8C-{c%OcJbGbk8Jxlz=?pJ>j
zD|5^3O+zA!j;fJ(iRoV`eqm)~OsT(|8`t-iL-xwG?i$7>BylB=hj&}>i6&E2<9=;J
z7~<D2%%WtE<iCqQr#)y3+j=Q-ytQKVIpI9H<@wc7sGW*lcd*gGPixuq!@^E?e`yx0
z;!3@`anOCj0$|4%U!&iU3Q&Pe{et)JSiKZVj+d893Sm!{>wk6|X%r7XgZoZ5Ki4@?
zChOHoDXX6erS4`vl7&v6T;`00dt+`oKYbsHqRi36bwZV84bANfkr&H&yIDbd#mvj2
z$gll>WnX-7rGGU9;IzuwP#oPVYs$M=-ZFJGCH`2*^6qWv)f1Zis)}B}qnS8HzL=0g
zgZmMdROq5>oX5&J<I1XuxLr=v9wZrr(8cZ)QXLZIe<QS#%-+7`qBf+Tko!}Qw)$4u
zCbgwC$y2T~UCYgvZ@^+8NfwlZprxb~HG0d$z#xM!D2jte`g?;1*)^l*@B7$D3r4c*
zYi-k2$r5_`{<?t#*R^OG3L!Se-V8D!#-!DaF4xlv?UK$2T49x_c9sh=gNI&5Pa{+Q
zynZh~q<OSF>RN2rGWI%U2F|rN;0^B0UZt)&UcOVjU<{d{CX(WL(c$!}NQ*|H=1~`+
zk<85%A04&@Y{UES4AHAsLDow<sK<D???;<tW~GKuzoWouyuME#SiCLMIR>|4wEo!F
zLhAn8mbX*M&s21tw)84TraaXTn&F#p`t~6%2EacbK%Pj!6xTuO%HWO*ifuc^Ln%D@
zKSMOh9$&Crzn1Bcx+O93skCZ?>`SJIQB05EJtr8BQR50vL~yImwv0%f=@wV_k1u;k
zUaWQ6GBOeGgO)&~os2^*)hmwmbWIaGe1%b~L_Z#46C%4n7yAlhv{2Bcr{L|qjIeuz
zaO@cf`y*+R#s^c$>_(dC!fMc1%AOm#j!q}2>cTje!To%bTY7-~EIuGi<15TDA5onp
ztJEDsr*+e#gVyGQ;RDqMf0-5(+E=Q#W9>|iVtmr(-zu0nIF!RMh^aunsyZ3}Npw<f
zwiVs2=f$rdk}t~LB`)>8^d~L8I*x(*3{^NS3P(mosp(`;Nxqf=%*a^h%6Orc)^YQ)
z(-WUl`-c4-2$Ft5#5K@6?=kD(3R;utRG3E&etc%+u{Za{5Gz9vGzn9!aWwd~(A0pY
zd^n}^0UhgO4<K4JG=@Pw(GU8Ft$`WC22LNKpJP^DK#My7I_|Ilw!o!<gbBVHwCIoq
zLUS0iW>FmIz>piG(>aFaHT?WNPdtR(+fEcQf<`hPc8<^f9rSaUYYEk4ahpDdM?3m1
zGop@5^Sm@yIXAe<#YN3O1k}r;{S?rnfy;Q(S~Q;Eo)jB-@zbz0Nj)BjumYZI@s@RA
z^Ti}K111Q$ovd-3XMzAd>|V+xl4pL6zxNjx_u7Q%aBl&^YysL3Fp2*}%_s$MFcX7#
z*)bI24yJOS=#9?%UG7!@iO==>V-ZXwU5LL)h17llq@rbNIZ_V*MiJ}2J@K51n)*a9
zU7X_y&~@OuJI+)wj_yze<>Wn2D`>?DHPip#iPiRK0VQJ_^qWvyd|{;=uwK%zmjD<K
z`B?I&xyEfl7px`!@Qe#9hLinkc(NBBF$KcEWSJiFty&}p^9%XA@83^wJ+Eq})98;M
zO=z_F==bf6(#{>A`i1v8b+A!TH95ZU2Vq%>CKn!fLA8J|D1?PZd&KXB1w16~1%0-j
z8*P(vMW-Z~94xij4jNmElEUtZJ4XlLlrjV0zom=QeS{Qbq(Y#R7gNdrO9X;@rGzm=
z=g0w7|4)##U9jH82Yg9-f%^Sjg|5CG3oKfh?!8LrD+S5#B4Q1(Z!3m+IvIOm<ouLc
z+rQr>8ibF(U``p`+a4#Lyd*N08|f+o_5xSHEhPt$wtC#&T$VzF6q4L@;D#j3#{H=~
z5|=#yh2segy+mUc-4G4L0XoYB-me#oM3te&_D5@dOsJjOPo;-4o)!kFm}?u{U$UoE
zy9T};aI0e_p%Zk^sUz$qdq&?Ui1ll-f<1F+7x(a|^8Ne|Pu{R_r+C1shi_?qF222Z
zADu9{tN5W|DBJ{N27q1TQ#_uq2yvJfH4OC{3<3>LSs%Df&`nRn1B1A9r}g;N#jnp?
zAg6C=TL;9F#@P=rZ0ku;XM;CH3Cf%nn{A`XpkQbuW=#{8TxH?EHRnLSWK>=&p+La`
z=cze1X--9{Vw&NlmoTw2rZr_1DId4}%n7*j$+``)jAbn4Emtd*V}8urAOR>j7cB-B
zQHHWwT%cK8P)|sO5VjZ1Fl4gcXB+D!j0Xo{%UjKV+E6I+x8I-z!Ui5&4ie#4r7mA*
zrmX_p@;C1(0GCvNC6C^z@i44^4xdR~rlso<Io5PFco#P9R85|~o`CdhioyJNR-%)#
zul!KQKu0zwPZkn#tS&?B^e3Pw4s<;~5zpZ3P8xp8Pp=x~<%cMdF~ITmZ`-AJOx4)!
zO3NJOwS3L7_5DOWvwQzmFW0E=38D&-)=&FsP^4A8Q1f|<v=l=Ur~MSi7mdQC=I$4$
ze#&#5Dy;!jG!W`yztE_&@N%0KKcMl?dRhe_wNinpipg?C7PX-HVv8AH8?b=N8gC>_
zlE8ZLh3P#i!>*{71frT{ZETJLB|L-seS_v#bytA?E7)G^ji=T)2w7p-Z@w&<Hx&Xo
zrdr#lKgUl1p9<dSp1nkPuZ->(l0vl9Uj1wshVV1Q&e<9#t!WAM+o}hf*hJI~;Tv~>
zXuZlD+FF7q=>&V-aEq-?WXkP(#IHrgr7mngS9l|dc_b73W=l%@^pC1{2rr%200qD#
zAJW=Kj(87Z$+srL3qo~5e<1-rCDW)f><cm=9G`0Fa0?0P8>OKQM~Tsc$>y7LT7$AT
z4lz5TQ+42j0*7o@8YW$;sY(=l&TpWir72C`qcW6GN#Q?h4{bns!>|pp@m^2_SJncN
zlPri8(REYKM!UtYNy9=Bft*kc9oFjuibHT;`5ySG;5=;8-<=_D`FNhT57cBWQqX&m
zcnq#-&Fi!R=cWr?u5;}McdFHofIc|IE7H$Cn&!GpMPtzY9qoePTS01VA|C6w{<*1v
zl&nHZw8Tt3%AlYku?dt^c4_6d3eXY_L`Vl$DNf&nl+qipe>Z|UECq8d4#pN?Y)Nkn
zAEIR}RdT`1tcQKi=08`Mrqp~R5VkU(fLL2ATPI9`XoC**`^e_-yB4b)tQ~+R&*Abp
zU3e($FN9TZMrB+zk6Z)Mm4Ab;?DP%rKDmCou3Np5$B%bLbYa~ao8(V+J!<UyoeJw?
zzWp(2{(uhSj7FV9qAakT@C^*+^7X3uX^Ls9O5j68e5g{*R&^?|`G?o|%y~v`Snzp0
z<UR^5>qLfxuao&7v4^|Z2DG9*^qp<j7LQh|49&dnwol2>;Z+9__`blWV}nJ<Ed4#a
z<>8(cdg;S6c8!9>W;%S}NO#RnW8nINs?WJd!Ig6w^uqk|E*EgbJL7Vd-P5=|W>=AE
zx59|eJdHWwO_foMuhp`WS;>BJfyU=m=1N<SVme4te<Q#|6(oXSHVfJ<7MJ!tF)t(4
zkQz*8|2S5tr36~d261?HZ=`DED~sa4aFGNJLP<TS=j*t?@_+Ncy|&`xU;<h8qqoSl
zWUCV#qEEU9vgGOWxJ0yifN5sBsReG80H@RBReF=a*>`-rY4Iy`q7FXa3wQkAD|K<Q
zU5i-9%K|>+jhJ__2}xIz$dC(`VEEOMwA|FWm0C~d^sgby0T64NW{|T-R%)5E6S*zd
z=?B6v;YbM((W>Kqi3Y}l>2Q{{J03qJ{6W;y{JBkA&u>6&!kfV+`2Fkm@q#ojVZ`^R
zM1fy&?xF7AA4M_#aSU<y_)4PRquUu!Z_>YUgYKTdD#PUEIGPuu8-DNG$RZ;Rjf4ei
zyp6;`<ey~@S^o6}x5vYzdr>ao;AmQHu)kC7iY9D>kUZPSHO6R!;r`{4!2P{fd~d=|
zzP{06la`;`=B9NSlJkwZyrP<8zIH;l*ihjdQ4t9Yuv+PiB(C47Lr8^5Ps6NqsC+8w
zn^+Ocbi6U7EpB4il23z}&o@5$=Ym<Mz=tk~PkJ_$GQVA01W!bCQeNR%z*Z9K7-^HE
zdzQq+107-aoKAh;RMFEC$NBmKJ9>EUMq$Jq=%th3d+igbZ*r+a=yQd(7Vpd&f&Oa(
zP5Sskm%d5_<}t`3d%k0}>v|f5{Z0S_L#8SH0bonR3$TA#&ppEY?Gg|xf~b?WBX%Ys
zZGfk8O1|2yA81V1U~zPq@xh{F5W91FdkD3g)cLgFIsC%gx|LjR&pDUy_xG-bpWP;c
zl<-S7TYkC?IaUM)84zV-&t=Wh3X@nJc(SNuhcNvPx`#t7s6HU^N!9#t=jmhUj;biS
zFvlI2?&wbRv2!ViNuMMxg&PcZ8pME+`&oV^w~OljbSeKYqbEM|XeK~B$rRy+hfQhR
zs&AD~^Xp6SB~YfE)Sg0m4kV5e5oL?$=Wc}E1$B>%q55>oQ*A&tlkX2PP&Y5|U)RG#
z?7n9PGJ&E?bpF>T$^GqF%*@?zGnaEXAr_xZ8ORYvNkG$7@eJ>EVvTED16GxJ+)}c4
zJoBkiLC|~FO9EyzcoOvyc7eR^&g;^TA*bFVf^r_qHdU>xepm33AFrASMt*HqFxJ}1
zUnlq|yCaTb@E62I>;<LJW9mFoYUi)amW<k(<ZADnd(-_r)g+?|Ow7L~=Ym^t+<GmL
z;YjOwpKR$yictl?#^1U4pe}sB#&P@3s$C`miun+Zf5q=I$97-#CVfh{#Le^iGyg+;
z{7>EES-6aJDAK&DgKgp7JJ_|$0vsN((D^d(ft!n7-;i|rjDGranelzHc{Q<@UtNmr
za=NGvZ!dLmyZu>llmARHBwM$cZN?2<yekqn4k}3iIfVzm6vkWGSR&5$;<}Y|QN|k(
zcS9#mY6+tHAEndb3Um1?ITbw3-^k}~S7V=PehlVLp&<#3dz$|;axVf|;o}l&g()hk
z3NLOT1H1(u0Vkn~XiYCjyA7U`?-fO@sVP1ESysplUn~2T?#rKDj)eyX3?P##hPPf7
zMqN3`z|L=&X-d4i4Eoy1uNALc?&mA(w^&jOsf!uyQDqI!?W@ua;XY(P-1ld7p>D<-
zc(JjXRMFlt=dhAI?042V2zwardbZhZ#f~i|l@IQ70cmp~K3OH20q=*Hw;kJhb*>g+
zOi>KDL7C{H(abu6S}^L9-=j(cpk-w8@}C2pED}?Yo9}u&lT;4bi(C&tgL|8^m6Xh8
zplTp+NW6NTwjX{L?D&r1$q@*PdI5Jm*761|XUY5f^$&9MN3j&oO2{dO%`$E=kyK9i
z7ulf(ffyjTXxaxod{v;O+DR08W8KaE0Q|_V{I#UTV`q_Qgp<Mk4ZRgyn$thB3H)|-
z8hkS9;B}f77c<;s@S{vs(tdFGmO%DubK10SrFO?B$p&R33mB!RdZQmIG{DcFE5Bb*
zkrW3e74wml%R&6!8)HW<M0!+A7o%CnKre;Cw22mfe|&uybwo5p5UO-slkG@$==;V;
z^Z3IbpOvDy8H>iPu#kk`v)W&1y5LnYAPUDMD=@0WTJ7WrHvlHc@ngy|QA_9f`WKiR
zO%2;64P&`8wWYv#rf{>He?;Al^4NAv=DE0XxvAudxA6=--iNkh6ET<7CVU2E5vez%
zU${KiGqDbcn}Pq5>*a6j2D3BlR+B4|dd=(rv;u<+Ai#TywVu;~xZJ&XAD@d;6W5<w
z`fbMSlb65q`zKbs4{Cb0D}DcpDfgV2<AOUw?P&5Y8)ia(XVbJb>B^rc2CfipiK#^3
zMbWTne|p-@KmPT{+p8l<na;Uo+_93tW;#0#<lRX64ChCv7D!CIE1BeN?+XJ-VMO9f
zAp#>p-+*hOFs@kQOw#<=o>Hd!Y*6K13jTSc<xu*UilFL1+~#4T{dL>Gl)+5&b<lNr
zS5>6@A1I@%xa1x7Sp6Hs38qq`QIl7C6|3-gHn=77?Vq2?+3(*c$6A49_L1REWmo%s
z^E6rc1r&UR@RWNZ>!_=vJ1)GLBI7cMlsci8S(jEovG$vH;vnNY?Zb!uR)kDi*@d}H
zhpwo(5RA2+=EnPW9o3M#KDFZfJ~WliTRT!rQ*GlvZjP0)eo5G$`ysr`^{~M2TY0CG
zru<y5j_F}59s)7qx%^bb-U{LR<);-KFXJHl=;3}_>>!cd44*xfR7oFEl6stacmoDt
zEll`ij2U}9V^{h`F0Pm7i7Ifhqze1iYfvT@i6!@XAFs;-RTil*A)QT+kiWxh_4uSY
zSw%sLG^X2xx1Bx>bW-t!74Nc9?KQ>4GVFsR#0`|DZ$Hw!`r7ZJ$;POI>punKR8qno
z@V<N~Ef}KRX>@56=TSyZK;`G%-o@@ue#lHp3k8p@WEQlW<qfn~IfOK2e(R&u=V;M{
zB;1zKXc8QB!|wtxb@=fR+h``<$Xz$Q|0!<yV09QI!+G6x0t0KyUrkn=+(#woI4g~D
zW^>XQne4Oq(YfqO%+YYlOzcVP?WqdGv+2A(48gQJ*Ov1=A~Oed0&H5h#Z?H@*}IOH
z{wS;5O+%YfZa&fJlz;ZkaF06$6Rsv(w(5<{^Fx}Bs}Ro`wXUd&uwJSS87728M$!Ci
z;WicJQ!rmkV9lp5b=3O6f`HJ;E+UwMg(=6Hg8PWYPj`uKS@(c|nC?c!TLNtj|5i*F
z`}6g-7A)37K<=B~2ID|7yA~3U5U&)bq%4QUy$W51%#jAqeE@emlXOGqOP3=@@Ghso
zAHKy`Ij>!eUy&3DW=Eh@O9!Ylzw+kI!^G$1WW#8C(Cu!L{+@Mk<P<<j+mosr>Eie@
zdMU#XL`fGq*1x4$++6+H`opQ?VQ*jb(h)IjlPb~%UYRBj7{D%?48rB&^{VaTfj%<7
z71Qm^#6EK*21k@K4>@16`2EXR04A!1s_Wms8a&|ZGNbP8sug(khF&pU8K2XI1YQU+
zhe$l4M>-BP>pWIKko4gPkUscmq@4{UeVwFlRHu<m@ybB(k<x=+<tDCtU-3J9nK?;$
z8_z*6%A=J{Ru?<Ch%AU0QGPw(K1#iIoVOS<rW(-_jHr!lVp<S;0=_*lN~^0yThv$b
z)Oe_jS*s$2(;YsQEXS4_o&eFf+BqPWg}8wFNpQ<Uw2-BfEcItMhN!}E0%B6=QSrR(
zmzxvCY4iQbDZvj?g%N_d6ljgn;ZVs(H+3WTQ4ODEPz*aDx_bGkZ@AMhaf-LTK6KQ^
z)?XGlxRwwJL7J9&rP#ggLYRRP8?({o(_?|yb-n}2Kq`5$X>obJ`G;a~>hptrmQQa{
zH8o17N{Y%e7C~=^Pn(QuZLpJT-Teeqvy|hsxbxK4@CcpR^Cg(}IAw4v0=r+APVz;t
z(EGdX;Ie<}7HJO|#3xn~(*7K@C{J&O_4N$N_GgXi!jc(GRzJR5lHODFB~?}YC|sO}
zCeq!9(MHhy?&<IE;unvOkpG3^$<f!a-DzLdsjq}75e|5xASqP^d}9|<r{Pk+5)-eA
zpc2`a`(iZ{SFQ%9?`rG84*Z#wm~X@6O}o8YTU+c^?)#h(gJ9PU+2wrp3P9#e4PoBn
z`oz>IwG?zKexTNZDuUU==rr-95>bS+JS*7bzc=7eABLif?csZ3ggg3^wOqgYS!y-C
ziICx%k{NB8?8%s8G9P8MybnN7#~bY!s6hBpg6Kxn6Glabde6axg`|7NWRTb!5cN>T
za6~XH^$hL?*IkJ$Qva!F(98t>eGmUv@!Yl;hCNaVp8Fes^H_gQ(zy${nw~nZ_2LNl
z-!y{4L0UjuOan!I^Y<QLN9VMI66(U+&S{q4-U}32%0dgVMBPU#ZdW=$CiI_B@X57H
zN6~ukt853+?HwJz4FL72WNsC4uZ*cR%8x9+zS!2(NbrZS`}ghu<kn~-Vp7W%qv5h+
z5C-|G2T)TRnFHOn>(QF5ubmykQn}42-5343isrD!P~v|sr4-2(8IMu++&!{0_5(El
z+V_y$XWB~rvl?-^D#AmA#@Sxz2Bi^kIe!rJOak-kFFPRw1FoJ}3U+{Nj!nk#asH*T
zoDN^YtjhFb;sLyql)G3Xf3$cj+aHN;TIVc@{<~@k6G;&x-#nF1{|asW;cY@FO{T8R
z{i~K4+_AijEmd!35!tJ9v!4EN3cvgtx{LskY>>%udG6`sYsa1adx*Yr5w;kDs!!>Y
z@cb+(s<0=DEQC2-*-u-HoHNfQB?}^3MuX~tyfvIX3)|8iQ>gA~&~pm2H2{?{4UnzZ
zsTZir8i_CbyG+t<09#4|0ET+e*&AUXt*oJ;q1m)EAbj+Q2}~+)L1W&6LzhIie`%??
zmtWW@MuFU0r`$AD0jP_R%vDrWE?9?t_Pv$H7f%)Pt{8mJ8}*kv9C;o>EI<+1G6ICH
zKY#xkt$2B2tJC07{61p|0LKL+md3`Jc<hGy8aWaNlhqD-j7H6kcr4m+I+-M3$G@Wt
zB3Il0S<^!em?NQiE8_nFmH1yvO#a{hwn0{$UZBG@1QdEY8-(t5c6EJni1M#&pTG3(
z06^p#kQlxN{*6sLu6Bp1e|Xt=r#f2rIspCmf~d&B6T+)!kVR8Ie*9RkaQB8Krp@C9
z>H|o@92n4vlMI_)_JNH)0mV2IHT3KukRFJPPuQW@0Y}Brec(-eRAgj)&B9ChYNtiE
z{l?=r>czSm#Wj<XM=M=XOg>s*v18P)Eo;2F*sd_?K&}G;DVO^v2`$OFH@iSGO$T(c
zs>2pZadEKYjp1YnjoWB%8O?jHW+3=Ye||{q;0aJ?Z~gvr;CI-3coTCLIgGT={rItF
zaSMn~$FC{8*Kkh$y#M*z2O1bu^~CcDfOb_)P{<I{u^i4Q4ypr-=yr{H$dV<11G%h*
zNPjK1w7fd(B6J0Fk2m=DCoouEX#R?u7Iaw&8BF6-g3?R;rN)b`LYqLeo30npD%*2)
zl|}k>s@Ipx;S@k>)|f!>yRQDj=cU822PAZ>J+WHDmG;wIkZKtiIH)>~mwmB7l@p+w
z$`kGYr2-3wt$X7rnQFJ^8)|?OPXsCp&-D>pm+INTud};5?_OE|6J8*yi^|CV`fGpt
z0}5e%;7(a_x1am*N!|j;`<T>tt-r(S07m}vt&(uORLrAKNDP*nC4WG3a@p;P;_=ho
z$IE}JK}%6b>25SQ40!Rmg=|UmlYJnHrv-@lyBHEaOB+EeXvYe}Q-V3mu(LX^LsKYX
zd<kO9I%y=AO3QBvVC?8VrC2yaz!HKzq*G9CTaagOq9GIXNCdQWAIPHCAS3!TKVq1+
z=kHCG7tfR$>Hpq(B!oJH29Ga}3B6QJ2kq#%L`3<E2`a#_fF>MQF77EN4ruaznrSA@
zqLeD9#f2?Sa8|6qt(pk_<ilJ+!cuUK*1cYA*!09fwCo`{c}d3b_%2s)LE?Hl+d5JW
zarD`Afd(@qL~hnp511`85$82@28ImVolLlok%z*E?1m5b^zsylzhcV0W1@wo{j=k-
zzXKy%k0nmfZ9TeQLZd`q=LMXE$T~*7arUJ#(5ldIaj6=m6#(W7G9X^-G9fblIr=$T
z`l<bzNj*Qad&mIb7fs0KpU7Vwh`u}FBB0I*b&LWWo`+2XP`g~~>=XirNf<nPchD>b
zY86Lwu|y;;Qzn-<98n{?`&)@tD-q3Mf{E11wA!8hj~DnIb|;<xo)tq}$2oI!xcFo1
zovCsr>;VjTQoN0Wkhr+GswEUPatUea!k!uf;fqabd?5=9K_gf0=DXXSo$euz=(@f_
z$DnGeo<E2YR4NvV)(5f`jRO-8;FuHuHVQvhd@i}_+2sq!xVah<8Y&BdNef_$VCwiO
z<Lq1}KkJPKuVB8SoQsqyz-``p(2RX<N#EQ5?~vqf_=O-a3l7n~%!Ek65F2~wAXSbl
zVhqVH^Bt2d8Q_CXYRL}fKq#CHR=j){OVkult)3CU1R7?*Uugm)#<oH;(Zt;P3qxwr
zXLY7sXi#IG=Eo-|Irr}8k>-OAL8I9mseS*$5zRZG#^80k)wAkna_YLIe2)5INaM5)
zl{NwJFl~s!00IQ{K;k_7RvyG^<()O)CsMUI2eE^ehNXXN09=7!mI@0JT$GA6k_H*4
zB?dm{N8lJ^G;FLbKu&$PEA$DB<R4$u=)D1fGk)25ynJ#7ip?Um<a4AN2wZ2}ttF0{
ze7>;oaxL)QGKZDBr1T6f903}0iVQ26&&kjmu$n)?T%c)ITko-3@H*%f2#+oZektQ2
z9+xfu!7h7sxm&{|L(1=LtQ#tgr|rGpbT(#|y(UaI+`UQ_*ItF(J@r(p<`_?iB4Znb
zB1w=f<tatb2r%myHN6=`>!_vry!a5BWJkXBvp;z<6XfWK>ZSY+TZh^0zIO%IJS*n{
zrdy`Q7gb3k`9#c7CYYDxq=q}I<mA|nyAmz*LUPS<X)w?8+WesTHC_4ny>M$lGzq+b
z`MPpv$lVn(&~wHOAihq_B!P9;vx3TAG4UNlwTx<cGJqhxBO#OSW+n-N;6Q!4t+6lE
zz`UiN>~pe33&lHN)`OIPYoO(^nbuzgPrnN(we%V-_zT)`q5U7Z-v5DT(y9l{)}PG#
z5{^JO<=#*7$-lqu33%?MzgHJ(h+So^E@fw9Qv{Qhz}F=%=;q-36vkMyH)aZZu{|Lh
zPbCQ%{UFlln&1qo*9uX-jEsyI@8@W1T7>Urss4z-^cdg@FKk^0l7M8utn~sH!JYfD
zn6IU*jN=dBz<@^V?$Q%4IpQh<S>`gH`s9My<OdYsK<pB5N?jCxiQWqbWeP?nb1MvD
z{e6CKX2FY_pGTkcUhGbli$nGNpmk5z*&D#3TOJ(b+rWpP2;gH`NdKPuV{@+##G!Q_
zyYHbISAc0NC2)`B@eEavlS=?@FU`G{TR-5>R6XnR0V`4In1k{D3GuMqp6;)q?IvjH
zb6er&c}9PC7c3|c1Rk0%_}%=mdKX!!U7pS3Fryw69ITNUY)n^(?cQ|QP4XjC6yXo>
zWsbejuC$DC-x`yJY>83q&2eAUKKBD!^Am_FkHD1`I$+c;>Nz#hDhdC|g;uK=3#j_?
zTB=n>{M3{-po5iiea`l{T-RhEZmu57a2o*QY`z!2Rfe0aN2qjaowL@4KN?+}?lQ|K
zv8c=ny!5S}^KJt3`&-aqLKG?0OL2DhZV2}JVyr0uWlc6brg;f4fYO5}o&=WEe(+}v
z{Qp9;q!5(eMMI6p5w}E}#EI~Q>YbbMnssBm;}pLa3M&F22Nz)3fv#(Wf^r<JPPve;
zpuwagtPk8oI-qSx2BEVYSRXZvC(F&G-+hUSjZFslX+HoU#x5Wl95S5ZM?44d0+NHE
z=bn13-TujR|Il_<HN~rgK(y#X;yWo@ycI_rw`ly^&Q6JUkqFT^cH;%_inPnAW+jy{
z00Nf)me+nr=Kvnag|14Q`sL}a1wi9m50*rIi)ET#p5y{;_XTe}u+p5c2jv6j=nDa`
z?f{b%eP3T66G=GC{o?m8F8594@urc0jeO;-Ahgr6V&;m=mo60g&t7aLYD_BK4PjUC
zw8^-XTCR?w`nBJ&>S-D=L(tC`kn;Du&VWUTEooF)tN1>DQY7fS6!7TQQUtcXvGLhR
z-U{=(b3==N_cPk=Ph`*h^fgNjiz{rEgbqXcbC;W_j9-L)HvAjed1Hb@z~h={a3He)
zITsVGEvO`XGtatoQ`RxGXbwbnpFm>9L=r|tgSrlE`Ro9bt!c);8;pOLkX;|Sb4$Qs
zyA!4hdX|8|>4&W-qn~-TeN={ioog3UW#|rmwHqB!fZ{&@M!o1}I<kpzfa{|Xu;iDz
zV0B-_P&_jvf4NEd&Ud!Kvy{mzC;$^lse+3BpR==gfFSSy9ydBxd>I=LN%V{VF^G`}
zc{MQuN!L@w^DxcEu{l!FeXjtblKSS8IKDKVvZ%2|GyCf=nf=uDE(9RAFxn<pPn00Q
zL6VCz;DK#J)XvtV{@aJ8$#(;@A}lZ+*?<XBieL#H1cZcyY}$=dMva#k2x|OZ0|cV7
z8ZM~Xp<qD4X&SQSA>gtiLCoiv4k`9pv<ig0kMfz~Q{)}M`nOenakkGski_~Hm}v_Y
zV0p-!pr!}YaV#UkU|X`FTp~jjlVmgCOKtn%&G9*<NxOR~8!Rm^FYJGIZr{!0*+q3f
z5d_RrvD0Cz9+L*1zwEf%6A>9gDm->|HGU(0I{6Hsw7VQslb8e1wbw9}7XZ9S5h=*`
z40uMaaq;On!yZUVyae+$R3cA}EOY8LeRo-w_P||2DVZ56uo3r3T+;zsIs0=tKLj;R
zS`EzpdndZ@Rv#B5BKojHLsa|0pR19*Po-nYHAdNC!FKA$=8|`#@(9tRnC5PH9{ul1
zJ2eAInYSQiGafW<yb=BfRWk;Byj2-1^n>^spyw6Z1XAW|R_FjnRsXYo`R`$Q3ZqF0
z{Qdo*5k*5sw^f6!qM5my`5=pxGv$uNcRB#6O1U*qK1(LNeG9o;L2U~v5XM1k*=Hp8
zsj!G0?x3&#sZRMv(388PD9NC&MH^!V>gnsJcY;C}{^diYot&A}a#ol_&ctwf+S(~0
z;o*h;I8&V5e`U2v1if%KlA4&SK4n0RyaY0Ah7S(TVcWf}NXW<qNXan{|0L0$Ap!DW
zTY7vbcPTa1#vmLCft#ls@?8OmHCdZL(iDWp$WB0KjN*u~0*%t%(IR*Ogkb*c9kY9X
zS6oxRe{{3ia|5<w&Ua@_)qnk;6e^HhyM^P#);))tu&=)dK4c{oB}&AMgI;GdfX_eo
zG=}gbRsMb+!4;%4{{8;z>@|dhKD2BGxs?MuwV5G2+Hz|_wZuk1hGwrlU3mN{gZtmT
z5?=*q42^+JBPn(Det6_DM2g;R1nsncXeYlVpq41qIzQ!(4fg*JKXR#I72Jq+06}V&
z0XQ7uOx$QjBXp3?%v}YQBXS3;*9m75wpN+~@<3}I9gWd9IEb;UiAPIrEm%c4lTec-
za61VOS11184;MKz^w?>EbPAY_?dR(?s_mz#3N?%SZS4a^TK$+_yYefFGl43mkU9lW
z?yMmFO&A+f2Wrpq2bh@TLJTOFq}brNxX<oRegyn>ZzY!9XtM-`RkZK<9{UTdK$TSS
zkw@4QVYEAsA>8>9;Q?O^BP;9kAETqY^f5y=#v+$l&uzd}Ox^7rTg@%m=d)sC<uMBi
z4%}l#b&0jV+I3K$>Bakry(fiyMG+;PB^J8Vy|`dzEe;*kqv`{r+(Hg?3o^WBbU=c8
S^=$I%^;d$gRSW;|`TaMCh=Jk&

literal 121315
zcmcG#byQnl^e<REcqr~(+#LcGmtrjxcc-`$f;$v<TBH=I;O_1kti@eY+zAk5`2OCT
zS#PcRcP8s*CHJ0l_TD+!du{uiSS<}DTr3K#SFc{-swm5Uc=Za!?bR#fYXJJolc)`J
zz{>}c=LaR(SCx}g`!6@Bb~5TRuU^$8VLw=)z1(BEDI0jcdWF~b-y3Pj^{4f#SI;FX
z@-jMprbl^bAv%BHLr|yFS}$J_%Wvl27UJ?2LRVK~SMQq;cD|qE;z9vfKUr5-^Tuu_
zpMy@umjfjvTG~(~+@$Uoazv`u6D~VoD-cOBF)>s3&FjoS?_gabW?>Lb(zEs*^xK`<
z``;K7c5$zR1q;|zA273^$ZRYdx$#40a67n}!h6|mYf<=mxAzw)QOPHJDed&zqL(FG
znu2$nWF^Slyo9ysFRvgU!dsK~KGw(r*=cvFsi~^c7%tm893=k{&glm2>~Oq%`w)n_
zN6%vB@IvH(kr!X=j$L2M^9lWfMz<I?Ij@+Q0l$F2aSxF~EbfJti%TUDDQVVN^8ZV~
z3}|jUiE@kao^}8C?+hcu!&7QMSu-Nq<!Bgq#O_9AE_1%?;J--A{PaK45^@)y_3a75
zqgbVVN|_bjr;q+OG37_dUt*G}fKt{!;d=rKvC{vu0Clg<O}5fl8<R4H9vd6`(UC~u
z7uF8*MY;sar~iajc%j`-`#+l%wp(YebLL$aUi9!XiDIY3JV)z4&b0prXZ8ZL4?QS9
zSXHmLCyoAlF7<!xUvWEeebX;nWrbffy}Qi+?T+OCF<K91`x&B5r11WKU7u)V5Z!G5
zZQz*p3!xJK=@-e*mHrc_!2TlLf8+m|`5KkjE$%z(|GF^4utfiF66vTiXu1FQfNIz`
zWR3q#n}}IP3+c!IUJ;i{_kZwJ0qVSP{ojk20PO$&vXTE*TrAJsxx+5DJLjpI1(&|<
ze=hqU=A(b*{=bwxq8ZTqx=PulMsIH~eHiu*dpCs~?6KMpYB?+5@_1S|+WCzM_IQpD
zxHESzR9n<x`1)CXeDA+~_^>@PHkL-H33~4jKjDH(A<FY?w|X#W(`h9AhjZ-m*$(Hc
zvd*`M#_}O24q5KogCnKtUw3jnCu^2hJfV_41$t(Ie`vNBtdcTTR*Z8%;94rTZlswe
zSF^jludWYv+Udth53(r+W2!rMn`j<S9AJ+fs}|cu2GY(<7M@3AR&3p@1!p1!OpOMt
z=GS%QTxX*T-UAoGb=W&S`*#-!1a1QH%0^~dmI@T&&Yl*r<EuKHeOaWV+5(q5H3oMV
zV+RZdDW?rx9C7o94d{vASyk1y>!4C~VMoEwhyu{VSs%aWo*Dvv36;)bewR6nK^s8V
zwBqqOH}GcB<LM4IMyzLSF+Y(fq1X9z?+8Bl^~1V10+Zi*7|5~pV!XQpq-N=5Rh@=6
z>%qY-H%pGYUmPn_wze#v$%$<cA?Q!chwT@G!?G>s;Q@CSrwj(orSe#D?Y0CtUBBg}
zDD_ygx>8&l*^)~R6HI<a6@J(GwuEZjS<~>tg)<XQLOkktiDjKhV9)%1N1Xrid#>f3
zn@nhX%QvmTDxOSG!?LeEuc5740rztvt*cQ9vBLXLuRoJ;r9R#)Yu;b&GcGPJ)_(fK
z&e1FG^^Y!3+`D$m<?dpa-+qctY%7M*`gp0K)^>uzVZF1X-g#N~(|o1#d`U@(N>|Y1
zk#Njg5Txy>_OqmmjEu}&f_`LVB){Ect}2g$!k`Bp@=%_1a3^stU|L{ORPLk`;gTcl
z{N<60=!ozLbYjyJhE@x{v>ky$n{a)4y3@#=%t(T;hoW#P!%l{J5!#KoV?fs~;^C?D
z$ka62CB<=(u#>~!xSa+wZ0l)5AuM8Mpr(dKBAbN6=9yt%j%r_dU4~k{6C25$qe;y}
z^>-~@vTNdUP7U=7WyM-0&{l&bkF@-_^|4M8)eO_e8`oUUMK+peLmMT_u}rRuHHp*F
z-bn17+A*H2JubuBU=<MWhfjZg+u$}7Gnqj!6%A}Ia%zt^@=W{}g$C_cHq013j%t)f
z_J8CZJ(oJYYqXW>^YmGA_@Sbz8s~MoX7cho-bg}j(x0CYnu${P_LSl|U%s%vu&Uck
zHXh>XdSIw9Y^b%gfv@^zopvH7PXxRVbi^)(8IP~)Bzu#9W8&Ti!!ZPm<w_@^7&O2n
zmH-n`t3ez{yOt~~)yT+*?p(dg>QdL>kdbSL&A`%3$vX}|Wbh!DQDZtRAEJ>ce!%*?
z#Q#Oam77Da@+ue`7y2vc@qVT&I2gQNl2vTc7xiQo06Om{^q5r_x$DY+)eO<39agqY
zK36dK30AdVjDCY!|H$)64Ss$U+4B52s{JP*(~RljJ_h;jW}V^C`E%42Q3|7vdWyh}
zMn<c_eP{YHt*=y3Kxr<PuI@*JZu-F|V<XPKOybc`mjfm`wiG>o=_L$WtyxsIy^<ai
zLpXR5<)iS?+}zhW4K4b@#F;$ij^vv@-w52Jh165Zt)EqGqLGl`C(EbV@*p$rt}QGF
z3!F%K8%MHCsw9#t@a5iE`J$)w^=~0z+P}X{jFy^QIVAmWlI!QvXD)|OM)51HX7?rr
zS@XDG)V&6tWr<bbe|@O3EL>6I3DZbTNijX1-^_&!#AWaY4ysuJc(NYa0}$h1g`F8*
z%z$^$^PLe;_hyAkngnU+rUeY<4_s2qkq!=ZT4<i8C{w2pNj;HlRT(3hIBw{l4e?qV
zir*Jd7dN&ZK$@TW`Ce{+rX;Q2ab6d+N0GYag*yo`XmpBH%M(`Dt<+~mi;R3DZJmQ~
zwdmK>`=wXq(g2D+7`LOsc3pI7JKi(X&L`9m?U5SmfUhlgy^#V2o?Xpq8oB?iG?92-
z+SurjyG4RhGe~ONwY^0e@@(6TqAVBnj4;}$$qfXrK|z=C#(tZTH*F}$cvZ!9)TnJV
z@Y5qX0Q=)JRer$5$Oj}+KQn-+th(GLKoroPXxicbE2Iqd<Aj|+rvL6Bz6WztcNc>y
zg*9ZS(Q4KjCTs+yE7mAE4~rrOZKvlYP>M9(F)=dsb!B|lkv3B3CU_Ecu&N1Y{;8$A
z1-`6;$sap1k96Cz&2V#!_|MF!nfexVPQ^4&444|^9%h*7k}RFQ`zto5ay^_fp}qTU
zaPVx7-zQoY&Dd*a$0;SHD=GE-`GLhV!Ed=;Z0PwNnDy0Cqk|1@`y>3+mHnIUli6>)
z-ND=Q=aB{X;IVRpx{8_;;mnmY9K%MZH>+*l0)wady80gnLV9DKwxRC)7QOO0626_~
zMx^4!amfun$4kSL9gsy&5vPUj=XYLblivy4C=;|0*YhbCaOvkSzFk@ummG%dD+0Y}
z1Pwzcf!0ZqK4rS$uX;o;;M4rz3mt=6YlGpl=A$FSnHnpM{aHJ`MZ41=VWMjB?y>&I
zBmoD@z467H4%ByqI~b@wO)e(Z1%WrIG^=en^@d`hCV6*9&rhe%PG&+8D!c&Sek(7;
zaT8*`9|ILQxK*r@=A>^c3WN}{Xw-*)GY&W(xB=7p|0`@vGGTS;xYoTpZdzvtG#3Qj
ztKdmLxIu7&BzDuacU}~vDZJwBu^{*<W!@}!^=8+KGGD}0Md>Z_a`K5^CT;LTjrwKR
z(|uW>bazntr61S2-!|@HENZ32T06L8w>OTM!}@${;9?DOYQTr>$Am9+Yu@HOTdIEC
zYJT>hOnUgYqV6>QsylGw57P>9Qpsz|7lna7=+ntP$Ie`nbdNwvN5vksnI->pF`%|6
zfG-}y-e0UAIC$894YWSpYok~exc85`_za4k&*g=^Wi5j*Ir518`xahK+W98@Rp8}>
z$g$l_P;u0^(GBk9vJVAHV}GUA8_mlA&42Dp!JZbOYRg*2lb4NleMDkK>R+tCJW#6y
z)X>sruXA(M%i^6<RU3USw-#&g{(V($(xPgjqNdXw8U6S}Bkzso<2rUDz6z(-X0FF*
ztVe&hT$%CR#=iev4r#of@rD_pB#|QGxGf2XCZJTP)UxpBPyHwI=c@LHi{~SOcw(%6
z&q*m0Jdj(6^T!1Ae6T2z(w&&ambF#2lwJHjsEyljsnMBv-Z(H)&~dF?b&dC)De!4P
z>m{-%yz#dxG^i`}SoPLZJ=CpG6R%hiI>Y&wb=98+HQ=MgE{=Z0IO2fidPb?>*L+^2
zHt80NCl?qrr99o5)7EV0Wg+r=-F1IZ=7F5}cm`rX2eZ7mj@n}c2~ID^Y1eZQBF8Ri
z{-m<)q;{Pe_|ES_l*H!a&nPTe3Tgs4;<QWe%w@~jN{h^#m+YQI5t||T?BfgyN*^%u
zmrmmEy}CNn;j~>!OOCHp{*7v>5KWCN@tnf3AC|7M0m29ju=@-~?Es)<r`Q^7@!V#U
zrMUWF6cHU%CjD?0u2vwm8l<VI+2oK3>J4N#vFD^O5d1VZoG7|nZXnR+tRB&s50!py
ze+=kBBb7Hvl*dL9o~9)^ertvG=(|1fVzy3l#IVoISYf_OUHWS*^an;L6t9#y1nJW3
zzB3yl={eZQlQzauzO(*fKy{w!l-T9FP1I5y_+?Htk#j9Xec`4*^H*(f+T2W_=w9uL
zWsXz-W+w1)*D`cmiJ|CYGPI@}+uSTu3(?xN>B*FP+>&n(|AjMc3}MQ(8~X5ZW$5?O
zb@L{f(INRD0yG`c9EdoE@74?zBpS3M!Oxx$w3O%Y0OceTCegu{@RU*R1YG@L3)e5I
zD}jo+Y0bas;Suilyfj>H+A$b6_@f}G@W0V$*Y>+g(ACs7fAEv+^U|o?z*!GQfsShX
zOR)2Kw{iJ2rK;=xZ`T_i5^iJb_<g0_OK8_`ole8ffJna1)3np5K(qa;$1$F+kpz)7
zf;vBx$-Bdt6p#5;_{h<s=YwNL1uhtC8^B9LR?kEf!6UASh9I!jH4shmEbn$xvq4x~
z?~Tsq9X0_|kDo2i_FyXKTah0GAyMh&`ik@n1I!AwZ(KWeNOqT<<KD&D?6rb#JekgP
z1Ml~L=qA&xwE7%>>R7n)wYkR!>0iN%Ls7OV&Q4B#PMOC{4QQ(bufC`r|53N%AGGf=
zzV&VvPTnHl0j^B*tqo41--%-0(m4-=?<~7^bqEfIeeG@DWS%S2gLVmve*N6;L(_yC
zltr%K+9W>W@g`p+-8lY*pEb+aig8u>1-{$}qp9nR97fUEKRHqkvbHka%6fd)rF1ra
z@C=hgD?OJ-Jy}VPnkw_IZljL8wJN`xg-l_&af|F8^efAA0xi$1`gen-ppN3Nf&Bae
z&e(G#b&npN?Jb^rOK!l#uC=2iUq%6rvib}x-T<+ixZN?E#Kru>9DZ;db&ca?>BJTv
ze(2@*X6l7Z$VPS{iR{S2P{kQIX)KFNWL`+=&69{ssd}EJRSTbR%bQ9rU}9g(!CZwS
zoTZ4<w0_u@MAq65@2J4(go|VNsc^w5v8|WNcVugTf1n-1Fx^4Hz$*T{_q*%}Ka_u9
z7nA-`J(u^ru#>(kLT(Pvo{K)uWvO|(ky~9D>KX<<si&K5mHo0QCc4JlxJv_+h=@UR
z9N6gKG-#KPn=pg5BunX5nL6ih^5+P7W(MlbmTELCPa_WrbbAsV`MET|@#VnkPlc`!
zk&uiSxpuMiML%0swy3FjI>}DsNnDwWUB+wgmJ4WRY$r(=-YnW_I;{LGY)NXk>1e@4
zl)THzxe}MTS*j~g@tkhLabGP6y2<$Uue02|I>qNDi(~Qon<%=)D|VZVGI0)1ZaiLE
zuv6=*??$E}xPujJ7rSpO%G%qBp5lXHPZ%^p8H3{<XuQx2x3~9R)Lt7;{B9fcf7==n
zCv7UTw(4qP$ySqxkTn<G3Pb1q?XKFfNePua@rJ(6ca!iJC-)U{LyrFTpBXfN_4xAk
z{?PRpfpkIYkZ{%?jz>X<anx0Cb!e9_M`;#AF|+}0J`FvM0J*F*tN5SJ)fu-CVCKJs
zPm)d-eU{G@wDp3oOZ{h5hL#202lvCRH#Vb3x!bN9xn>>bE9;>BEqF6?{jq7z{>LjV
zR&(=O#>NcNPq%5r1H}RuH&rGQMmI}k1(MPZmpf^i$w(;o%bht9ue8@ZPVp#(o$BdZ
zAlbg8@09Jl_XOJYL9?MK829t(7*)D2vE&hJ`n2Vkm3HRrYf7#;v-l^!MVl;@aP;ZO
zhu1E{bLelSdG>x|qYa`d0(Z$^-#&gp_<(fM&wW?iqaUeo@<kL0Do!L$+v@^pLM>v~
zgP%L{P?CH-mYtg-ukJ4^$pq1wU3(=~@6MB?pQ;P<+-6i*28f=NDT;WgVdfg0eRvY%
z&!1m&ym;!<1onCpS9ZBP&LpZe2?c&N?~m;PIXi(*g*$=p45D}%`bfgTcO&-=sY|VS
zL?xxexe3@dRz0@+i{H|67mvw)eI=1vPf`3AyzeJ<b_8UI9g2fknwVITPHU#6*pfR%
zCtJ#!sz)V1y@VKJL5hBgMv3w$H}sY<7vZ(}cEhY~p(q3&MHa|$zV$=QUlrT;Je6=-
z(t@5g4-f}oK<}dim(|vRE8~^Bc)PI%^tLtCXf2HeEl%hpH)|eyh?FC&UOg@E@C%{t
zW(()U<!ImRW%*mwMDI_Bg5*=I3LmV_NIiI@@t;+KD5Q-4RvK8L%Ep1|_HU$A=9u^3
zPbfEOX&O>3S3Q;+iFcmy4;Po3wa-Wx`c{E6{js(FFZu5cui)P*Gdr9*4UfSbJBpeO
zq2*@>9E8J7c7#>U1clYD@6p`*$+k0DD}Z<75PJU{tRT?#nE`M1u@a19apUTY)1Ib5
z!_;=tT2@?ohdS{YD(xztU!@>OR(~tN+t2nR?+pT%Y{RIDSuf~0u=s)Z2qN*COi@Qw
z_4f|>W=~72_bIm$wP*bmf9!&D@#AeDKL1r&)o!DNOA}mO8XXk}o}Qa)$)St7D}}hp
z^2ET;v@PZRj2d|lD+scU|1O=dVC<Tpny;;*Qu8?JSt2WW6R~~l+6|VC)VeT*P2_Q`
z2aAkH-MKwR7+VA|9DJgbdx$AYObtxn3q=~12;7<;DldDU8RJQ-e8Y%#G4yl#;#`S#
z<h>Ft7Y4OQ^#tqoU}A=AbP(g%0~ZSs1<vi;HS@w(qL)W?38I(<n!hPLwz@q7E-T<W
zu!6BB8u}Ft$!n&?7P!V9s7FEQ)OD+v?c&9$QZROvGshr`Pp32VjE~F+X%a6d+h|Ae
zr5|)oD3tpUM;$+?0?v@$xYpx46jvCxjJDUYT?}NI8EJl@q)>2a-lWiGLl+s(tkP$D
z(fD8=HzE;SHH(08J|!|n4?73je#2_nL`Q{{c!Xm+4&{v%8q_8n{d%|6v8w(%Gf{h8
zH*mBlNvgNB<0Uj%lA@#|I$y9#6ogNY-PB?GNUk8#f6eZG=ILy-`03*36VXg#l}L^2
zH%SBDQ8A!!%U$(ZR?rYX*XQ^ImhSoHN)fm#b^lM`YZaC9p`Ix?miYZN@29zfiwlnD
zI)|mudMW#40!PGk6I6sOmk|G-T|uzJ=$GdQ$H13LilIyAzBmG>sIc{dT*nu{3<saI
zz?L!(lGMcdI;VlBz)|yL5nlbp0<S(#x`*moc}m&4QI0Bof)Q|=*S;ks?yA!SP_+?p
zv+QcqTtjV7#`I`@%Mg<!anhU|zC6V$_GymwS%el~AHkPU53mK1tT67UOe_6TT6qav
z)`cVREz~7rH+>(@hm697C`9o+iTK}QTdvH<%#I``QaHC_D}B6NT;$mst-a3Bc9S$>
z9_e5ydFMQmp{@y{i?mWQ;>itXX=}8m@or}-aIfSXF=9N#7FI2azn>}f*?C{SU!a15
zbQ<;E(xDxbbP4LyGkWty5hTaTMrqHv<hveN5h%-u9e4z2!TShH)~)tt!&Y98*)q35
z-jBU{*AYiO<TD)-co@M|OACgAHRVaPXi{NkztJJVf^R(n|Gu|434K&K-P|y&sbhn&
zv8*I`T`dpfmrRdTpfPMt+8JZaQylqoaBZv^>a%uLa5_$}RY*PlnJIp6_~*4yg}^B%
zd+^cP^@p96#$O+xG5z+NrizbGZ68;;jOx?T6DSNd@iFbMfB(5yjv>$uFmXFq@c2mp
zB-1#$9D_=ajq!9^6Izbxa+TEmQnPoS5ROEF?ya9c>-W*TPqCHPm9%>6b2=?&*x<yx
zxHquq9Di2mDpRnE7`_%+)Blof2hHcQQb+qdy;$rZ@KqF;>39B8pR+)ngO^^E^XM6S
zm(=qUi|~?C5QBL=JR8Xv&pfrKtVeA0jJYMQPn-p2^vnVrP^Xq>n#`mZ9Zx%scpsQh
z*EqJ^Z<G3*k!=c0OfsffXoDF}JFhd3)_n09-QE?askk*N41n5n>ZCCdN$W}@^LuL=
zyu`B#S*Ba-0)n|1^alF+<^|+Xjy|%O$TwCD=v}nJYNcvlIN>Y71xH6o1$fLG%0+&&
zrvB#&i|y`dIJ^vP@0u!o>J<{3=3?pzx*k5Et$|5R=^&?dNfp+m8aeFv(zo9x-e!>z
z3@LxW^U4n1SVNQp+s1Rp2rh%;X}GY}Y!-?gv|r+n8ta_#2;Z1#)Mv`)ssJnY?N#`K
zoQsg92-P1O{Y1neE%;`UbRx*+?mPIhba1$1?I|G&bjeK<bc<<4HE~vT9@{*|-KKT+
zMFH9Bxg^W9>8?Li74H;_rpg>mjrVUh@=YvDe$8N;XSu0l+Ze8hDPs;j)@3wp&`eA|
ze^H`mgX_FvrBH8~N?I+P922w-1tC(YYf`UvLX`eZO1SWyvNr?v^3bqEQE7XDNff6(
zRUP-EC0WMi9HsW(UPJD4NNq-m%xIA@Mn6NMR%mp!kmsLA%Ul<x6fka9o1WJ^Mwnqr
zk5Xs$=utacDFGL;(azhNqJPT`I7C(hq8FPua~oV%#XV*d<_%BH1c`DW!G8eV>j^+F
zX$(|;(?0u?qE<Ajit`~V&-ma3{7RH?szQQ_knM*UkqFAu&$Sp9Z@0<7(MYngtLu$G
zH;_OC7Nc432up!eM~EK{yPP0$qT{ZCKm+nw03PV<En?Ba5mPk&!x|>?u!)ptawB)^
zKL$vAH-sL<QQbwS$k^B&2w$~Jcqc;jS;qEE&GOaj)cY4|3x{5E;+y#d7Mtd>ex)D3
zJ>m4XxKJ=tL5lfxgkFN9_HK<86LR$990ohMCL#1z+Io9c#Le=V$A+ni*JojO{@no$
z<ZfO*3O3LtC&)-t*?PbZN&1Lpgu~YZ89EH_AjO-Z%qP7kxSaedh`C0$9|ckn>$(2W
zi0VSC(rqzIpPqZEfZZF1j#;W!_&yvdez>T(;w4pT53`Gh6uE~EVmf)SBxviU+Mcah
zdDhWXNe(fqi=CO;ixxT+RdHH=J=GBF@uU><C{7^q{QlOD1i-$x$h7*QbnzdXXTnNV
zUkDP46DrbO`UAAXz2hi}yBmFwa=CY5x_p~vcpgE9NIfipa-W6<#u#o;u(|q4?Ku9`
z)@{BtX&y$8(e3^y9&s_ttWeVTOvu-w*+;Z*DzO+;iDg{WqC?)+$eFesf4EG?TngS~
zHMVb%b#8K8pM0b7nwY2l<Du?MsWWAcgXup@s}PHSm*1L$ar{viLNREWSn|DUuY`Fr
zHGW@H&SfbIkC!!Y8npIPoGaz1!kP&+&YWl4b0mdxeg+QVDGI8ks1UvxogShw`ZSdS
zfLtB1`GqRu#GqBiIv!3H7O^RNl>a<hh1f9~2E<0j)fJ6+g}#eUq_)}W$9(gCGJShO
zi0-elli#7K2$~p|Zx&f-R8~bwOX>aI(!Z<45=T9U^}=uh@veob8Oi(W2CkEksXM5S
zG2*M5dwh#aznM|muYcuZ-WOD@KBu<wq+#V#{$;tcAR=Lu>`Qm*iUi=^og3##Ui=u}
z^WCBA2p@bB@+y&<Ga@w%sJZyia=BCPImtH<4hfX<RyW*TWUMEzr8zaw)#YL=Z(Fuo
zUm&}+UML89n6^dFm!4NBg*+|7oI=5O8G2+rrhjMReF<b%ra`dMJqk)eC-VY&*x^h`
zowEwH>&%FQUGB*`qv#)kC54}h&yVYud-{}*j{U{QB!A@yD*RARS|D)l3s|<hu9qn9
zCwlrI1A{^%s`I~~yPdk-d%x)!uL+^75mU$wu3>XTSXBCpeJtzV)+Ky9nD_P7b}+S_
z$LOuQ|F;dbzWT@aBqVhvy5`pRXRkwvRu;(#dbgJ?xO0QdFs@r=V5MY@lUdr5*bP+8
z$BYcDU3`ilOsqlqA_``sP$mt8|FeRQ@jl}<Qt@X{pE4Qy9cI@A8=aCbO8|u-u>>h8
z7bTSK#_@3@!zp<2`7IXPZ!SSTn0GEJU7AwYxts<c*2*Be8CetrTEYw#OwE7ud=8kg
z<B@i6l=<UWPQ||X8dKkmhw@`Vu?I;96Y9+6wWBNn@`SP@U;<a#pxeUW1gL)TAOC+-
zg^dGCt4W-|#tKycvqcn{9tZ=DTsS%6KWYQ}!k1~UP}=T!|J00|qTDRU>e_y;h^XZA
z*Y^`4Gm7B@nTFK#OJZ!{6&BOJ-qa@Zo90YEjZr?4GzKu!SI&JU>j>#dV2O$h%rOk}
zjmwI!@3t>U|Ai+XK_}ay2NPpl>AKsagshYKrsD_r#LI)E{jh-5N6=FzB}4Rlg5{ZS
zZfjszBu-D@zryI9`@^cH`5|FHc%qQE=ryN97_8ent(4qCa1N0q|MAkGgEQ$tyg}Jm
z^40h6GF(Zyt-fFCoi<V)EX-6l(_^2^kXhL>ckuF<8;q{#8v$6YUCHY}QNIo3O&XQD
z2i(SxOAsJOPHhSv3lz_nP?XXRJaefDUIEAWh7aW&kMKBY%HiChS9eug<!)b=$Fee*
z1fCC&3d^S+kCYX!(kA5ffeBs%>R<lu%B*91&Tmv(aOs5A5rK_+Wr36n(MB?d)Y>ot
zu^MLrp&g+a-n)goK_HWB*JESg;De%-I7~)sy1VyjK^Nd~fz(Bi9|znrY{rjZw<vqq
zHC@by4_qp8*3d%ZTEuFq7Lz<ECBk7tEibl6w4Z)xEhBo72g5rW3p_6$hV0OiG%{9s
z6wKiRf>3FPwhcuw=3}Gi@I9nDQP$Zc@;n*X`(6U4bW+ee_@!4&XC}Tm7h(5ZA=Iev
zsyJJgs&ATZf^nalIopaTm+&_^Hb~1wij3?qfkLQ}Aqr<RDk}nxStNN@NKXQ!ZWrvw
z$>9Uuz;@Hza{s>CuyIrGQHCmu#D0tHFzv^QR1iI*3%WmNsZAOwj!}P&hIqI+1bqDl
zZQBU@#>GxWYvCFHYXS4FyAKw%?vV0!qm_dN<2A!(;Yt=_tye}CC-e4|OzgiejyQM9
zXCS47ht-q1F_$)7zs%gC(2?5}k;fA(^`3uI!Ciq9wFM7?4=i`fMEdQQJW`M2fB{U8
z5t}T-l>IlxOTTQ^TqU#tXpKJyQLtX>R*1EzQ8C`Rd(0tPtG;VXG?c6R!%BAe(b@M6
zG%Pe|98jz&WXFIOGEOkFOtzL0D#+(|X3(fP_fq<x{s6L^N}>{LtxrYW<7<1k_^f7F
zzETYbgkkfL85b&eJ=|jQLwt6;{%sh~<|}s3z<8?_q$n0(VV6)LV#Pa^bW(0(&e_r|
zuquj-a`3Ii-CzH9ThiR%M0MJl?zUpHrIWVP1G8<bi(<CYTI1tqT>?-`ooxoLXs`QA
zMk2bn8ndBKkMj^et675{fwGIu_z67|dXCPBio)g=%iQHTk!9==E3|>qW8TLbBNBsY
zIEX?TL5(iU%6mqTfVYAjmN=yt&m&KdZpN9cCn|z#IT;2h;vR@WJy2_RO)*a<l=rE}
zh9&3}Txjt6frpB>zq)oVQrJf@N<H0f>%O(jruG=7_kl=l|MCv^&19u)|3rlXRMRtd
zkF0Ry)8Ug^oL}uq^*j&Yb>s7LXs|Pi*P*})Ic$4rRUX0KHcvuMJLYM^eZzl*_lVL2
zoXI$DSXqmIk7zj;eQx~jyv-!$k)@-8O=Ck#REjJ5%`UFq^$1eB2E48vu!>)G>%s`_
zJON;KE90Zfh6vJr+U#I7U&N!od1=@$*GGKoZvIiZDM|0YuvU|&oK>Wh)Od$&M1;UX
z>Iqtq=!?RwyF>Xp`C?5<t^EQ?t3&5CV0-iKI?bg`aoL`R&X;P{4bLuDBHj`5%x}hj
z%~@{ArOg-@S&U2PAKvSp8JQcX$e~U7dHgl7TT~=#N9^FrMi6{Td{v@Gmn5LdodEPp
zJf~ss4_hp5mqz7HLBSF3lj8L2!@#F;YGayyD_ewq_xgPJA=%H+!0okY0;@4T!rd9i
zu&)JiwQn^RyjGx{ct*Nhfau;<-p1N}d_tVMj`^&S!bdJQ^E{F!@AQ9vUii@ZIY3Wy
z4u`QZKBx{QM+bw7gQMhV#?ST-FVIGechbO*z-1fgH==<=1vpHz&*PQGIDtjUAm2)^
z2!|FI-?C67Rzu5k1a?bcX4SF>N1jkNA^$Vh6!7pX=jG`9({RXwGh9m8E6pmSucZrE
z?V7hF)hCh(!Hy(W-3yo|6TE~?G(F<A_+V9<?_}G(jj~N9hj+7DPcbHm&qTQe30zU$
zjqA9r2TQA==kY7w@REzB>tmyZK^(ikkys7h8ctE&UoB2V9eyN{Xz@*|?F`uo-IRXZ
zde%)D_?(q4Fo%SXjgp~5eD5%QPZBp>^vuaZMG%x_9MHMQ5+JI|^wRm`)*~bMM^dY^
z#i$s675o_w3pNWCWwsx9&E>bq-F5Wc!$LwQ!Ytsa;n_%`l+qn`Lzfy<)o~N&QAl6o
zaqkd5iuto)5MXY3tMwptfbXa;&O5(BphR`>Od{czu@C)cJDGctKU0BJv8&S!AcxB?
zd|p1oLp#Nj3;Da_X2XO|SSWiQ2I*GDHnx}CqmYv^RLtf53E-O6Fi{o@&B9_#3gVG(
z+Tg<W<rE?N=^_&k3)lTL!=0~+V}xqSBuaa&!1;~|@Yy2*H;OVWru`V5kvt{R_f@uH
zD8z&A*)pcJLn}$@{%4?K`H#u(?VF-x7xI^{aN7X#id$aoTux(k8?CvmiD%X3Kw%Xr
zrqjzc<PLKxLVW7e6yF%u1>NGWmKXn4KUkk~kGz!pf&LJ0RpIYEEnfE_^(OIT>TK(N
zhskr_Qka^sY5i#cmXR>z!ck3m0-7o<?C;SJQE==U4a07M$Zw_3gqu}@A#|h4u*9sa
z_87aI%WZc{iAFo=_JLxwHHjgX)ikq({D2rYt8_IAJfMU;s#aDMho>ldVVGYO&?Vh2
zH_3|bqD875yaRQk8y=<=>`e|rIu(K@q4V}9QuZ_Ns+wC#Iea}+$j41k1!0_sRZA(D
zf7ok2RL1CMfDZ&sHoPimQ98j!@K70(CNR$hgQT61<atl$e7yPR`vS$FnTLvGwW7F9
zRN<hpoAl%9bcv75!e!n@1jg?aBBJwLO}UHso*b}-&4Ql>Ze)y_%?pVoO7NR&#fx7f
z+%@>|=`;Pt6cfoTq%V6hp;%dW=6^;%UOae07sov>otrW^+=Rd2B!6vF_#*6R$eo57
z9-lZYL)TBq&rBZX_Gh|CaWUxA^TT6P#RDrZnbsSMH=1AU^UZ3wa5A|KJ_(Q_>dvGv
z-{0;OB~5rP*bg|HTd0Zti)G2PM+|q|TyX3`AcwIy%xgc1M6%W1>9fs`H)xBdrF>?n
z*ZN2r@uQ`Kj)=-{eFg1fxz-fl>%y<#OOgc`*-OXJb`f^C;RpTgfjj-y^v?a3XZHg2
zvA5MHp;N$3wAJ_2mOYRA$yI7F{*mJVB-1K(T%T6_J{`q5qUd8;=B~WZxX3Qi+O+wU
z>x796-Kd05jMGC@cxQx3Tl#g%MoSWn&>Q=lh-pI6Gv(7fNVH(c_sAZ%GbZtJ5u2s0
zfphXOF{1Rm80`3iZZv#Vrhk+#z1=DW&M`JZOj@t_mIL(dNZs9TG{zJV72bugr}6tW
zf_(Wyey-_6i-C@5OAURdimCM27e>6AxplnJR1Bn6DX~38$1tS=z6kYtY||-aTrZxE
ztJLST1qhZP9W~JG>Kf_d)et_$T9-L&@6PH0sVkad0@z2gO}t`Ljp9v@jLdsa{hkb<
zSgUQyNYlhTaR}a7dHk@uu&UiRxu0p{b$AXb9^F9tHS^nIQ9x~f?Hxa!SQrM(Y`f2D
zH2(?(KO?<z_0jP=%J$j_e&K~kl;j|IV=Hm%+h+FJr&0Ww3nUCG^Jj4r@R`)6&6}#^
zs^RB$iGGaJ1Vx5+WxtJuq>%$m2vZgPZ)I123aESm3D7Qiv(P7uH#HT8k#pAL)<|0v
zl4e`|+$^OOLf3ru5VnovBkxQHpd3{?p+<srgIi2bk_Le%V@U2w_ssP~JQ6k-(6K<g
z#7O7f8kHiep~QxHVz!IW!;Qy{NJb*cnz0~|S&TKdgt?~2q^8uJ{95pH;34s@z)(@8
z_d5PiaXbJdJP*CxF;QTDE(<>^F(jKYkyGCzrYy8xlk+y=9vbChht$Yx4}N05s3s@d
zsZpZIaI9?qU>fj*28QSP?qrafrM8_m%ei#y+fqt33DMM|U?R$Ry3#4DIy6qY;8Nhj
z1d*Kzk@eIenjm?X)+hA4vvB<KW^6P>7h-a`c7!ZjWF^2eGP4mm&7N#VVu1)Lv^cvL
zBqTI|m7+!7AWla=8z#6j*|yVKR+Xt|=5MeDMmU3?a_n|*zPhGJxOPln9oT;}w<sRr
zQQzwv(5am_2}}q~YFhUwTQSey5A?;aiR^8G6|g+QSDR!9RBRc&qDNC9r8a{Ot2hxP
zmCxjiM_bt3=|k!i{)mdEo#3j@H~1@%Dlxda?o^9_S9`XUm8}>LM&KHSh{MjaE*M#f
zY)A6N<<dU6+N7uZ9Rtr@#3|j!uTalzAG!$D3Jurn3zPJiwgn8yB4VE0Tmb4+B9o-p
zw3zpa*Na>?ZawDa#>dND0Yi|Pf11*FvYIPewvQK%i`~pqzpSE@9mEWOCX->#PxPy#
zg+-|fJj#CCScg;vz1PbueB0h47d~Qj(&N~OZdVZ4NX?6aGTLG8CHlzJD|3rSFO<@q
zZN-Z-RHVH+WvLD@*^s^cSWM!8F~eNs!Vg3a^su*Q+DZBwOvkn5{q6Z<b8EU+=vlAh
zE~grCh_XD<0J0+Yrr|U<S>xtLD_?S`ILb5;UVfQ&)0Dlw!OCE;Xs%+4`L*EIy2Ef!
zTO3OoZ*6yEk5R5;sZ?qEx~`UdeR%<s-ANjIQf?;qkE0BBldmi)9fmZ&Tr*F&Qg`Y*
zw09Og`HzLU#v4yQ45WR0S3B9E|H;HFquA?+LFM}3-KSHNk{euKmA&&HaV(OJT0S!c
zgkT_hJ5h?fp*N4;r@Z>Hwqlttus8xuD<yL)5?QJq1%JlX4R=}A6cJIpuC_7F%GiKG
z@qYZ%f*{inbGe-s%J*!~HLkrS95Z6DAs_jEdOGrls3<_MZwnp;SXK!1YO~N7d+lv0
z0iDjW!B0X_Dfn)q4fz?0`-s0hRqwyuEVE2fQ@<W8CMPP8d5rN%mS7wiBi5@b{si8_
zURg|SrUFlO9I1ZE8rnzGb74zD#V}`}b8ebPK0F5Y1s2=vBMozNJ1)#O0{y(oIcQV-
zIxd9lqTZ>8Ri=Vb&cRyqeTrUS07RtMn~1Im40!yb=LG9Z)09*=X-05^UpkUIoiC8t
zudZ!>G-$g4;Wg9^7gH2x+wk?Vtb#xnrN;JyFJ-%H7$;d6v|#;{PB@K(IK$<B@?E&h
zpt<1C{%q;kua_QEp17V<g?u{S2TQ?7fj}$*NCZzScEW+}MZe0$$}BY7kv;j6r&a5u
z)h-5Pzt-1*FJ&(OPu*wuxmznktGKjS^^#;_qOMJ`vgJwql1d{(4o^}CPcSFzJzrJZ
zIXbyz{_a~VH`K!<`FD6+oB$d0+PgI((fCnv0NUZQYdafbFeO_wO;GlV*CE%NRS+Hg
z>tSaf84V0aO3$##nQ$Jqw?V`A`URCp;#HlbUr4KOE+Z|bEZyARZfrobfn?#*aPa#5
zrnwYv)BEAeO75PWZZ|||<Yh47n+yq8)}=3k-4C9B6sk;#VZqqK@5T2;r`|187+sGm
z$v+GEw%&l-IsB2H$<2f}FwM*B?G0K|uvX0@k_T6vj$=E>v1nvoH*3`>p%iL|_Vr<e
z^JCS($dY1T(W_lmFz5uIZS*`fSmM4JYE~1p2M>{<jrMDKSEE3?M;K3lqs~Y#o<!FI
z&~-T}(HN|~5$1j%h-&$+nG&suIHweAe@;+^dqMhRzJ!RJR13Sdl<=4U%z7A%GE=NB
zmt-B`N33#9sl12zL%D<0Gz*<Jh}oMKqC0aV+3mk+8QH0}K-aE~s>u6ZgSrRe%ai_R
z2jxi|#gvZWmZtoD)OBKooWG}%mfM5*4XVTRIr?mNBpgTihp`-8IX)@crl0${7JWVd
z#)M1%!M@p2i+Qgb+a}E&H*t7C<NI?yq!4FA3V>VU-lOsrWF;H1Z}b_4U&Z9Zz9vkO
z5#zG4_}*t`N=~1}hSi2~KLbfO<`ZCVe~3hej(>}*u=wh{#A){)SgyvKc#Ey~LjpN-
zkUk%Y0OKmW=sfN)M(2$a2Rqk>xLp>xq<w{$7<28kw6D>}w<sD*`lgtT<JMY)&snV1
zT*QQLmN2t5L3d1IH_h8lWfXyZWUDFQDRippF6WPYzb3l$0%Qaqq%M5-?IY&N?FyJ7
zUm`|zkm_4`>Co4{nfR<S+uLZ!+HEAf?@CJVK%6o{j_Z7<NRZIyl{Mcr5n3ySJ-L#>
zh>&}*5)VSgpI`-@)kQt90@ojo=2vQp-aCtM!JOR<IsBBUgrZAUzPaG{7AXY3*=mgn
zq&%kRqlaJWri?>WzU9P*YO&qQDLr)Ryd&b_UA&esG*T050l|DB{#Hs^L$(+)s=bxV
z$-1KW4;xbyie>Yrr6G(=bW;n77}%tl_*I`QDhtIY-`&3eKm*J|yJ4@!yGWGbpI&2c
zZmjZZexn4Lo}9HFyrQQRO=U@gE6oCCY85V%3w1L>LyeJPl6B{gZ;o5tlkNdK>ok%a
zldk&YfB_sevrq@1ZUwv9AaH5aIOt;}gg&9o9e<-vs5?!@?&>X+IR8yl_WBzAX{3P9
ztD@LU_o1|*oA`jNnC4Rv{SJzlw$5VH#mwe)LQ~<r+0XSG>Z=sEZs&a~o|_csm3K1j
zm$b>L1|o63n`v9Cq8i1lUZ~ovtbOOd83KTf<MYa>8qL0+Us*{s_M}Cc_Yun9%VeWn
z?!5mY8aTE<ld_rKATQR8M(yS_8f+&y9}B|;e(~!=>6yeK9oWUIQ8*%q8nOS4LlG8@
z)4M7{pB?iSmBaJbW~3KSdEfR#hX+?E192O^D|vV--5h{&mmElP5c4h{n??<l=ux6Y
z3KaD~Q%R73=t+FT`4~m%QvDv7i_bUGAbKaf@JPQlg)tfP<xiyNr1GwV&98dq=6Br@
zt98PpW8b;rN%qa&tNbPQ+pTJ_DRjs!(#x|RNwf$;HU*N$x3u}O{c*7Vm}%@OS*BYd
zNd8lO>TizL*%oXvhr=xRF5|dQAq=1RNfud=ip!*x=_lU_Iy7xUtAayX#gT~>r)kHj
z2L4%M$>jVaI_kPDBZz!Z$e5lG=f0Njlbuh6lURR@BI?ySOFms1i+U#h`tvtqnYv!G
z_+r|vmZm6WYz%Ec^WD~1;S1gXrZ?96^Ro*U7P~IPXA<t4VD=*I9}D8KdQq~Bb4|Ba
z3)D;X4j+$C!v#k+$XzZjvBRTq(R=^>`hz(irUz9=B8G%y7rW+Cj>fz+M7aM+Bzltv
ztd%rw4yc>c3eEr$q+Q;qe|df7mtl}C+&huUwF@P7<#lmTAM|tM@!LG}kVo;xI~7=Y
zouD7XQg1)IG~OY$sZwt}t2H3|&PViTwEZuin9}K@H@$(Pn?v<y7@47=S|Xz8IkVEv
zMl#_%4wz)>Nd#96Tx5+xe~N&_XdEThCz2oduZ`Jq;K_bj?M%#`C+Vpw;r(q_Hfcb{
z=y5dJXAV1i-*)YYO?r~Er{<p0o;rDH?sml;&tBYp{2@SQ4F0GlI*SMV2V=*SbRtlC
zjnoqJy)4bP2*bebstTlB&Z{~RdiA;J#0v7XbvJ<LF|Q2}X^$?3YSC5ZUddVs7X4;8
z(2v&NX?1A#xld}@6!&`rLZN6r({-0TiS!2HUl3VB3DRO%6HlW&GJy&gH1G*ee4-L#
zQik{`4W~|X8yo}>dE{EGnie`vK9J!XRu|^n97G~g?n@#9GHcII8I90Nrdbf-rT$2o
z(HJFe{S_nKpb@brTlLWX`=_d@=*@#|j#v~2NuBAZ)OvnctW6xUDNSsy#STgW0Y+2~
ze%4eOCG1OF+`k{X=U?=rMcG(63#xx=tp&qPj5cUqTJtjtOgmcDxNBnZ>q*k(q5%yk
zFWGjIbf&HP*Tw83zn(|_Vjh%{sVsIfNA<l8xIG;qBRY#_KGO?q?XoHXZ!f86O5)~r
z`>20|s2zjKEBQ#oJOl@EVkEKOh7v`rU~C?`{Wi*at-5=h1|Jz44~y^b$D!A(VVGIi
zYF|%c%R;?!bI1{HcQqeAd~oWha5?z<YiT&G5P(B^F1fw28}-!>6n&5MJsAeP%}16@
z10Fr<`d~8g8mCPic6-U10({CxIf-X&cG;n$vW|j*++<Q~9p?5xeq!7!g<c?5lliOE
zf=1#i0}D&L`#8~xb$}R-CP_P5=qUrxSB`!+kU<uFdIi}I;$!OT$@QD`>~UMqY(}W_
zMK9bW@zs(_Swt)yD*i;fiXAn3!;D=(eEMmR2&cdWg&vnqRbP1sfG1BC?r>&&&ds>=
zPdid1zmn{2uz$O7I_?~!V2Bn;Y82VSP!2|7iZteIU+G(3`uu7c`{beR7o2w=6p*Hd
z)~(!pltj0^;0LHVJqwur6Lpa#_3H$@(kzT>CTgE;vGKYx#Y*B*&_g<c!UJktCh;(h
zR8_#JO7N+0gMy&nk1y}ZZbf29k?uofHBA*MJWhG5)o%}Eq}c4$ZX2OriywTR+=`)&
zR7v2<GOw{NK5AE);yIg?*kgGX|GGC;07GUV&<%Q|WV&Z^t^xc6!a$enVN@npFA#04
zaM~vNP6PRB<!(Glt}cCG|H~`Zh+~lvg<^K}A7ZjBzotf6%HV_kevcX;kE)~eNXFg0
zO+-1f(V)iU4SDjoylqe4mW#UiDj@CcN&BOZ1iy_rLjtMa&{D&xxts(!+O!x?FzJ_w
zhp=DrX2I7&i$HVUuFJnhH*i7?@sTK6#otUhIA67Tq0)~!QbG(|Q@5M9I7hwK3(>Uh
z5yu#0Jgja9+3>g6Bk63r9k%u30ajQAMy{O$QLU|;H00(sFObV!!|a3Xu#+o+$z-;@
zyw3CcXX{^V0OWOAG)nSyY7WUgDBs1SZ{Jb>dRuny7&wXuKx*b6dyUumJlR$|Z07F!
zZ=q)r9z8$HwKd$YHW{=o3151OH|nd7zv?V2xIkw<?ZJgY99NoVH#Xm-f2(8<g%cjF
z^sutWb6!|RV%D$vr3SpDdHZ}mGgbUZ6yc6>4VoSdGSUp_T<&k&eMsUH?2E*BiI9zc
zppf>~I#OIYdhYeg&P56c+kdpZQl?9Aapcn`21uihirmMe{pG-$LmJ0LOD8D)?2NgF
zIrMY4l~1{k;Gpw0n(G<^o597GJK%Rbc6|rb72nd@pJZaMF(Y0hCyKS%vkpdee~!qu
z+`#K6v+Ui6$LL#i;~!zrNz&~B2>S8piM~Dpk*>-V=;)g7R>>bwYx?ePKUbSeG%5|#
zE3zsrtdZ+PI2PlRua5+$s|fbfC7&g_uPJ+zYU9&&|3mD@yo;a;!+;W97SsD6A5!-N
zw_*bOuIu`4-k5k$`%xhoCEG>Eyp-SJNB`>LFNUvYsnL{>WwGBY*|XzCHLV9Nmpa^R
zw*E_s(&GYxzyOVWAzat1p%NTePVyQ!i0Z4O?QgVEAv+^facB-aF?o-?I?++?En{;t
zX+FfJn`2T!iPO_~as=_48}x=C8*<fx2fhQbzK$Yvq)hySW_q{&fXuiOHx}C{Nm)8f
z?mUBvktZ?EPjuAA)Fl7psvG)pnveYM=r4%FK*KSEUGlKsrJn%^l<%8(w}ZCfErUas
z4}6W11nT{HgmyC^eHW~0;42JIYDO%yw|^!R2E*e7F8ZS5bzdsbEuO=xJ|FhYL5Qo_
zy%p(Eqtgz@KV41zj%eGTdD_j<m?OElZtID7CuCsUdFrJ{4n`j)#Sxqmah9E6QoDTr
zFC8w+7DD}X<=Q_gb3K-Rc~l3dzJL7>1ujjO_HxMUrDVXls&30N7d9Tf+%ZKK`{Q+0
zz~KHa|NehS!N_s!s&-?;Zq)7RXqv_fRvbmyt~R%Fe#?Uqmi+W!bwwKwFaFoF)XDE>
z)i?XSFLRZHp&hqj!8|o6jqMLxpMB)jSI#x1;pJT8fRTs$F`iw<%S7JNR<A>Lt}ki7
zI!Z%~9Wkml?@ay92Xw3HX{wrcJ~+7WONB3>a`@R!X0!RO_?PA3pInqx*2BQhtMGDw
z1|M?iyR6C&mWf@~)0UKRDz`R_p0s9x=aD$nBvJw%1?>`8<Uh=r9~HhrC#eHeTF_&p
z0)2j6?JTBz+pq}|^F`}^%a8=RPk;WJwCvI*<8wj{PLBE=^3(sUa7Z1^P`FyPOqXlt
ze2Au8t41dSLF+dc^-3K0;ja79cllwzTcYi%)^QZf^ir$xlW_O;_CQd>v*WtP$7WkR
zS;#%$6}u6A(tCNJ1Ai7!l7I%SHOGkGRE-q`LX8LdEOFXewc_n#zpjgpvoQZ$;V;8W
z<MXfQ$+hR#Cn-<4b#6noaP?fZI|U`MYzi5PbiS79EdO{!NBx3Q7=Jp6b!7tdN521W
zp%B@g70vpk?Z9sX%lou)9HQZXSJxqo_N(Sj7gV{^l#5wb&3&>h=n(`oQZuaRj5dzt
zcUjvTNH*45=5O)tTjV{dZc1eEDMPNhuieVK1Nka3o4%*SK+GdRa5^r$h<N@_w7hgE
zG-~h~QQ9;c0Rx@_zQMed>y<M~Q^PfU2a==+398o%=s<*@?%53;Yj+1VI*+2*^0rF9
zcK;)K*pDc|&q(CXa`JS4a-i(tzQ>v$F4j|?T<L%QF-dLhpv!NCAoTjtp(&6aUD*)e
zT8#6sz>vm{B;A^<H})pjT#sLtaVx|wT(sMBFla#_^pqSb(n!0D*T`Wb2BYUm<3@po
ztH;n;($0w7#XXrFMcWmuzaRlDzE++e^}HNB)Tg*`%%j|US|FiJciaFO)9z2?B~b$_
zcM!^qCdlw@kVt9u29K%W44*^Z>118;X7r^yuKWPK{9+aqz%s)><Ti?TGxW4`HwzdH
zl1HXXT}bkc&Qmg!r)n(_f$gqt;BJ$<;Q~I4dOIl$$tm9tDxR*kHLWHFiqhuG2H6?e
z^?HjK3oNIKH1k}L&&!cdK3)d-VWcpu{|6NAyT&L*`k5NS$nNs}1;D_~p8gS>-7F>g
z9$~2BAz!ECkhPn#^oxe?86(n)bMkT&vPV`YLY#AWR#9c;Bl;cun`%XrnVo<PhaCsK
zg3OLxXb*j?GSAu6-gj}xI_`#gX>37yXaO~7iXLM*qF3PUD>Y$!NK(+FDj|d}?TtpN
zSe@Ni$RkDsmEd-j`?mE>`JzX!3QOj1U;Z1fG`AqWg94Rb1lC-J_353D_e<kVDI_Uf
zPmP#yo9&BFghdOUjD9ry>b<sF_)Xb6CQB`2ib<KoUc*A`1ZqsGiY`ZMb<x3N{MmC~
zNeE<xMmP&M*0PflL*>5z0t}A>YB3eHW_(LcJML#3f8>=nRU2H|UZnkUQ@f~tfs-UV
zh(hN^w2OW6mOL)lexy_UB!lA!hAor^(=Dg&DW6$WL@a11n$ta%=JcAcEVwtWQ!+@A
zfjIiF{XO$=v}UHPRsSaNMQ*$9kyi>kMEEL?UFGAL&vbk}W>H$$-M7o%lvZ{7ZMeDW
zH+x(=^#}iWN)A8u620L6qV6rDvh3RRUj<1?>6Vi2?(XjH?vfUe?(UXSP-&#Q8$qO7
zLb^NdJ$csquJP_Q{$qbzWAFXZamyX^x~>`Lc^tn3i=Ax%dizt$#_;~*>IXviX<?rI
zQMJd_je%q(K{_)7vAg6ML+@LibN+8m(}LZxm5pc3uHE0$hX}w@W5z=|!uJ}}8Mxu%
z^?in5w(7qj)*TsnO=3w_rEguYwD}GlHZ1>zYzJRR=W}tP3UK#s9+vOC^xjOQI%G#h
zL3)HtC!=aD)<&f6vC}`>+nUaX=?+hWMB2KGSKfEWQ{mE1B65rRpTPc{`xD}A$_~r6
zNyN;~jLm|!1cgE#(uGV|-Fd$G@Sunna~gEI^IpU_pgV?HnaFz5k}pPVPx}+7ql@DH
zagTu2{gr7OXy#16#>@P6H+c~4w%`+1E;?HoQ_fFTemXClMv~8J&gn(C&7+9}S1BBI
zH|O&6#KG}1ZUf|zT?4-BmCz>oqfP^UX5U}#qCh8!VBv0|W>m&{hLA?2hub;B_H7XI
zQ%&Lav#$+xz5Rl+(L7IFnP4WK+>bstec9=g*Kyv;VtY#O+&0JUrT6z<9k0IWqaK&2
z?32JKgyGc>AH0{>t?16acUtPmd3Y1~?C7y=f5QAo)9&yW$umB>uIW~JqtUd&f%joV
zJ0I^|gKxBUKCYZbs*m~@+-jOAG>bh{wFT@#9rC>*mNinmvx;eL1Qa8R?#Tz9T<_h@
zE{2`baCDjS*xZE6G2;o1Ll)O1UsyUU`mZ3M9%J<+1frF`NHU}UXv<qX4}7(YvPPLR
z%U)`~5s!asttRCA6R$%g?d(09wzl`jPU{t^-cO+z3VUX(1DV6SStC!2rPq%@6fWL=
zGsPuS-s8})@|N#d-*2Tur*?ag$a7^Mvi+wQBR*M#DxHhj+bR&M!k~rI_1d5TmWZdG
z^}BYpY(?`OED%5g4_qZyq@(mX($zXH+(#<3__di0aEMBt5QHp1#s)^1gvc*rtmm-R
z8%!c!9{RUCp1dMOVNT(}IqnG@PCMtlZ?1%)&JQ~5xf<sj7u@7Q`haw!92<!OkJ<f|
z7JA3?HTNe8f(i5>%4xSXLu)h&!l;6Q<)5mtR&Q~U^4^TLU?KG`DsU}>ZUQmC4N~_T
zM1B+aQ{!1BavHH@xEY)f2bM&xjrl-_9J7<mE=KrDM8cSuozAzUUCu^yJxSsC3h_G%
zB$O+@Yo|ogB>Z}0zJ$zsr=?`$qHRpQaH6QFY$mR$CVH>Ahm6Sj%skmL$)|fyWj1VF
zgDBkn)hXJxL#ef}LJ0hNtzQ@?a189jrs#NmkeT%w$IFqHkuSt@9s&>EJsQL+`?O4(
zdBK@MQY>nyM}aMgVkDws>(+37Bda6!R9<s4b&$5U-OR(abIIj%m97yzFkPnbWXs7)
zRgNlN4i+e=ZLPhd(Hx}F2u@dol6J>xT*O#X`-;wnY1;MXrXE&oFc*O|?L(SX@2*im
zJX3iJ%ih&WOa9Q3p4Aa`h40p|25W%7&VavUzqzPGSX(?*2o(~^lN?WQNYzHW?|~tM
zP39jIf~%s@9G=CFe$NgsZfG&_IiP;;XaE;#65Vsr-O9`w=DK?TY5=mmXu|5}Gfya9
zoZb~Y!9G#YN;=Rsk-;+p-_8o%<Z)=fyV&59C&xbLd5+Ozo!*uW97wYF9xj|~qRPKN
zV8h266kvxS!wM3ki2j8SCSWy}wpqn`h2ux^S19mj&!4m1FZl}zV!eXIf_HH&nH(mX
zby*ngqwar1%mm3_q1=Og0CTRfapy0oP_*V@dVK=eVCYyAq)UYiv}=)O(HG7TuPb;1
z(Z9|Cj_W%g(Per?tK3M*;CrHALlE=YjR#y><qM3BVit_=$qM86YfPz$3v|;BeabFx
z)@8^A5-S?5dWWD<`HjUygE<xw-k}+GjZ59&8*+BmMrtw5d3ZhiMU`moGsCtg=*1EH
z?q-0}N)YC<<DG!wW%Quf+$|?9^mYY{w74L|gk9ZJFgi13L6XIh1?MdMR#o#VC22)J
zN!NfZ&&M~DX)*sXLRoV2*P5u07<=uSf+y^nurgbRd9fr($@a#$)uzQ+R`TkS`aJYb
zf)N>O4}7(e7S>SjrA0x4@&tUt>xB}TcwRxzg)GrZBqKwLKd~S6#^PWZ%he9ed5UeK
zE?5KDrq4gy!>Rx1bKR}ka-9%BO*?<cal6`e?L|lLmj0R;0LeAJ?^2QECJcCBaowY#
zy6fnR*K=%A+ivP5$FN%UJkIT}`hHhD8FS|Y9GnrpdDTbDEyD>dbYcCc<4^+5Kg-nM
znjq~hLD&`5AJ_d?ALBEkn(Uw6##GK4`T^Jhe+NWRH_!J83&QnEDhnMW-bn*>PJwIH
zamrK2iz&GfSgAGoGnVc`uV=21(DKC15dET<M^9-dosh)#&PJFTBssfLT*4ahnT<bQ
zjvE`3t`JUOks$<z?1Vp2;~skDF!!SK;@T>{zON73iEtS=TDx^s7<XGSTz{-_Ud2_7
zbe%}~)yj0hQ;}{L&vC(oxwr%k7iw2*-u0(Fkvdv`%SBi1w5~-bDJ$0|WTigxRRmY7
z-d*9Ob_h<Kr<@to?)0v#=wAj4xL5{R(>LXC6$`RO@7;<G-IF#)XUbgj(Cvt7GO5M`
zL_3zlr5vZTOuZcy10O6yh&N&tbS)`IhRT1LJ1?^Dswwql7jm;ZPD}H8&<njk0&W$J
zadK>@jB5sEX1~YF23D61b4bImnzFJr+%iHRP=qUw-^{i0NUeO>7ub*dhKNf<;&)@i
z!=(9@2h|VB>}A(w52{cqMnkGeZ^UXWX-6kyY{!r9DueuCmA0F&Os`>7Zt!m^=<4g1
zCAM*JAN&*>#;a5NEdOI9?gdn|pb}DPU^sj{CM-R9Gt0?KL)1Gubl!p9A+IuAijVEm
zF>#Ww=}f{}nR*HQ+Th1}g1g>gc&(OpmbEr;w3@|;+$~j|@SumtYZNSXVMGSzNMFm(
z2d?k5e%pKF#1S?fmNfmOD6_CdaQePckH4HTxp+KYPBlL<X?mmkP_y;rF?J<4G$sqo
z%N6xKrC#1xQTbv8h$NO1AFKvS!TVtS5!tIb@5L+Y1p2o9cZ&(q-Sl{ooBI}_tgaS`
z_B3y7Z0mP_z14_wNTikGluLB{k$l)msbM#unUW=;A*Ps=oGc%>Ebq?&+fM!lO#rE|
zW`MZ2L#x8I-`PB5Xfhxs(b1qKghTP<sMxA_L<n*d_6oMbVx{$Vs`8lF1g;eUE-&)_
z$XEO6(W&raxkyP_EZBPzEy`{-b?i3yuj!}w6~5bd9I;pxkHDFnsw8wj)zJ@Eq@U-p
zH&kCDfOds|RPf%Iy&w3QnsPox!F0ca9PjSPtup=W*t+<oSYMFX)rrC5dGtZ2)hjX}
zR$IxmS;lU;`F6YRD^l@Lx}m>;End+NV3&A_AJp~u#Iwz`FYs#W0%(=zPjo9PSx=B~
ziEaS(COOyh2<r!F%@45__?NxeVd1fYE_FU^#_%f~cfpg_h$516_yvkeLXUL@6P=)z
z0=s6sDI6yzO=)@zz(4f`9`M|-lm!s2MiFUD3%lAeF>0bD&`n-Z<-febQtIjHQ)|fg
zQXOj&SPa+~n#grcM6>wJ(7D(@4dr)tQEigUQ2-oI_<8~%swyc_>QaLU-sg{~f{6uU
z9kuy^W|#YmQn09ke3t9Njo`~7afEFu)36LI+Ex0y9`St9`g^?<X6Ju&n{IJoy;`-7
zLbTA39L<PNe>k?Pt!*AM@U61d<lgmVL3Fx-kBRamv%?*d_es~lA)DKpH?hxPAYkYq
zu>9au?MLR?I80V*r{1|G*FR0muKbZ#ZtM&RBuzo02+n}L8$=QW_a*=#1!30}WH6f~
zwVv$}^bcOgPQxAe-`RW_oRmUE@}=+HB=SUJnV8~YLO0K@^Z1JH4@XiLAV2$3zh^|U
zRUwDH4{<<Ekw*rF*I8IQ@!|eBU@)4g%`*R+*Z|5QKn^j+Q@?;Fb%<vPI$6FJ#Qgl~
znNXttVba?}H(Yu;;wPiT{P;frEvf~yVcW}RuvXrlb+vg%y@T!uFvpIbaxS<NnpLwb
zA?WN+7G)!+BUo8O@AS%@PpeG~4X#S*oo@4uzoB96IDBLIUaF!%EZk#8ZyQ^1f2J~>
z#SD_RJ(jgFe!b2gl=IqY%&8-`rspRcVLLQ5><Rq|EUYN5jR83$l{|emQl4)nk6k@r
zrHD1V@7o>;ksmzPl1v<3LfR~PgqBUwCRW$nCKABS`@7>anZEBQIL8TpzjB;J5rdl%
zW41ntzN!9Jx)tv#C>(C=d*{}4!zZI%=Yvi`zT+i5Ms7$>!abk!attS@<C0Jb)Yr~p
z8vbuQdag0JhsG;Xn_Yg5RIT4e!yDe0Io-JBp}L*8@2^PUbD;=CNG{fwmwQeVJvBKl
z``q5{pkMJYzmO}&Hd$?%e|y||U-03ii^{_pSGaeK_1J4d;C^8TFAo0?o^r||?mu|6
z!pqLGk7_C0ss>nwl9J8nw#ya^7rWaTg6MGjGfq{|x@hBrEOu5_dMrC1T}G*s{`5aO
z+>iU6?WUJVHbaL!@mS@fyQs_PhMc4*WbzttI?!Uc;NAy3@r;M@R_L}ZU~&(sju4Kp
zG;=vFQLtH#*ZcJe-fpttnY<#L=o|}#LrVA)C$MmU@u3s8n_yRt&#eEu>piP3q3Qdp
z10K%rNt2ogkfNV|R<tsF^O;Qwk%)NZ1#eC;S~>_;UI$kbdaXTOxlwalefntjGcH+)
zcYKA5EXLskJC5i+eblH*88JHQ;IX*4c)>F`+%5|}r>C97aiwR6^MUz<r|#Kf)qFM;
zaoqiqNlT182hY0SkG`81a<@;y$MN#`C2A3$c&eu){Psjg4D;++s5h6;;>BNKU4iuw
zGjk^Lylw%Uze{CBx!;^`qiVX08&^AGKjqqrzhyZ8VK-Z|J79p27)Pa$WiK#7;`h)b
zpQ+Dj!^iHh0MKcFKkyCcR}GKE;-~$a=NGBjX#-zU*qgYA!XxrQ;4)xhxKKil4i|!g
zyYId~gY6WlHS^uTN?htBa;+)Pz+;zzOFuwEoO~I1_GD8{UrYz`&6pXVv}e?<qo7la
z9oX8RC8T8dyUZ5^uwFN`n*uz}`l1qI0_4WdP<NR?8y)z>QLs)7B}N@Da7EoBRCthS
zWmIF>JDilVSsna)$>m-lbx)1kb<^7W&zR?k_xaL^f9BE0r<jPJKi(NAufPSve*C2<
zAcMkpU<}C`Jju!T--aPKgrMBE^Ed*BYe?QW0hdEf?2E1y)ZG_d{$sz>vpXiowu%c3
z{{5RL(PSmcUaEUWQ*PMDFW+Y<8SaZ9SkkLyZd`T?OZL7hisKo_v&u82`o4W<+GW^7
z>_<VvGZfd2J8Ue~8j7Jjmd%wc$0%ro!vD7QdPISW+!cPG{2*DmO*y3b9o*v1QO#?M
zYrz&tO2VdiVN}B4(1SWPB?8mNZ|L1#yvs$plCSfs&Ef=A!IV3KxsaN@8t>c({3)c-
z>GVGe!UKUZ*&&IM(8+|E*Yk)v-N+-+uVa!Q2d>^C$X$HnfXD5yG$SwpTP8W8!MFo@
zV&kL-wVJfpYpqa10Q%oQF>(g_3n2JyNb}S3-}zpyl%-$n{qDIUCx4${CJ_|$NhJ7A
z(_Y{2(qgK`lL>)y!<3zSw#w}|*ZeTcJt;fgF4T;|3k5z882t4LepVO|)G~fp72sNe
zzP~>1NH@o;t_3zdI#{o~#KF<Qs=QnbC2q}e!Ta|%Q;jY^MOiRc{jY-$_qQ=WYj$4l
z*#vx3`%-_g-w<wYWwod*!ymFPPI3RMGCq*^g#EDPcx8qUfIEnf)Ba44S2&2tp=X0%
zD_KDy4;;zyDd!V>i-=#e^P-_`%FG6?z_Nd&sqe(H%bN5BlN)EE(r%W~^|WtiNCcq4
z+~LcD{|~peS9p%H&Bmn(8UpO?+6U0!t`>zh32FFlgRxz_XZ^}Z$!4-96N~XTLLOFy
zNmr0Wz0h3i?Ya5Z@NZ3F=Ly`Mu`CW2(~@ZP4+7;ou_kBVptnQKjMH}6EQhDde#r^A
z>0<;H7NB#K>op|h3AD)VjOTt=j?^IJ{uwZSIB7rKSm0fc)h6zhnMSWeoBTzd<9;+H
zl}=OVSj$y5s_D2{vaX(^yE6Q7<WgB$i;F+qVkB+Riq(DLy{`a2&J=M_x#6Ra<|wPR
zycW55HkX~|ZUICrP%7#CiOVEuG>c=vpK44E9ld>Fj$bJ6<!+6^;!M4ekp%%Jd!3xh
zM*KhY%E>V0ug}wMAy(eM0hRyIJi*g{>-y>6)JK3XRQdk|>G8i{lmF{L$^ZS2@xXlv
z79eh$-(OR6-cQNYfTsqcOF88>G&YtBjJoom#v=&9mzb}1K6?TBwv?0FwU)7jF1_eU
zIXQ%`^9~Km=KV2vPXgkthLtfye45V&)!m;3!ONee;)z}V#BvQk+@3$`-5LvBq4K{Q
z0dMDcty5>67_eWnkDyg~La<m>UZ{?9dwY8ZyzW32mq+`|EVD^@?|1lPb~pq9!@hZS
zrg!9MLD$3CVt-WQo956jB_X0&yG8t+Io$&^s5qo&|Gdpqain}6zfONV$uY!YKr%nN
zka{={I_NK8TOu_-(;rm<+pC4hz2}(-8BXMpQBE+<{RgQ9EVabc0Qr?EFg}<_epv4q
zMAYH5+I|K$wYsE@?{`%pX;C$ofFc$NAecPQ&JjQaLEcQ&GE4;cE}dt}7a*2ZAmK6#
z=LI}@)6mkM9aldlz+bHZ=mGUJeF?y2Z)O2}?GK?|G?8d0gHiiZd(|&RJ_UeuA$g`|
z+S853#R=j7vMUi>@AMegD&hO4?JT=uomxu|zP&I&FdD&c?g>Y2$a_)$j2`&zygyx*
z{3t+0HQ;!Rc5uVgdJ@!nz2bYelOHf#WzzGYM>{-Y@uxr5b%4lo5Rekf;o{zV1AM^9
zvmxad4>AUP6IK2PRq!t%h~Xlt-mjIB(b2kz^w0^}4|Ali2+NVd(XP5Y9JCr6$`dRn
z@(v-t!~dKC!ksTrUiw#O;?MF{ShAi!qd@^Dw7P(TzR`HO!bI0z5c1&m3`Q&goH`F9
zu!9UINvq$EAr@$6|4uR2eJm`8Q1S4Zv9YuHM(2j#W{TpT@0H<kK{&B(eAi>s1YVqV
zo*XrNLDOcEoT%X{V5m6c{}MwLpy=!C^B}tJb2DpmtDNNLl0cwPy>l}*X#9YwVCeR#
zFK$M67<P)5l)KK<6^U!oP_J&0W_SkHEM2)s;Ce|7upzgM_Jh&rhxfsl&N~#E%o=#6
z0>q-mJao7*ptj*S_-##lg5#}fQ*%ahsPi}=%JkqLY%+WqU#YQ(HaMP6SA0BzEy6xF
zYjmTI9H{JgvJEAOMFdBe(@F+%O3FT;0`Jw=*z_lrvF&jY7A>Gorw}J_rJdbgs#LXJ
zwM7<6s97=|F|(rA;!(Z?JgJ=Dr>h4BQ5~ysYzPJf-B*>0{j4cAZpm3s-|(&%P0mG(
z-;&Mu6pSa9+FwJ(^`0(!uFhk_=^}&WjmgHw<_ZQD-|z%njA8K9Fa-m`2ymPX_74Fs
zE3)f)D9%DR1iV;Z&Oy<8y&AB3)Z?=9Tw=f3h<2cqkQ)alP1?)-*@nzRP?Y<z=zh5H
z8~@|c766|nL8EYYT3S}1TCW#dSf6i?abARgy<)t>;EiL>K4tILeq~2J7S;~Z5@UjP
zel!8Cw5UbSz8QeZ(s}x+&pnsES^)y>4Dikt+#wI=@#W%`?RU1>)rEeJQ5)~FF5na?
zWmZT5IyvGZ>=dDTljCd;Od^(g%%{{ZVBf+Ph}F$vA9_U9FmyH+Fy4Y+7aYZYhUWdn
zpvB{fW1Tej7kpHrz7CqxGt`y;YR=5{Y=~-6xfGNy^p9H46`OWHXAgTrYgOpefNPv9
zumgZRYPwcN0gsoO)42gpw<VW+u#YtsBM1JtL7Of}eB&ZSW2;tq-k-a9QhEzMed_#d
z5hB2;MEy+T1gy`B9IuVYCyvQk^lJNXKb;m22fNx+J{uH0Mna7;w!$CR(vVG5QHE2=
zq=BntPb0gLQ~ko_cL~;41&nQg(D|KVuy)P&JEtjyFZUeJ8+^65-U>k{SwZ71{*OMD
zG&ikagwNAbQtWX?>TG_sJ_A~3&EMVU-g7Wb`*tHxdCh*;QESev&2#u7pQ+lsLg8FF
z@aGiY;+P}!qd|j74{W8%=?c)1E!m0Z&Z<ML0Kx0TJ?QUv1NgJtwE>v5t`o#iPFKK*
zwPb(k^?+GEcphpma`Z7~Jtie^<~=v;TRaYReHD{W5AT#`NA3x@os9?Gc;&uN^zjhy
zY~#znyU3E)X(qZhMJ$-wH(Lhe!GqIro*lwtFt4|PnXo2b#_GXvekV=(wRfavw}8`9
z<0Rvvnc$+XT;1-({CMt)Ha3IqrT~}}+JJ{kchtKatPCpvwOaX}FK*=YHrBzkt={@9
zfpL-{YoeP;GQ>M}uG(Fvv`=PWT(IA`&RrZJK3n^lf4KMEoc?L+x2E^B$NnOSKwldj
z9qo`u9{<C7vYXGe7rsR>Yv06#px%PSta0A#0YDbF;+$f*od6cG?Roaxk124xM%J`~
z`?lKD@%(`M@8GuiUSB<do2+E9D&bh?ILA!N;0;H<$_$D7v`Pu5UX?=-!{iKlwc~($
zV*`Lr8npq&F5OtLMs*8#VZnFYLs8ITo$&cye0I_N*om+t*tr>h6t6|y2gc`IS8qXG
zf=M1D<EM89unRqbJEf7)eEFyLKdqO{)@qi0*OVXL70hA7y(PY03r59OLwI%A+cZGz
zE4^6n@VF{oQ&V%W3gEw$b(hS1e9zQWCoyOn(ocUHSl}Jxh#mFdmfDE;JnG6R6mbin
z@BP3Oe-;3_4tT~H@2<497$`|Cu$cAFVZZhKi#Wi$onZ!+nj9Vgr=v4ux<ARqmPnp_
zw4-~o@8)7m!NPB|#O8-DA6EBzKu$s7$4Y%)G|r-f%PCt8#$qOvBrw(4=&>k-+5}XN
zx~X~C7b*l42Z+i^|5Ee`PGis;{#hW_r;f<7!=YxP&D0D%(C4ez$ejD9sI6Z6ETK@H
zs%{Mqg>{3zE}~^*z3pmY<g;8ioJ@M%Ck!7+;*DCCj}LljA_n~|Iib9@3fY{TF$l+a
zi8zkk2B;l8%3ABY?}XHLcyJ3Wd-zZ3(RjBin3mi*1Ik9A_eW;R_4Jqu)zXvUP6elo
z28Yt=L#C@c78ffhQ2MFsP2RUI#>5~&bV<b38h_D8W8C355TWmTe0Ot_YA*IikkHsi
zs*mnpDN^ilG;_w;Q_92h4u81m#Za2Fi?hrYrmaQowI$b_Ay!ncw|gn&^(yzXOM0`I
z0Q@mI3YRvO;v0li(YHsvJ;yRKSR_P|N%G?TK44+r8fBZPGfC5j%N#&<d`~OF8;3Em
zFB~NPmeXNjy5lp_4Emh~W>bK|-+g2$i@fNbEDk$+*w)J#y{%8dD6|b|0rD^VPKiqO
z@h&5+Tz_k5$uu}FKXjy^kNj)sziCz0Bg3wXwwjLF#ptHx<sE@mt855!CNcr1xSvDy
zHmO7zV>hy?s5P^Rh9h(9-~GrJJ@DP|ZHM)X_W79IS(@Lq*H@>cm_@EYJ#Fw}_tzFE
zkTYSaqpVkiuM#p({q6qf^&Ve?h<T7@6ng))XYL`Iwo&uG>oD|Dc-0U9Gd1hC^B*<-
zIY>bS&|<LOM#@xOOW_?A|Mop<`l26U0Uq4C#jVXv=_1Kk`<_68DRw_OF_l!_;7jn}
zAep`&qD<FTX#jgn>2yYegTK{tmeuwc(iT@P80E3rc)r|;^A#|JnPD?1rlDmKUQP79
zFVt`cSgobFt(%IH_JHs}<GyYs;b-Yb+%!%!T7<hGBG6lQ?ZeViFP+{{aOcN)MV)H9
zRs-f)x&=b&D(2!qZu15a!hJV2%y8-5_=%%BY^Bmh8yNQpPABn}+`{QsrC*p|M1K9G
zk;79$UAg=U!`zF2dF@SKm1xG)1hw``uNTtA8Z`&WT{fvE8gHu}SIQ4X-I93oO2f8?
z4t1f)h*W3w&*Kq&E9CK0@u*HD$aoi(J-*?}=JfCfP1f}Y#jN)q7b4c@9bpI~vryt1
zbzKsZt(RW+WEcJ|!mA^H6N4l^u=mUou7{V;<^AnZPf<NNKZT5ts+d+sz2tv?h^0TE
zj)KV?$dKLHoa?b5gU@OI*9e18!;cX&B$v;#o*Z8y=y8|&2_R+XyX0?1Ja-za!CooQ
z`WH!gUWjjE|I$y@sM%~0L}qETAv>>mD1mM-3V3AFY4QcTXi8L?;rg{nDUu)kSSwq`
z%yZBMiPL6!&`64{m?5-(BHhID`fEk=Dx(^^@1@&%iZ=^d@nlWATPQ!IEB8Aah(krr
z5J!9P6>>+QkWx$`ultP)^LCZm=c?%V#-qte4t#h<t#9;3o&gF(y3Z6*;rzP<?Q0bR
z7XHQZkL15TokY?}-h%NM0VjWZBxZzkuj{YElu~lAV(C-e>%t>&k7k6W2N_b~ND^}K
zY&&rTl4Avwq2havsh>haRoty5Y$ppt0~`+2FPqRDg(F^GuRZ1Zie*eK$Wp#;d^lxi
z4ccki$+Lr;<zAv4KHmdiRAAqwzGHbJ;BvfhqGWYi;a`lB5hRZxPivJVA@I!SK6^v1
za|k@;oJ>-_4%M5a67$$UZ$#{Pkevd{{}ij8VV&EUs5>GPLg$=Mq5b_h8R_}5C+CAU
z7840s;t|pkm-n*6PsMD`u{12)957VH7cr8uLJCs}_m5DIn_kC`m9BMH+TRze$J>ZF
zQ7=`gY4xmnZI2V<|Gtj)Tn-FV^t85SUwoyS=_l;adt%=J6(^b-bW>Us_w}a}jPfxw
z97-J)RIcbZD3@DvM%cfdJ>ny<3=h4#&C8ca0mH4<=A4MAh=|vslwIxy-dm|285&`{
z?|N!hK}Xb)jeDKwD1?v>+&f-KAO*mF@r`)Li=%WqbK8&L6tQxZ{^?V<_2<-3K_`8_
zN2I@IBB2vL{_$5$G*M#D%X(auf$8CVn8UL(o=$}}WO$R~?vy!@WzmJ6Rv=EvFzRxF
z%;5|5Z)DhM@_7C?t_A)tx|9Dpi9zu3Md2@ZJi`vGoYC%T6FhO#{Okf>#-R@{X-$(V
z`iU5tayuhUeGPJnO`LdS{foF`%<Xl?>z(AICL>Sni6@#D#`g;OIMZK!&QKn;uceQ(
zN3uYEKo1kl=_4K=J}c1#%*VNzo&TI*z)US?GyQSx#Th}{Pxp5D+@4!-(iG`u5Lzsl
z)eD#5l*sdV@=&jw`R#i<-6+_3cotXLp_Za)oavAf@f$}o*#=P7omc~jj#5peb-dmI
zML|OEgZuGFRq>I-Ks<?4_z+j-Gzy#@>ldQ&3jnRE>d9r)sp&oHsTO4zSN;M{EWp(H
zIE#|MP-P<4k^$9+(zlnBe-3bo%KJKB&-9mis%qpjm<ulM8m+=?IYQGLN=pX~Az(vp
zU@~Zx1~$+ujsT>BGv?-YoNp~xh|;3}E8TgQY1*ThNzKGdtJ2Wm&X?ZXVK9Mo1rYL<
zwvoq&FrnBiN2!;`b!goj*6V*!o)zBGKfF1tdPJ%|JP_kG_j22d0qn;4Rhe%ov;7IQ
z>54;4Nb5w}N+G&A`v?sJ9WW@@3PhGjLfLRl#82&~{RCTk&BpE?r9P8QN1YBvfK)!^
z=6o#KxM1Uw<KQuE;X7yr0|RsAXXM-kQ?Iy2bbbv|B7XdmZ4BSFiu@cG;m&tZ7>p`7
zm*?B@cy;docsc8#N9D)6%Yof8(ncg_aUgKy2YMSTfKbuIK5>2x&i^$7ZdZ|Y3RMr-
zV=hBxz*Rn$c}9v)z%q9$Y9LkR#=SV3(w95oC1}lFA>@2|g1Yh+JGsi91huweNjWE~
zP#q;K|MO)G+FPqSwqy|wkO;$3_#F2Vzg>GH+@S~y*Bu>}{u7=M!ZaKg!IzJ|px^_a
z_O5;rR9ycnPNWRMID_pstl!pBRi3|h3{u&KLL~NoN0Pje!aazxLHsKxE2MZAH2Eb<
zFb!-8YoVj4W$XSscO-?oTbK-h^BRacLrfkszXo>&;%0<&$&<wnmr^_acOr>wPJt6f
zkrXSE;2%Zi20ZdS*8I}K-v3`Ep8W3@sQ!Djq6+^X%59=BFLzG89`Zqw4NA}!&2tz{
z+kb@7uzSl2{70yaD2R(G2R$=^kP*3m!f2jjbp9PibFRws@(Z4_%0@7s({s?z&2xUw
zzp9jpXPQ3+o#`bcB%V0Htp<CRkem<o+=%<n44#k?t3dUW&T0xu${}!EDh6jt2&hI?
z3xi{CVmcIC5h!?;5&u)&{I4FKzToRHDp1TBfvcRQqokx%)z_!@_V%s;$2*-4|2BHV
z)<$-*n%F|>E*DlkUEQoY+uyQ#zrRs|?nf%v+R7*@)@QzXe!s1M-tXU`Iy%&(T8f?3
z33x+ZO-)VU$cufS?tUa+il?eF>}<s-n7j@GPblf=MxH3mn%N4T%dkZMw7&kU@JgBA
z)ZnfLl8pjv6i88$Lni|9l-LRjQ_wS-ZWKqJqcaR{2peOc{|x#6_Gd7T`^vOyokPYC
z?z&1#`ylHjp0B66QI9sa<9V?EX9UyME5b&$JWD}|GJZwN_ku0|$KlM76#jSEk3?B!
z2xy51ctA8TDcJ)VsJe93p1<+$|GbE2I?O>RrW^Eur=uu=t5_CN@0f#mGC|u`d5u%C
z-Sqi05)nql6x*ksFP@Y!vFlmt^JNi)I!{Qh4BBGbaeBM;DyKH#CEm->0(zbdaQ#Gx
zO{py;|F~L7|IgH=kYOuD!`H?7jBc!CJ)NwPIhLXlWrUkVpl&fVq=oW7Z!u>OJjMB?
ziA(&pnvDUZ6R8t}{}{;+Q&&;-@qgYM%=w`hu3K5Ma}h{@`uNwVh+0<TEeL;F!uuzz
zi3z;-zhk24^I?jDDzk|Qw?JUi@3%}aBO%Y9eD@#YqI^rAfJh7ymA;aAt~~$r)90|M
ze+5F3ZIyp&Q{WJzmAyn`WcnxL>OWpB9FzNx_@yY-bNB@~MHLW(rJeWB7)C$e2>%sA
zC7%3c8iYpuD|YGs#XP2e&rW%uR$VEg9Tgp2^68WCD6b7@4hLXJsL+lC!MrX8g0iHv
zw0ANx?N=c8>&^4r5ldkz*m9OQc{X09Q)43sSZjqhrMaoJR>%kR6DzIp0SaTn`)qw<
z2S17|{*IEWZZKR`%Fuf_u}I%F@E9ctls$29Kzj<xDJo*e516G3|1%tKV!DCH4icR*
zich=3@a>Tft7Px(is6m@UL560D}6tFb+i+f*JwJ0(tP)|>VY8rlb^nj!$bn3G)Xb|
zvMI);V8Ayb>b2j!TWNJy<$toE$vX@5igKka>j6T99psI&Z^z%>C0XBoUwLwR*XYG2
zi>P9;nKlqnv9RksRizonJ~*D}qtm3y#iq4qzA1Yc^Pw!&F|Cfj7E@YMH?g?nCT+Ty
zOh$$=k;08Wu50qUBpalvzLZ-AQ7)<E6LlY>cPLF#a{fCW7`&Q3`s}kQ3o*km`=unx
z{d)7kg|YdXcVE)%hn2Vn-Xdo}XZ~@GjYQ0Ayd)pL3;^OAzt4>{mz^@zOy@z#iDsm8
zf(3DZzI)dE@Flo*>bgRHJ1O7{(zz((tV7d3V&X`zqiRKY&S-rmcK!{8>??g%eJrDT
zi2Z~mt3Wz=rsKM*^_XwV`c&uCtDN*jTz5u~X*r82ol!wcHoX5^u~?DD)XIZ{#f%j}
zf#rGB?J5h~ck1&18V-T)pIrUogQf_rVa(<|L-J<Z#y^O|Zf}+hZCqYjtnBr(I%$w+
znmvu{PpVte+tEhP{7!$<o7+S}QmsE!U*NSA`lPf-Tt0!xg)fRn0+uFOWMR72SKaUp
zNvRmcUcC-vbX54iDn0~8CjK2OWuk%Or^xewi>6{&ybi7zQ+cW7t(qKNVz`Z6mj0v9
zXB>ZKm;=h8<h`klX-F2jjRQeNqV6utle{rfcvhvatjOQ_@Ahd7w>CA#4$zqmHIbu+
zzqKv1JKRQBfAi?nkaGQ^*QxQBOT)yZTJ*DnqFqbLib8C>c@9RieWANf2`}NT%Kt}=
z=4l`A^n3nJtuhTx`MMS2#G8)aQ5+fb%KUt}3^XkUWJ!~q4IMQKDlc&a%4GVc3MOrL
zm+VX4bCQ)i^!%P`e&V&!2xzNg4<N8`N@FMA^T34vrQb%Qm-hV|Q}pa?vV`6KNDxOX
z$hGpQ)x3E{yl77cMur0m%lY(q#+ZIl*Zu9*D>}OPX5zlY{H$eTXw{lvUyd3>jX!Cs
zG)+25Dr&|NUCFTe_Ai}y8x0;=`Bh?ySRR&=CV7kEyhrKB*P*iy$BD2yJglGg>G6JA
zgdO~LsW_~Z?BKX7E%|~Cdz0m9?>x7K^Lcpt;r)C{x=EXx*^WmFI$O)I6C3YPR*t<H
z507KpO%ar0S=p;YXf3=!*}!`^J-DOGz(wv=u|tB7h>h^h`m8s1m-`l5!>LKx*>pNO
zI#VDVqs*YCp5axHL53>qabKX-7G{p@m=s+reoTXSYj(Qq<iu>G=ToI-&OB>AE1iIj
zYOI%>qEdBs8fLom<Q^f-yejqQ-kQaXQEJsM<;8?oL0(yhwjp^=dU7Ulm}B=E{FE@;
z(ev0^hL3ScOWzxAE4B>Wi=f8gXy_P9lxwCCi!GL8U9IEGTt^upx|3;1>V-6wh{~Lp
zsT6oDvm)(JPB^zFvYz|Zi>ev#U?j>W=b}?ZVRBT}=068#2(NLDUn}3kR{6@2ujzjK
z`0Yi|At()`-k$G_0I|-23$%=qQaUU${&NAwd=-%859|-s-oQ^BCQ1>X24L(i{P>21
z&prrIDBHCT)e>XbC^uh+ry)Mi@Jf%ziNAH!qRI@a(Tt*)9Z4!yS94ayczb|mzukuA
zIGCCgd%M9B6t`+mq|#7MaqRMg!kvI3FXg>&&IVq-%xRU}2fYk2=Ek5eDoQd$?D7;G
zzZ{FyN%)#SlGE3?{lc0mx^>-MbZkFZ^iI2Q{GrV{-GYrXczr8br=Yu}DmlJK@n_pJ
zp==~;=sT9ew>C%)B6rE>Lei8|Qr(w3BZ0pM7k=11cQa}V%?{?OYfSs1K5ZbMou6CG
z*I1l^6V?!LIM8%Jo?0-mux<l12&r*%DVdliIy6;MWK~otBp_|H5*<s_lp{}^tI>Xm
zP(y|?U-L=JkZUP<IADghFsvn)R6Km-MY1%7<W%u3B8w?+#fe73ZdLjB3~BBj9Z$C_
zetby?Vz7W=gri}3GoF*~Mkjl@mMZy!b7a@|fwD6HJgt@O2HE@apkeDy%s<8da7X5I
zR6}Ez+n6*>ia?BZ`_Wqd=I`2-`U%Ko?P?O+eQa|7cH0eWJ4n3yP|Eo-oTb+5)D*y$
zt+v47?*fmDr3!5DZh^>p3#9lZj%Cw4q=*+*RVjl63I<U9P6wSCnIjNF(*ziKAUlOP
z8A$e(KP%?i_E~-36;=6);q>&5=I#>qG_F~XU($=krHaD4FMB&NrGGw2m#PPmW8dBE
zep8)Brp#ihd=>MGLPnN>{48sR6FW!8N>-Wf=xip+b7Noi;jl@y#=m0zC)3?SbY=BI
zj#Mmf$*@<6#`2{rghkBaA@^GE)FTWXxusr*!kz8R-bbyTnE#k}yXkn!Co#uy1==+3
z`s}BRkxaJF{wQ|V4wgij|5t{PGthHOdd}0)4`2wNo>nKglj>*yyI<c2kc2S|irPUw
zOWfChD7^=%H|x1?1kIO+RdJt-zogGZ9>`)n+&XB`tPX)Ti#gy3n!w;6w}U#f>t>>q
zEa(tC+BG<_Zx(N_8L%11_Vbfbx^>hVavkW?m9Zw3gJ-$`HM^E`K7yBgs%DP>dMu);
zRbgYuXy?4}wt<G`N_MiDFfk{n#-`+^+{WpYeUEab>^OUD_$>H~362b}O@<fi-rX$q
z@4h%RQ5_*Vsb(%cQmaD_NyTNeoM{=YYRzQQeW&4xgm}n`!OEQ*Q)Yc@=J$~@3)#52
zpJ=irpOa2VQ^0o1{Nd*G>~w(Sz9V(AOpWSUhgjl!eRM?z)kV$*nyI(IEMZSg={yNg
zjTO(4bN;K7WkXPoXPZN;@Bb>(Etv+21KA_o2*{0Mpqa5=@t;Jjgf$k|I&D4fyT7j8
zFgT58OJ_dEh^FGxP~EePr0TnN{1nn9Jw*^fwG7W4&}B0+C`vx#8XG}J%a-HN)Wl*W
z%ekF(NjL|A9)O2WzhGyEIx+_nzp*d2vzI5zk)T1`s(UNpidz(sh^h?z%(nRus+#XG
z4&;bdNb!)7;Jok{?%VUUY)#U5gI4~Lu2z>8fn3fXBsBs88uKvdYFhJvHev}VcZl>6
ztR>Ey#S3hH{Z>&U8vnX|BZ^MFmGT9W@;UfHsTfYJ6JRel1bWVn63>!W`e)bR#r~|)
z-gKE-)(e`0(*<-^f>AR)4DBDbsh8BX&XuyJ8w^WieSh+Cj+tho=^-OgT;|vt3}qsp
zi>H||oW3*345O9()=y%U_u+tzO18YsPDx|P(;fS6HD3Ah@NScLH)*Lf%}ecM>2|W+
zQC5Q`vdnXY@~pqfX*k-d)$YEz%u7xle|mxJ%h6LqtMlFc+0`1C`Rq*e2bkyVO3Jb%
zxVS@5R)KjI7z7~u2_*fKwMm*>Elf8K+Uv!R1`fF%J@=(H#EpsMQqSFe`z#mXs^ix4
zaZ;%_l<L?jA%BZ12@^CytQ4=!bm{h35r^-Uy*Cg~99W44R5oqn-}7kJ^|=xxb%;a_
z@Y7Q!uV5;<6c|`gYDS?ODA*E+Y}?JBm2=72rb%0xk^h|{6w2!Qun|pH?mKpGR`o{d
z$>&^c{%&LA4UORs849jm=R;&Zxigv7Z*Yt#_DaOuQ|ikx`b}q_jJA%7!X<wXH<wV4
zp}3GBU)+CB%_3os#r8=CQ1LMc13w`ZTZwSeBcnsj;OB?;G_JAT+CC@{xK)zXD;pY#
zq}1(^bd>C~U*^BJ7upX;U2-1<p4?8B^DP+ozttR996kmlC&?ZNGY!dD+_K|JP!j9Z
zCw|!FZ6xs|MwKY9`%so1X(f18uWZWM0%<AQ+|5Txxopa;psS4VdEOZ*lB1CTQKyDX
zCku1mb60pgK?wAZEkIN9EI{q6tOAC!{OeQD1W9i{2m64<eVdimrbl9gDQx0Jpc>FY
zbx>e9(deXPUNNQ7;Zc$87qoqI;JNkse)PS}SECU{bmNZffYyw`UH6!(uU7Q!n09Dn
z7p<R_uPrws*%r4Q*~zL#|6Z~l3a?0>ZaY-DwW%~N{Cc}F<CK^*)@12-i{Ja}Q!PdA
zNv@YI$7!X?*XTouPSU8WeSSSh-UB$E(nWR|g*d_jVOqZ9fMAQ8x}$b1bZZr-+?eJ|
z6&X3&0Q;y5^xEcOoyt6`b`5rHsNDto&{mJ5?5Xy3{Cht$|FE2=Js8#xUypf*iMk+t
zI_YWp6!Cc%nfi6BgRUTPxhb9SdTC2FCYqmY>pZ=Wc(freKC=vl^)ioi>hsIs$Pmlh
z{)MT#&r+k_!4j`wc$8kGlbmGpyLNxUdS%0n7@fR1i?_TvNrIA&&c;Wevg7~80u;n-
zX<e|K6P;`1svZmZS5OE<ht{w_9G(r&spJjey6f;20@Ui`I70NZXrKt&Nw^Sjypzj!
z6khw5EU=J$GoQgxJ^N)Lcq&Qb7-L$g!|!e<gQ)hZm~`qxsEJGxO=B&-oT?`DDaCzX
zw#@3v=KI8hu%r|fJ$@UVt6HwuNoqfv)ju+4-cvES=bz2zF9|1;b2hWZDn#m2iZje6
z$tFxIdRn}jQslkmT5qc_@iaV;rqrS~r(Sl<t018s^YGA~ZY$frS)EdFFVyAS{sJe!
zX(L2eB%B5_Pc@gMmO^%E=TLo5Cg`8JqVc3NCkakUhpKCaQGRmm0X&wo2(GrPE+w29
z!I#?-Z5c#RIMK(id02C-M!E=P4DqWwSVYF^C-bO`Z+%4wbdoU^`RmJuC_M@<wMOsT
zH$I|!=t4=zNXv85^gZ(E(zYm-KGM3izAB!AiW8B-y;}{O$p9kqk~$4byB<u~XQ|P1
z1M#e%z-g|F<D^hM4J7p93Ha0jsWaSj+htKi7uN=3T^H{!-OaPu3KU&!+R{e72Y3kK
zS>C68y?XrzKXW+H4zdb)qI#A@f!w&ZoVf}lB+fq}LclGXYkuF>hj{-S6$VjI`5M20
zz$7<RIUDso)3*I~s%2LDvy8%?ObyZZ-q~(P0ei_}Z#1V}T*H1uvBuOCX_>DwzkXy#
zgm-BFuq6GiYRi$5H0t9)P#;QBKg3DK$j~CcM7mMKMo}W;=Y|A%D}d`0O3-&dTcmon
zsIV>iTU%{dxYZzF(Aj%!j;X}#kH*sv4uYh*o*HEL&9Hc-G%SMso7s@hzULOMJq1Dw
zG^q86=wI7R$pwWT>^#qh{G_T+uOI3qdo?OVYQ&0uyRNbr4J(8{T2mQnfxzxCkYN;u
z&mr3mc;*nUM6fF!{4r~2_XTNA_pJvSEf7=HK7I=dOfOZ+7pwug5Lr#lv;YU|j0NLf
z5Igb!gTdd(hNp4f35}Wp<PJBpVPVW_aShJvqL3_B=XemPdjtZtgqwgq<I%3KLX1#^
zo-}17oe8L^Zu{a;WVFTJUNBL#{s@3PrQ$y({u~04sq+&^if;XQR=A7xXQ_tVURUx8
zok7;(2UFb2K!&ovThd{W{BB)r@~;|?%Z9k`7*c(AtvElY{oK#1NhCt!aJQ587hPko
z&O6=sE`Ev~TD^^}bz+aj!gsB~I!TM*vT90+@l!U>5-MKIb+4VGEKxByrMR7<6<X)$
zIhXV*P7Z6!qc|now&j4&tZ>lD?9k!Yn~_nK(Gh!1n!2*&UcZ_<VBd38FTb2YZulAB
z*(841TuX-cMCX+Kz){`!z*~hzi7Z_C4X5{Md(mbAhF~qZVn5>UZ4MXgNQ+rfwEymu
zd6RsLQ-nJs46BEU@BDpN_9#p6mjEt#?59T#rR;;9#9u~j%IysW$?MlWR9Aly@<%Si
zQLZa#Ca%?dh9_0Rl_mSblu_`=90=~H=1{w5F+9?K8Sxp_i_c#(VTNC4&>9(?<`h$C
zeD^IfQ8R2_y4pfG+i<#1ef?$nc~@0fq}(6g7_Q8yTQ>+9f+Lr5R5%T*j`fU8L=ubS
znrn0^B9cHmt#&l{%k70o8ZrY86t?$PFc?lB2PNJD9gW+w%IR)aGvtta1sp1f)LUPM
z5*sp%@4lOE>gl>ShH`i%$9xJKf!q2qM8x$-do5#}kBXE9Mwpts;j42l$nA~t^Y9Wt
zH;z!-M#*6@3wwAtpE!F?j}62cB_IA#vkZ<YHUibHOcj28QCOU2%!e>5LpVkss`az9
zfsV|NGylVxOqmySMY#L;8BnKa&C7jN$AJHW6>u;5?hDz9_AcFl{aszaA<1hY-(UL?
zR4tJd^FfL3l&Yp7Iq5vBwzu1UZuJZ@3h7C8^80<z{<Xv{2a>8uNsjf^Qg!<RnU}GB
zbDu8EuZ+kM4@vy3+H?+x=^&{aB;(0RJVx2cy=ge9htXoqO35wpPtB?u4thNwsZc-o
z7Na`}jB*(@qYljb#wFXKJ`O?3ZKsDuQ{!Sa_2Ml`OALBA-o0KBC8b7sVEv$rIz;!?
zhkW>?Y?<~jG<X5;)^%1z%))qr1{B$o-inXkts~S2l$tfq!a4r=fcoRuL$U-$|C4o8
z{g%biQj-T}k2ZEU`7(3BlA4l$MLZS*?lCwLwc4+=&Ubv~k6;jGE`muE)-L%|jqs}S
zttkz@C_<~X$lO~=FUCOb{?hDcDN@cgJ3g9<7k^+{gZ5opUpLqQl%Kfr`xOWav%J{`
zp^{rhYOscUq8iJjU%TEKkiJ5{l?_tOp`v6H;$%uLBS!med5S%cEyDK`{`IGW1J83V
zj*)z(2!7@b1|G529|X}<J@9NDYc<3XF}jj}Nv|?~w$!KG1c-$o6jpdZ)3BJ1Q{np=
zaw*Be;`Mqm`eORCll}T|JtYg66Ne6FY3{%J_)aAbF&dzMHLvh`pN@K#$Q&v>aYRBJ
z7GC#XBy0H}9~)uhEiW>rP{H|`$He^%qQyqp=h0Z{IN`XxC`_lL&%a5(o(K>)sLgGw
zsgB8>omrAAQ7BN28p{=USQe&XEYseRl>2iqcGit|EZYLx9%zC|Nm^=~hVnm#{tTr3
z3HCzfadF$3Wn1wR@2E+=mVyGi<nXy14<OiCKyNe%Tw>;J`jd=}Z!$5LURDwm#G&At
zEjPO-gI=&I3eP4Q0hx3usVKC|pO3Q$MSei2$3SHdpWa)$5vD8<H;`iqoZ!F$xqgHf
zI(fKmN_Lhf;G0GuNMaHhK7efE1yrp$gdTGyH9%#V8g|_D#n|~Rd$JkSagP8h-bBWU
z2%NuFop?76A?R-$3X|r<pNs<i??4#QhTkwqFkd{;=T>4rMmq7~NiP3mV?lOgyixV_
z!_9iQrakn4r>HzC_u7l>erdP}eyb;*qu@m5TxZQG3O{TjRC03ZJfH{Ue3fK&Lfm8}
z&K8scqcU;-_VGN|s_)+O?jGGo4wrFX2N2-QDtiz}5#VNbJ%YvPZ{;I}Rlj=SACpZz
zS)Y0ch$S`W(#R}NygA-GC2RU7PsR$#zQ{^Rbrf0*n6ite;`yC+>NT0P$Ij9{@}TIO
z7VhtCnXN0?Xkkepu#^3IJlV9~Dj!O1JM(&=fIkn@%^sGM#q|4JdL^2*YJwR`a0dI?
zSh1K>g-rB8!m>R7y%62hy(+p}?9`+E>3gnOy>EDMwJmh1<dbLfZIxLUHjVCU%sjSV
z0B>C5=B&fOVk1fO$!Xn2+pbpMWewiQ1KxNj?C$LD7&uW9fBo5ReShm@VyBa<1_H0y
zGigH4YQRdXoA!I4E2Hge<Ok-8%AxGUiTD9ljxzD>@AhQ1n(V2WbYnGznes*}@=sZ-
zB6mOXnFPjeyF=OI@_xw}67vWnMhC|V;_-dsv>NF&R4dlW9p*xu85?CIW0vvH3DZ*B
z1Q_wT4vv@4#aBBwoS{(;@(;o8JmTZcSP1n>rGFa#H9I)HV6eST;dJ**rck=vgzR<a
zPE@g#BfsgDEPY?A_UTj>HV)^=SqRDFu8qf84fpEeb&(T3S@e_Fvyd)~?-%|F8X=mN
z+UJx|YUP`sPNT@MPhe(sijRKrN!fP3rUc9IyJs{<FX7|MWi;#|7b5O{hK426KIq4K
zG{VEhhV*9v&v-ueL7({Rpt(cjl8IFy=+@_@A{dbTcu@CC(G`gF?i<dQxe7kUX)(O9
zn@RZr&pS}=C@ltWV8tjbj^p=|&-2JWA8-l^+@Cd4fk(m_CD6yS=(YOfj~CK77rtUC
zq!(y6#mFI!;V480wJ*ozd_-=@e2Bb0e|N(cZ{t&-S;SV-#E`L_R8rMFCYLF{kFHyg
z;@`bgAiH+zgd4cG#P%Gv*pxX}+O0P{U)HM(M0e7$Y;(#LOQKO0xWtbvCcV1J6#f06
zz<STUvr!4*pMf4kgt+hzfz$5yc#3R0dCu94C0iz$uZv;OH=N2I)C<!_bZ{QIyO1<5
z`_Yk<zI_G3=pbT-w9vzFO}zfm!?z6q?lDVuzpGsrX9os}ESuc^PP$%mNew|Hj~#Zj
zUc(icKCzd#Y+{BS-_KC{x=j;LDcs_ntsD8O>!2OYTBIRE)Kfw_%OIU#CmeK$vB$kW
zzV)xQfB`PXgbV4eio=;;|07y=Datl-*GH?Lah)=K2&C`VnwJYQaJSP9Jah&pvC!Rc
z4BjU~vQh8UdB`wO(fle18^gMS;leTbFDTt!$yvY?+F&@DRM~xD!Ywjt?n>Umwp@39
z{azP#gLKX7DnkAhb#j**Q`edS3GhHXIkltYk4KaqRms%9zO+Yoc#Z;nj&tAY{giYw
zS0{6Z#3>ux_b{kbB1E8KC6padV=Jd>YaKiu>^#A4K>{7Go!GAn36zcB>Dfk`nU96?
zSNHJZ<uafd+dslxul<8lpT;G5tmfz(x7THP@m9{<^|k8ONPy>Q!0Ry%xJUZ*x!XOm
z9g~p7()nET$>zTy4%E8S222LocVy4)(jXO^m*HH!$OWN}m1@){pNxyc^-eMxMVPr8
z#mG!11g<Xv5RnTK9G8L^l+liAMi^(<Fi=`WjuI4Gou9Saj|bS{ooyi)*0|Zv!fuG(
z!K<*OtHZ@v$QT#YJ2hJ7Zb~(>MlqpzY!iaRPPQW}(LodT<K~*TtzM4$<YdW$4iV<C
z)-IwXr=>g-sdg@{2}qyKkiDP_&p~b@{5%dy*I)Z@_C_bV<>8}WK7F7glJ_AUW7aLs
zpkH^vOPwr+ldJ_u+PLA42dj9C_}#_+*kj!fuNP?%*}t`@>{+HN^wkOkuioFH-w)M{
zC4RvxMIqZUO)S9hOtT>9=E~z7TA|-Lcnl}3Jr7WcJ`BLFOLRG3(4hG;J2af!k1)ri
zs`4hv8ZioAz66Hw@#E4@A=<1&WR@xf1*C>oDA&g!^v%fboi9)?Dqx_^@~b4^TFv7I
z-u;BeI$R?R6r1Cl4V`nw7`r4Dr)A5>pXd=lBwrEf#VY(q3{=F;ieKbQ(Eajy&J49_
z@9VekRte6(tgUA&av{mbw|#8p*n#~5%$<WkxNg65W!9Miboxeypc2#sTo#AciH?=u
z&IP@hF0|Z?i+_Te?Zj2InYqlJI_rWtKTN4Aw6Hv$`!-$v@-gq6uNBeaK-Rm5`zFuV
zs-#t4FL+G_LTi@0i+{_LCX^Px!;#U{-Q-<0kuzz&z;pd#KVG@u{D?BYd+ULg!TD6;
z(01uzW0tn<&KZ&mpBpGiOzgpZ$Yrv-?34Jw|D%P-pvFD;XOVWx5S?slL9$0$cj0ij
z3hPff*&u>+z2q%BPFREbO^JNLmo3IU+pV7lqAb>Nl}+OsK8$@v?6^-;P*o?Sfr*ll
z>Z`F`-hO-URJVV-JpX@@_ZCcfblci)aDo%u-QC^Y-Q6KL1b27$V8J0lf=h6B*Wm8%
zewugfwfCxh&JQ^CeJLtM)zkFTvwQTkG4AW8pKWH}OD5Q$XXuzLM|9DfVmRh_#2FAN
zoZBLfvsucbJzgjtzUtFrwpZ87;Hb?h*G)8=FB(eCGNsC!@c8`-HjjqxPHG|YbH|3Z
zV`u;S$`)$DIDe(jBb|jx<f?0}^`@7Qoz{JXXyNpACy^#5-J~rvpx1e9=ulQO3irJq
zOmZ)6!iRe%5(!PdTkfX$cE9SjJdt3i={ayMPV)@Uv0@m4hfggU3+7XR#F=2&D}BE~
z?B0Gb({C9ce8V*v78Srre0fk1r)>lRjRZ*qvL;dLvEh#3zL4eS{{x<^91|K+VaM*z
z>7D@!&S=>kP~QF5nJCMy^|Ie7zo~`rG@)`Os74fwC@pqBb-z2jVEhM7z{T6){@|a_
zHsQd0CZ)^hll?W@g06`G;NHuRDB-a6Ifgx#Dy3j2N<<Vrjy+daDkp`02>$45%g4;+
zRGpaWup#=s*es$%UgR3*y#GbNvASB;$7e-Y*=9I5#`+a3ExpEhL|i4l^8Q_&a&2s+
zS7pnZm1@m&`qJ}#u+aq|O{d%UsZ<r5|53Ij)#f+BZz^#8PFj_7+rV0(!lnRg<*73Y
z)~FIiR%5*^cD1(H?m8KBa54Nk;7iwt`{tu~i9N#WU~$eT9*WDSIvpJ!Ttjm=T)YBj
zrSDc?YO#~SJ)*Ly>xU)OFww&ymB0a$uzQ02n2@YYLub&xb{BtR*|5?<&W;JUo9{NB
zT19>{@3E2pW1UOkSs-QqiRC^`C@dNN3H>wp`nG*syuC!4eRv#1;!vkg%g8DQ9(A)v
zh|?9FJyxFDRF0AMG!X_Keb^~pd&2udhs8*b{hKm*s_w~bb;gR>_)M{uPX?=3hVIJ-
zML%SDdKDyIYm~0v{HJ%0q=~n5dJ+A{t{wRINa^YFIhaGgUd|-+VOXWWHuTX-I%o5_
z1PJF^v&Tf)r6#iG8}AjbRc^8AR<p`)2fwAbU6&~3bnFw7lP57)ch^IipC}+#oh?MG
zSlI>^{Wg@#bOatqA3Qv$uDV9>Vm_5?7H7GQ3OTK23`l+@+Y^hWIP=~Ujf99oh-{dx
z__P%ULb$>hXjp3WR1ktBbw3!r0#5P{0uky3`juBEHa!X)v{%lvWkS*!h6oBK-x7Ky
zHVp2}LBRob#euUxq(9JEK1}ekA6DWwkw2DbQOy?o!S&#uPcSDx<$aLgVxtx&>DDgf
znVtTrjKzVL=Q(AiNzJ~;fSZQK7Z=6zAqCsap1e=+_}E@=bHbN4^9ty$G!ED=h^$u^
z<@Akr#uQ<mMWf=ucUEUhMayiEmo(xX93M=RxqFt=p1bAH7Y&infAulIV*7Oj-=>nr
znzkTum!=_pVv&|6lLm6tj$Eto;vT;8^mqxRxF0D|oc3@Eo+K#N2z8ad1ZD+(^SBxi
zq0yTzU(uA<tF$|KFBaW8w~C^neU~*b=2NX!)ZI#f7@3FU)2D?U@Q=PhU2;o9xYGu0
ze6&6?_Ba^3a3<c$dv@LeCE40jUG#g2ChMP<O5Ka`Hj|XM3y98buj_WDl~GTfD6r<R
zVLGPKS1BJE4Y7`1HCOhqdb=3@j)ryB=XJVUaKAjH(33e7y|h$Av=GkG(6z!2E8pmn
zscd?5^Y?1Ck#48QZFxjNL`O?O%W4%XexH4(aQLLJel&4c`|D7XANFw5l-$Vj-%1~u
zNIqsisSV$Dv(r%4T7%ArFX7U#oWY2%b1OcjjIEclM&TNeqHx5Muf&Y=czNAi$gv3F
z9Bu@^6$#g}h&DkIcEgc{VOB+XN+mW5H^wiTqkRuU+XE6d+Ov^5H3fe_9QTN05`p=z
ze4CL2pcaJ7)arOi<!*=5eeY5yxHc)s*_%IdGnc)jIO)K?GrkDSh;FQ!eUszOUdi-g
zofLFuci*9Je=u$aR42Q$Dhl<lWS=2iut6kCWWl;h^M>yrQ>3fyH(AUgf9Y0>HI($K
zQk$8nEmc(`DL(KRaHaF*YGoZuZa8oiJ!RDAN62bzWxkOr9nLvLthhhK?Wx>Y;RxOK
z1?!PE{{HGREdcv>IB1=K{vMu(c<1V*3TimC^=cL6!Y<}lckuGmRX>5(lE)r(Phk<A
z>EG#x(fXe@e7{BLxs4R06e<=q1!J*zJQ7BAzIc9{pF4Km8DL_G{#|aVVqsA{jQsjX
z<|ne_GOpj90keS4nnznf^z)y9a=kQi%RiEYd+O!#X$F4Q#8rF?YV6obW{8+(3Cfx~
z2?k*tDUY=W6G!V*!#*h4z_SsQg}A%SKmXD*GeEtCWD=cxfIpVlJMn|q4s(26ZiA4t
z%W#dWk+Qd7pqglsCL}z3C&6gT!p(pqb{99yL27@o^K#{S7b1x3Apdq2V(TmuQdZ4j
z)n#ER?@T0wlNg=GnO}VL+gVJPn0sYP$UWWx(>H}RxxdDe?}CVBU_Ouqw^^Hfq<Bo)
zR4PbFuE(P|P^{dd-*0rJXorjJ?lE}kIy!8w)Pp3-h4~|I^pw0&J(7Uj2Vx?8bM?Jn
z<(JB*vLpAMz&DcJAM~{^>jm4*1_<#67NTba_y|tu75hdQ@9cKISt~Q10zE?S%}LKM
zCK9>d7=lJyEOYVqeSK8@me=Xg*|EDGTaj}L5y>vg3@BvAv72-zZ8S7>i51#;x(k)w
zBR}oYYSWpJL&Uj$T`muRTltj<q+BhgR<y4!p-#;XBcr9~Jb8o2Mp$<0E&dEohm2Mo
zy%WKN_jsNAotpK1$u%*T$de`m@8|WYU+HHAl*IFQTp|aeVE8rfujhVOz~tz8;Q^D;
zP%xxR7!(4JgpRq}nDXL)Fn^^fuImT&nKNNSQ))g^UdGGY(XXoSy=ti~7jRrN)Ej6{
zVW~B%f6m*F0UtimC@=jM&Ce&!gV)eE2rWkA+>nq)-}*JhaJw;(_x<;W`6vwP7^R57
zl9AdE&>hR&4p#rTNnS!mq0_+}f{;e1iGz!pGhT>qWqj+UjDZiBF>DfRN_2NGw}`np
zrQ0{Vu5e~NU-Z76mX*|Jg!Sld6C->vZ@ff*e1|+i`DXDa%}*(r`5?;WoI_%nYv>C+
z1zQPO+TgWCJIyIhs#vpV{$lvgV*!3C_C1LMdu_VplwBQ|>7DbCQ&Sax4f^<0m-!JO
ztw_O;_2%2WTkpJ<SnQIY{nSMQ<Wh-X=fw$NhFHN7eZ7KTxJrx<Iue_3`F@nl4wrJe
zBf1PMmcfdIdbYp6x+jqjb9@s?tX81s@gOi6nT>WbIUR<3d?|3PvGJI@W?5@8644pb
zzUDw@-zy$e=+PeRzg3H4mqGox(ko&>Dw^e0>ivZ*FDN=VTq6dAXyx_0|9Sa4AngYD
zu>=epbpl9{;TTkt@x=-#D#)CM-hS6(9Cy4ox!XMZ6s8{{<%ZX+l^pC|Y$JDsaPTM1
zW+7E3|2{y1BqIi@PZL^gfhu}0nXbDINViYk_j>4@I<b&~ymqvk_<Q?4G}@l+8t8k?
zCV~i1m1#nP7qZ---z~apg=R0x4>~@?>>hyB#Q3aM(z5uv64?MZZXb7O<bct6Pu6RK
zSERRi`S}1p0!0YzsG0DA%bqb5k-!dudMi=LT-YP9=Qa<_?Ok~KYw0#68`^YiZ8B7I
zAL()-19}|1Eo)SxW-AxYSdEahFzw6UYK;(YUr0aib&T$zEYSvT74MtLa-Ur>Wl?;f
z{)4T6hhwil=V$`Dg{-!#a3MFVHr@U`!71r=X?k@HF!h+{sy<PsS~1cEip-MoP+TJ~
z%G;cFg*v2PM<Z9UzZRhle*W`@aqBa#KiBJtU9gm8?YVpJvzvkZoEDq=YZljgW?J;p
zAijS+`%hhbn;Qat1CQG!HV!$~U~!hWhef)*=TE_}79}G~5@Ptc_SXJZH?IqH75B8b
zW#kyF<J76-OMQu|N=d20cw83p4YDsK0St!Zki~LUbnmgYmJ%H+$fY-w$pIUDM&SL5
z`iQY{Q=@^^1oSU42_PA?%Y23rIKiLsvjml(P4QC=AvyNjfeaoET=qmglPDjG!!;KY
zFMC&I+BKmiC<LK5kk3~qh3MuF@9(@2;+TpRKJ@1_ne8(i5Dn+dXGbcQZF;#H5#irg
zB>Y0)byDrL702%{f#;Hpw#aKSe%uOm7T_k{!6jP#tc`h@lpp{?6#mKi2e7eNcE)dw
z8<rrVQBEl^RK^0co6cmi1MJgN^9=xkDGDgOZ;D)9{eMhx2DV2o?f;xMIL&BGei8%=
zcFqz(;1YehKR0+f%g??eni3ya)~9^D5=`hT@HSv(A<J+uPQv%dTstY=lDh5anAP4g
z+L7XcDXOAnba4<ZE%iySNkXP<EWbXr2+p?Aw*tq<_f{$1v-zTRP9Rt&?{$g$n(cmc
zOqo0<m%qy*p7(90@J`(0YDd9z^6Xtwx-;A^flw~@_6dU7NnOI==`IRI#|Z+NI?H<0
z7E#8tlku7KHKCS!#789Vvg)(KFUaGa*n&{$lKytcvAp-&)Dl1bUf)>sVxp$49|k)c
zXDX+X2?AKv&TpCfnoQd?o<1H8y2UpYloIi3sW-{Qk4IM+g>EwIot6Uf$MkWCIG9SE
zeC!7*OGR_Sk<l>TcjuK(b5JEu?ThO)x_L`gHmjR$#ls{N-sHDEqmLS@iKelCGIg(~
zhH8vW9-9xpp$-p{-nUJ?m%*LHl~f6+(-YJIMX{#rwnJ&w_|L*?FAez)NAs1{aDH#b
zQg`caoXj&SMe?p0F`b5_ZXGW{yl>;-Fe*@_WF9+DGxtee40x1Q^l^VPMcz|k&)%D6
zAsq?jR-MKJoZffAd}v;xGwKBIIv)C_tsqu(eWd+7rg>q*c7JgU7cPy^>|}O{@W-Pa
zJ5guVdOedno%e;wxT-&fp39M;?ELW)NQo2pN~LoZ=mo~Y5@~kcHb1BjrY@cR)Luz_
zykI(H4@~SH{EnsU!e76dr4H6N%BFJPVlqyW_(gvuH}F+%hgD<znsJ@&^M-4+4gnP^
z(E!ZtNbXNan;+3wuHh}<;-Beh-dC(afvNlAG;M%traxG#Dv;MJx>Z$!q(=(M**sf?
zbYo=Pxvb<8IhO$4ehGx7FGV^#>}+Fc#Jmm_iy~PI<Xg<oCw>IhlP82&35ML@ZUp6;
z5V(X}?)WwY@WCEVS;M0OP|z;Ia$q8e%^0ym&mx#acbZ6Es5c0_EWByKL)@V76WHC8
z%}~_PPnSfjQoTTSu`y`$!iEd#nTvQD*`ZY;=T8zO7b3ohuo?1MAx>+LZikBG=Haeh
z%cgd&#~zlK{01kSdk-5jxr_mKV;Vgw^+p3cnE}2j_{w=ZL&lpGxUA7Q_=@tc5cg?s
zlo>CT+Fq4q=q5_)-5-jsdzBb*S|<y{Nv<3W9H8TAp-~(-1-3y|#6VT4z!$wvp{N#|
zopF+m1eQBoiY2RRc%+a|4w<^``C}RkOL5S|&tm}4(Y`D6((M!-?M$S%r3z=C6Pl)*
zX0_?OFE2}y?iGjf$=kFL6~_5Ys54K*gdMToO35)1HPe9R?QEH9(Kq}`abSc>G6}K&
zHpQ!hPZ_=H*P^{1gsy2rsQ>ZY6m+ax72B=v?onj5xFLMYDV|nqzA^OKxZd{>3B+u=
zY#ol8^$g!1?+_D1ek^H!&+U%p#^cxZ+jTu%zU~=plc0lOM9L5FlU}D)u{;iD2Z`RD
z%!#)@c3j)~@pH^*yild_U$sPI>#gtTy-Q;NV2lw*3fe@3Rarc#)#Lo8MG}+&wqynY
zd%9+!(1$P*1OxLDXn#ux+&art3X5z39qjMd_7k9$iRv;ANK$auz5r-MD*)*vIFE2U
z*RdbV)pSI13!bqU3qRLEOyU!GkoAuVoaN9j_MIa62B>R<Zj#1~n35l=V>s-s!%j9w
z0SN-1Z&vS3hJ@VdN#ijpjQybZp{0TwAz+(991%v3G&F0U;&AkpTb0lD<B*CF@w>yC
zU!YS&S)2QC0;BicHUgkhm(%upEfdZnLApXMzI6wGo<CMM?Zv^LGKuBtkL(k4kAqX6
zuC-8jwxbM3vWB)IzY_o9>{GrEC{f|qkSaHU8YLP$z2wtN=30gsT~QwGvYHq|9pgH#
zsM_)B(U;sdam$qSJ{?`t)UCy|eBn!U>}vJOZWbuFaV1Z?dk?fCVi}IvluQ8uA-k6y
z(hQ7kq$}uP)dk@WR#9x&9CltMtV*(3@Gbeo7*|J><My+@fMch1iH)}8wWMaKdp@(j
zKnbm~7EEEPwPP<%(*&05r9DA3sxu$&Os93t0+z$+_?IL1=s}Me3yR?w^UvgK$=|`)
z1?_zE&@{SI1G_I&w2{S^3Va@!@NkPK49J|(Q(VS?l_jwjF5`_rd@RE8f|@ov)p^mX
zdbv&#J?x$Z6q>zOMj!KrKiWJB`l!6T{M|79tNO^M1T8F;X#d+3<-v<>f@TY^Nc(F|
zz0DG?HjWJ<DUifE6?w_E1?>wG3ePZU>W5*7^fh-+aQ$mVVI7RZ9FZ5R6de!)5_=f~
zZGwc*Di_>bZ9pl63KxuDU`R~NiQmt+zv&E!tWUz@p&`026l)VUR>E0AGxr%LK#{n|
z>xl<1TG#;9`5XG@ZXsmoa1>r8U4n(~gM?t{t;fyQGpQ~t=JkwN|NStI&yJ&kire>w
z4hT>8mnU506JQV%kBe~*!lGnb+}P(zq@3(3u#77L753pAIamuP(4=oyYaSHQv_sb>
z--Z;=u`gu_Bp|7XR+~ZyCJ-2~b3}4LNCKR^W5@XAEol#<_B(w@2&E>@thd%KhlvQ0
z1MQFxLvQ*Vk*r&J3=QU;dGG6B@&s?1b(DT^UuAp=z_O3Bxlxcx_&S_33dh`Z?xhXX
zC}g^g-+xIOaOTM9Q)TwR8C^fXc<Po~Bj>Sd49k!T*hbsIB$?E3b^qKpe($l}YIRFm
zcC1w_s3cO+B30+@dj4w{YTjv(K{~|`qdwKZ;#*P<U7B>}leJ7zMa`>WV(|-CV^FsL
z>z|=E(yHHEpD+;I3>F98t(H|(u7PY>s!N;0#uMb(s@&6^&9(tbMqsxw&(N)3ZR+Df
za<!dEW$VKs!PV?D2@O3#bh3xc{2=o4H;y~`3%HRiTe+hJbV%~g!OgSmc0*|@h^uq*
zSQpV;d-m_+wTHcOEoCG0oW2T6_KjE0j6GW^I6GtZZw&)5;D~>X_OwK|G7u$(p2pAy
zSS-!Jw-3Y4%|%33Wpn=suRVSJi3&bdIhMIQWHdmVT6Z%^v`LzWZp%It+<`rmx)RB5
zB_v;Sv*cW1I!B!-23u?PG<&c=ru&CpPdWO|ddf5Eg{dY26-~pN!Ry|R0_tpPsc?T$
z%$V14pXJ12y5@r$Yz=KjnKe3QFy0g5V1)Vir9vK8q^%`(q?)t%8tGRgi6mzZn0~ed
zzDMu7TsaiVL_FBUSb(*|IS#`9Uz4R0>UdWqz2ND@PagZ>@-Qx<h{7~~j|IJ*aC6}E
zQiAEe$VX5*gJUc^#C5h9o`+crYHmU7RFU*IqccmgIv{{&IrC4W4@B(gvj#gYZ@fMk
zUeZ5}-IKBC@rwUm-Z6T+G;d5K6{dV03UNDn)*N~{{X3>(8vJIi@`!=9$VhKLj!r`Q
z>LX^|m{w>K8ZkSAG<;<41)T>69ye|F{7$}qOF)K)UCG|B^MLMKw3lR%Y~1$C{;fKW
z_x|{;hAi5_c@k-O&?W|H*g>%|%X5(aYM!qI>zS}l&%dN{pDJ!B&<sriW@216DP(=k
zh+>ETygknJAw5#X`Q~g^cFk`CFs*IR2Q$(MRz$#Ilag;a)B}nhD5sxJYV;wqc?LAj
zLj#<B9*v+Lon;@ZsXozpFPX6v$)(9Lg(z~*R%uXz#p#mg`b}$J!BFdmhboE(m|Qps
zVQ#(bdbvb#{7@_#-@cm6g2IFie)a*7{LBCj<sF+qH&^9?Ytnu)#!FOT<7NCylRama
zb(~r5PeEf5TsG_ZQBU#9$j#FpmVTbNU2Y^(+$flU?TzZ4=B73??_y+svf1ZNj@TYl
zXm`a!*a63;dCSekLXBy9eR<KtdCy{Lv%Jo0wemZh+-$dg)K4!K)CeW{HO9Yc-XG&k
zFti1LSP^U4yeV2RO1+xP&krVVq*Tj!I3(mcERxyDJBJ!BB%@<{C8ca_q>|AYBmp*p
zC_20&c<;7}lp;9Ho{3pV)a-aiFkH|E(Mis79u1RUczhyW*vu0AY&~30r<Ge8;PFG7
z=_qM0Z=vADgF?mHJsCiGqn_Xc?;ne}R}(ROwf#+<WB+QQiP4nn<WhIMt*Aye%qUUK
z!Bm(tZl&MH5Iq4BZ8sjNZyTte?T|cc%~R^@I0HqLlBqYStB|$qTAQs4Ni9;`w|`5X
zeN{OIuaXsJW2Th0<Yl(?VDZ=q>)Gi|qfh-Mz$;fwXN_Jr5utp&!6#?1rds)YfXcNg
z7W}ZuY^LR`LTsh=yTECOf3--f$L76}Svvak;c3Cd>?ZiJ#>iPrsOKOJBeyOs7-nh(
zy|8G55Zae7lS6`n;djP$L*auq7wE?1VOiI)3RE!#=RxPLG7Zav>!=(?uTYcT575f*
zU6MV^JAzxLb>dIf5Z!|bqcr=$YM?%y9|9#cM-6H-Hn2yQvO>kgD~?{XOVr)(&JPOa
zj^ktNglx(Aq9A#0qf~b*;ULrZUCTQ!jZip5ld?{*DlwZH&T!liA}c2ABdjDV^xpTl
z&+R#Hx1Tc1)(Fm(q-3FE%Cl4Oy@zDeqU#RUY-d92cW*@8;7cjI8HIO~&g1G_3ve22
zmzrX4j@4>~yJp7tO2VK8bgOCgV1k0_AiKkBYAvvYaBNgeur203^X;aKT&r_AJ!Qa>
zvYGLyeI-%^!GJ>G-0*iIj%`=5Z@W#+{)2Fi3)>yjyqwj=<nM#Iq@<MmQ<*#+Q*E3s
z2gz|B2R*DPD{V}*d-s=-SQ$50h}G46?yKp7*LmW$ev_;|WB%C$7oMq$JY8Z|4g5xR
z71M;@=*k^X69%>ujKPqkch;=4hS`>8Lm$rjvqAa#aQ0`O?8Am<#?2HwT55#3$Iekf
z`Ct_IYsJ&qtol*&UW79t9<bw@YI>AXVjsQ|W^1j|Az#dDjq06zTaPns`c}@3JW|*^
zb3A;7l;m$woFci3w3)KYIA>Rqb8OlH@+&%g*%3wv&B4)Mi05P9b=hKg>;p~}|3C|i
zK~KxU`Pru?9XhjJ9Z-oPr^P1b6H`$QrhO@fp<LpYk#fvSW^D8S6(C+voP1O`m`U1G
z;*w@9fKO`C!K69vcZ}<?@rPvRekq%@gHsC!aSLvuv6FXy%9(p8XdOQJdYxZlocTQ9
z{<3HGOxbcRuI4rVd!mhmiOp)A{$!ES>L5urSd6#V*{ryy+h@KCHA36(b4dZu-|qc>
z0gTuw!KXV*z4p!NiBR|(0c&z=^%;NUa@MQi?xm%x#{pYeei(hOyET`#LW^L;yl)#{
z?pEAFzE#hlKWNzPTYg5Nz$AZ9)v>Dv^Arv2o#1u(bJp?;&{<Nd6_i_Eqo##hoQhvo
z>XMMrG>mWQHLIJRdR`{SR-IHS@oMi-&&gCH!#JyRq~@V`jl7C?;yu3<O@Am`iWTG&
zm7KY?^Gb4>j{^1wuU|~;pqh7QP>Sy3|Lh*@(<eQ*^C`FIOzP~Fe>J91)A)+u>QDwf
zGG8oK&@w#%oomVK3YYHM_v1}naGv$Z)M6N)Z&^~^v+qJF=V+yQ>1u5x+iWe`^KIV-
z{TgGvZ|5>*XB+YF$dOAI)V0*5yIC%nI_5Ly)4fWgejcv-g_E;hLJxf-RKqYUC=YY6
z0(1Ch6shYpsiFX1yHVr;=%`T&^s${?>8-pOXd4{qb{=KkYhTJaxn<m=AD-D_v9D6R
zOV#ip<)Y?ZS>ivm84oL^?#Q`*Kg)9c20-vLb^)xTqX~bpJQA}L7|MpG>g~?MQPC+z
z{t?1u5?MRf?&Lz)p1ybsD4dehu+Oc1HH!iY;WG?ppaoz5VkKsy9e`~0J+`yRU*x^E
z49{@jGhm4a2WN^C&b+J@*_ymJUfi6nY3sMV0zCN**~~`eCxeUf@^w|mH8zehoVip5
ze13_$BfPgIyC^Gg!_{J4uDzCp-MG(h9vVTNDE6zY2HFkFw0%Iae$9_mTAd7ejK&`u
zLdVN)6}W!Gyi!-k^NKUjrkDX74~bgS?aSov*i|nqKg+z|#AL2%yJ1c5SwUr|+b36e
zRCpQJ^`BGWPl6BO@N4TMT(Lm_D8m;C)lOTS)+7T)vNyRcDi{MUJGxfX_SFfriS`K`
zYmqR-DuA~j){SYZA7@&!FSB1vQ)CV3%gTmetkx}K>MA?rwK+6t=*HGhSl0`$d5F0n
ztZGd4e6)IN8rny7Rwd0VzEZQKaU^Kbd><w!+4IKxN`tL@DGDqrg+Di0a{9jOUyb`%
zXa=Y+d2E1kS>DH9ZNsKLbU;AmxopMQ4>uN+JTIt%7rf2}xusgz^YfRwJJnWz<G!%k
z6=yl_d)0WZqQ8ItIhAjfljYs-ZMDQc&_ON@A~9cB$})WMT%|oSm=IH82p2-Kk<ROL
zFi3SZb@5J0L#I~5JY6WXO3UThkyCR&e)^(g&-X7mD#QwVKBGkIh(v^q_5saUS-efr
zj;d@x2$5|-rAsh(O_|nixyxNNq_(R}W68#tC1uW@SClE+eU<7@6f}ow5txb;eZt#X
zr$T-6%Gr_O6j$*<7u)t(=+>0!q9rFQ3vhkc?;QjsQj<b9;lW+ze($&YsD&dhZ1B;X
z*5;|jz`*Ey-IiBAF&2NTQ*#n{UqKFcUj<6f96`D9X=?60aGwU6JWM;I>}qmXV%Uu0
z9EIuJRt98Xj{yE%gWl_*_J;~#udu6mbr!2-z_(?OHd3-QJqb<_w;W`LQvl+?&u%N3
zG9h3?p;9vcS<8(F)b4G%{_2qd#q8-N#c5iIXJl0D*QiV3h=B_F@Gm!nD`4CJXb+TZ
zpFX0+PqkBxqko6$Qc_wuqRy&G-LqbodNumTF`$(|b{#1c*;boow2oq;j_$qGk+`d^
zW|AI*YkwB#2lLW?j;pE$CrKn43J~ztZzP+!lMyK$V-&W1iriy1lXBKF?A11^mZFe4
z!J7D0&P}~RoKy;0+0-QZlymKOwLWV0#<qL-x1svwZg_XP$#(}txuM8EXzNQ=>;0;L
z)9Z}E$Y1#8xBBXY99`mlxEzXX&k7cgB-xYqE~F@^@y9=g>b8#;ZLT%sq5?b;oiW+1
zf_wB@Bpt7O5>GcabTj*v@jZ8E<m+$zYWl6Gr0?@Oh(Lpt!9Lo4NdZWv+g{gM`;GHm
z>h5*KeG7idi|4Rbwn=@TT_TOCaV|8f7*k#kZ}D`$8C~bjcudSrrN}4?arG@uE;I&G
zUXnE>i{5wG&u{_z&m%^7%Ta#mGBFcmWKzmD{1_fdF+Pc=P|#G=@~LzoR5e@zBJ8_n
zgHO1;=JhrL^=z57W6Cn>cI9-x*ymE=_uQiDeK?K9NJ`07cxcq-<2V=IZ9mplz!lna
zJeE0ab}TD*c1Z}lQCO_r2EcDS2X*<rF?kJ+AU{oW*;s=>k%{;E@Ob{@eLRE45-=&V
ziM`4;HA9v8bI0X$&{;H{e`;P^Y|J0ij;M>pJ1yX6OTO;2EDRGA{p}FC=`%~(pn(d;
zkU<j0P?wi$f<gaS^76QVC_doK<{<^Bl~2+E(Ms||lI7#cu!Q&sY_1%jUPriG_H*A=
zPLhuHr%A0dC0S999KAP|5*y684R0hR+0zB$H))`irxtTl{2fhHN60Y}6^#ssgtiZ1
zHST`&vVkm}JSt*D{)FfQ9snf9N{aVtt?}QL^`E?>-*s-{zr|urimc%}eMfdEQ}5J!
z@71`v`+eCCPdx<U!#()hq9Aj|tv@iu>1xb=WY&Y`)!MtTh;*dm+`yvEVagxla6`s`
z*`uKKA&^FXO#;8;&hou+@*B@4vX;O9wV&I;a&3zDEjTFsOb0n{ltyKmQ9;Sjy@~l?
z>HFT$_gnX!QKeNV_QyuwRbi_ItJ<4`YW=@%tCaOUvVUJK%KT|^H<2CowLu!EfO6_b
zkD;s65?iwq1Nax~;ik9f*K&WIKTJTH-?*Gst6J#oJal`9heUWlPM;F$_LMjbQ>d~2
zgW3FFFW?uZT%xTtEOr_YK1b=+u4~u%&T%&BRw21@3z~Vm#2Bu}J5S?0SM^Z5Lu4>4
z#OYy|Ow*pdpYXnFG$eA`Y!&<O=sDfp#*c3OUud$VA!4iNSf-VCOMSf3kFTntJ{3^X
zYqziA=I8tWGT@Q<&Ppl3;K;Z~r$quG#Ot212f$OFrO3;fq}@|-oUcwOQ&`X|<Efn0
z2miwBkV-`ssk=t7+Go(*V(&<sn<o@nq^9dsy06;pO;pjTP^fwr%W_OwOw3LHmO5}t
zP0J)M020WLkGmzti*AhwP7nHeeI{qt^MPttqfi|qiE}vR=>q4AzD|-I6%1>F|489K
z`skPcq_jX}F+frO7bzp3k`4dAK|}eJpZ_l-0VM|;12P+Cmm}2=h;kKBz_{f2(R~4T
zN4onrAf@qmIxP99_`f=-osbq13V8nqnGE({w~Q1Hx&eSI4*G+Dk+QL=!N9`4b^w2S
zf?4X%GWp%@{q_EXK21L~G^AXm&-Vc--USMK(*QKcyZUnKRDr|@P_0D{@Ut$e%DDBb
zG{|Ms2^aLeDsB#@de(US>{~CzGX(tF{eB&l6c&p5J{?j#0-CB<Yg95$z>!sd@f!e0
zv-tCU5P<tqNZ>jKmuuDbcN9kh(!rt505Bl$aGyGqL?L5mSE=i|WahfbQm$P;?1>G~
zD=j)`cn}J)rV*?L13dkmzIOyDYOFdA;5EE?0##|YT*o_zcc1HnrE?3LljOOjwClf;
z0XOQp3Lbs|>*OIIDoLZqRzdFtpzJT-5Jbn~@eBjX(UZ3E3>FE%4jQ0FZtj#Q4wQ@=
zw0tY`J7y+c4SV)b+ruYLu^Nu;Ybfmj9YL-i`jUjF-I-tsAeVg(XG-GaT0fY$FAv*s
z0ADwE5$+aHHZ8)yiCBA)2nZP9dz~;X2R7R*F?&@ByuZ}y0(`kEplE;O1Evj-zB2ue
zk05$BfWNFm`<;IhPPq7ijD3ErXk1GK$k0m@0gCvq!1XqiD<>-|rmlHkk2UZ=f5^@(
zvK&K6$x_tLXKfMr9}FjV(@kO%Zf?H4r<y6Ak7aSGd}p;H2DVHqP%$u`{XQeO6hAj*
z)dFfMrx`hp6rH#Sr-Oy6T<^!-w1zXs4~UvRg20P|$5sS|jSp)1B`6|pw=GcknhXGe
z)&2on7ZC#~{<M$9$IVN4rF=H`QYiA#Jy0n)&|!uZ@Z6Nvxv#e5rI`FZ@UQ7-X?gir
zHvrkIf4$Xuyij#tsoOlw1Aqgom@pfj`%r7vxLuCi!{D&CcDcac_|1k<tI7ylIaVBk
zI5;>OyU%r;nevDK0LV-C61hzDn|n=k6=W}<F}7akFLcz@)c4N-)~OAkIp5u#udf4T
z;44lYY6CT2=cud7R$2kgul(eCV@XJ>Xb!Qx?FVO<N1OlGurT)Lv@@-Vb2c`*RX`EW
zUXiqy?NT5S*Ra3~6aby)i$x9F5;Qp&1Uw%rJ1*g0XQ;?0hGx0Ug5k#vyt&L*XjXT7
z0UQCZKQ706uK+dL=JD)Bed$FBzzwwldbLwW0>2kW9YpLd)Njj*GKH-;wDz1)DiV%W
zfZ|XjWF0W_*g2gl2)hULa@Y7ygG6`OtM_BV*}g#WdNA)mZv@JzPqst%=Q}L!;~qVY
z4do24@EhH)SNXSEw+w7~2+qUbZ9U*ylIhO8L)k?NlyV^GdCH+;0oXOQTz);BX+}oI
zo5Ao8w6pvu`kFi{9Cn+Fs@ssCtpLlK{pK?VtDk}AVbSz&iZIK@54g7N(lLOaXSG{d
z4DA9+?FkyhSTswc!Sw5Za3b8g1k-BwZUzz)JjSBmv%If{fcmUFp&&@WpaDasLc89^
zy)1}{-PXMb#ct?UA2_jV8<<f>fB0{r(%?qUWlo}uVAyn+1y8hS#~*p5()HWHkRRwf
zc7f`PB}|_;y5a6$XFz9=sSgya_+=JbCA<S6Xd_d|G;Z@`mRHZuXpm4G6M%Vz8{nWv
zA|4%oJs|+(eotvW5bkw0OVjVxOKe<3$?h<NBs*xWgx(sc95Si2)wdWq%){TNtC8W}
z3~1tzS(yRi{Sv?z-u>N!P)ov}sL+7E0!9?hb6S^#DbnB309RQ7(9Q3BFS`>YN*9bl
zs_ub;Rzd^s%aE7H{oJ?uIf+6bq%|W<ujw>@3x0sBJ%Bgn)Quz9vZf6gYFC)DXUTMG
z@i%Ue&u^^{t+_H!YRi-@4)xpw0?h_yTcq>`kg9Az6QUgFyVvyqvilVv5Tply*IQpG
z0kJQ%qcC`)!)LV3d(Fl_z^dFf7bvjcSofa64%N_eQCk$wh5NQ_+o)9#yN%M&^$&e}
z<uXX<0iCFz_X-L&8w76Ds3&v?NQ_u?m{1Zv6w789$LOCyIm#4E%yJPC2OAg<-kq&3
zhvxG?9Tdzr2_k*@xs)#ki&H7J_?d`Wm*E(-s0EYDsSq%tyR7lJHqXp0^*=avI;kE|
zhlVA;iD+jmJpBNs7n?-4z%sNdHrOl~({$I4bC2vPb<g0I)(r}bb7@{(T?OWVA=wA3
zWxK6;I347L{dEcmO>`h6A0m+tM_)(l12Ku|XPUnIC^+~AQA!UuA(+H)4*S6|zbO;g
zj42963t)?WU8p=Y*bcJB<o9kG!?>9s5s1O#X;3DHy9|SV0m8%<zrs#<H7LJVnw6d4
zF`!ji-yuw-hQS3Lbad4cI?txY(pIxmz3r^!*hU1=g?vGZkJofC9^f)5v$<-5NR0CK
zr}5wmU?b7iEdp3&YuAgoAO!9~@&mvtQiuQbMn&hcuLbWFmXUc0VFC@y5Rkjq1X7^Z
zxyA<|`<_UU^{xXTkjY5O{fDlNraG7gfEv+3&g@QBx{>2~L}e}MBw%_5h#Oa7SS4(X
zTc^MGv|Xjye)I^><;Y{Irr{#3&Nm_l>m42HcG8*`#a#O9ogud`Xa0@@zqc@5;Dkke
ziQ$RcOBV<I0?=UKS#KmvcmNXcSIg`YWfFLjgIJz3&P*sItCP5U{Dgz7AP8hPV3gP3
zlyG#c<`byM#9WQRW`jApL=1-hbOr4Z5}9@$3Ch|%^;>XN=*e>Nciwy2M*LmiE<vi`
z1bsg~yf<L)@cSE3?pQqcq4}Dp(kT$&ylw%&lLie6;~-GDibCnk_c#k-7L0UsaxJc>
zU#sFg5fQ~`k<5gBd$%RmD(Q*;`(b2U!oIyd+aFsw6DCHkXY+28C(EH%z{4&kf7GU&
zUQ1;U-7SW)MgxSt#f}u^)sx?#v7o7`eS+tiELR$aWD>^^cqK!ksy0ALOPC*`K`A($
zhbnP!h9XFR4PGc2y(JVinl`U+Kzz4XxJLYM<nEd*2rwpq$cN8?Y9q2}N}I=yS&E9U
znra8|dd&II<ol#X;42DHspYLt0p7#cT!Q-w%IYsRgy0rNd>uTKl7f9sz)1Jkdq;4$
zcLS_Y1X93((67OEdH!9_W)0-3kflb?)wQ9{etivw=?cH%2+ms(aK%`jJ_Pveq$1y;
z)U`1)RV_7i9YfesttJ=0Z(0uDK^Zj^T46&X0Yl5Z?&9Z)9(d!eN-uw2(POSWP5EtX
zcxbp~f-yt#YX1aDzP(E82$;Ma(sw5VyEu^jeiZ-4POJy0%ScD5HWWL?BtyedNR*DQ
z$|$nM@a*cfkEo|*Cy4o8$k8Z)EhM37QrcAmkc8)5(jd;bQ2>gM1&#)Q{x~%#Q03q$
z_Xnf?*%^-nqYuYd;?#5CL|9F%K8PEew;!L7V8ntkAd)Uti?_V7*U#B2`E91oN@e+-
z5VH<-+Bp0NF?jOuh}H!vl6;^9t!fl;cRgT!rA$QPK7AoN$sE)esK}yihUdmN5qoUN
zN0(U#Ipr6QBr8@1w}T;^fn~<v@Nt9V_l9J4as@T_<$w8Cqm>f{-6Fi4ol!)BNNCOi
zQyXpi^J*l0T+K>D@FXb91k#-tMpR*$hJh*DlnSslj@AwUm!Amq6@Meawfg{r9B$1w
zqddp)0B#T=P|U9gtfNHVk)+Oir_}mBBNo$>^rk;GIPZ@)fW*g}V?(bx*i{BtJ74iV
zqEvMO^3>D%0p%(seei3fVC)d)|G1k5wdP|s+fgXuhrtRh*RrJf^)9O@2Y-%b&5nQf
zz;ME-!ST6S^Zt$<Pm|vjhrnV$QmPL1Zpt&j3XB{@Z$c6m7l(zUBe|)CUFb^oMRN!F
zjJE9#I1<(5!&QM*dci4o_kPrgzvJ^;?gFzujgw0zYUqx<cO(eu)?zZnWLpsZ$WHHp
zK%zr--%I^?1<UN-Z3Rj4iqadl$u|&bvmdm@<zlT!_)-O*4y^#4<Qp9|lvP@)TdatH
z`9MK1^1vk%>aQ8ElVB*75+qMC==qJVpwm|(U(i0xHQUMn-(T(%S;x*TxP%+VSfj!G
zJec{=s%}0JjmSiM=q<F1kO46jA91CcYH7ni432FB%~sqM1c8W)&y1$Ei~+<@4{S(+
zm^(^{J1Zv|)*7ic#meNB5G-`XOR$`1tsdYtP@Wd4X~tZHvoc<{0~HhrD-tiicb58Q
zxw&S=_5v&+gkdDRoh%Ovow}bd#(B;ce>1H-M1PMs&H?SQE#+49pX@-e2P5KWnsQu4
z#N)2hjf%-v<xn-;A=9P-Gs5!$Jd80S!SUI&7l%kv0^S(i<pxy@d~Q>)-KA3;IA9oS
ztkkd}02>?oV^QATy1PFZ<c~!}^f^HplY;>c-93YrqpXRX4hFLnx?l@m+<kOe7Vt+5
z$!D$HXOR78b>@VcfRTL|>DQRVt0)Qh>gTMBAw^`puJHMJ_+3)P1_a;^kgzZwk?0da
z?m?>`Zn_Y|bqC0b%HX7c<0az(f+WhOGA#CVq$tgX0{@b&Z$B8oQVMxyN8mG}CPk?U
zv_)1S3F%70sk^O(1ER7)a6WuRc$7(V1UY(DxA{okGKLf3WY$5A^QgeH9GH6vgvSFc
zBy&llKQ}Upg4a+=w>n~*^FN=|I(cAEfQl2mf?3*D!N7To5`2o5a<dkAM4$L%q`oN}
zx~h-z)FYS6zY4HZRS`;*J^}1~FEeZi%f1qRqPR&5gt$bnymt(%OwTbr12qm-|LK7^
z&q#6MBO^lX(3T|Y4Q<e>IO}4gL~pE^@FI}V7YTm26DUo!kD*pIRMVFKM5iPd8~!#}
zyMNjNtD-_jF9u(FchLv}cSE`OlizVgqm8kJKS-;*>Zcp~JYtc~>KK(ymPo{c2_{wk
z?zmKeyB<OjRHcO88a(0%2z#_;M_*wE%qe<YQ%&s$%fkW&>;Vf>8DS1aMJ!}WR8wy8
z5fcOP^=+0E{sDJ@Ic2)h833p%Z>OC^XkdfVkPy08G;JjOV7p}iXNv9V*fUjaUWz>&
zBT20s64*Qn_cM2VRJc0D8bnAo2wBR!pOn>GtfP#RAlDTMZ<4<X9kHdMg}Scw&?NVF
zOyC5c2;vzy5g}Pgw1Gjq3|TCHn@vbFu|&B3XQBeRe3s=%-p|khs$~ZGf=@>0H>=%~
z&LX})Y!K@w36d0P#0|;d;^&_QIiYT`MN&SBchz9t<G}TBkpyRDW&y>np{%l-1AG^p
zL}i9GMRek!9c~i?Hfgq35d%&T?vy<O^d*IWI!D00zo2@uV(@Um=(G5aaydJ}9UTG;
z`m`r!HQFe7)O@H~0}&S&muss1b+_vx)@>St!I{rqhD9s6kf$7RBWx~#?}O2Y%Lt9l
zL@jgZ_XShRKXiWYxoy{<qVpfRAJX&>07D`kP|)c=l4?JR3FkZXRBb5p!|{;NQ2PQ2
z)#CMw03}_RBIxJB#&_?ewjHPD)ExvaZvi}~KaIe@KlT>w9`<V=J|R-PX&FoM5k-Z7
z)e=n^Fnf@CyxNt)W-=C?ZuPjnu>jWSC5@W~<MUP6j#RnZzQ8R^uklkeBK=q&Nf8kd
zQ7tdh{8W}z7SFgDB#ApX-{VjIB%_R`+v0Qa2dImsdBt0a-4E+9TW|M~l71nV%i-lR
z%LOcHG!nlANd3$!kNV;%wcKd0pp2Hj7V5V@nvtYhYcBWt)*pj2<Gs+PI%+YCh={nb
z+ws*RgR0~0$-3ovt<`~Vec!d%rM1KL^lu8Q`-P#c0xdu`ld<4Y;JZzx-@B9KVQJ)1
zQ6i>UZScA~k$OZ}8))5EBP+M<d~wF({wCStdHZ#kTV>PnXQYx}?$6TOHX{$&IuU%H
z`*&c6g<MQrym&b-*=uIped1zmlh0J!;`og<1u&62vhaw*=eub$sX_GZ*WY}5fYX5Q
zDg;aibyhiKR8>`{fjRY$<17>kdCk>+RotaYzGXwB*CDUm4vVRLF)8V+!}C2R&E=es
zf${Z=C5zlcS9sf@zvpJJw*Z@7yMhJ}FU@0U(0-$SlUrhc*N$E}x4E6OM(O&=-!I$t
zIwT~7jH)_i_K(|nA;9i0^q9EfGX5pECyO&b&oVUcNfjH_oPCLXq1Eqfr`7s^xSCIU
zMEtd=YQ9LSf^Nm*fNsNd?Zm@EA!j3l5jO=N#}i*JuVh%TZydZUBs}|b;4?B03i!Q=
zodsO3n(*RJs9OO%VGoW@n^-s#)?A&?O=46B+09{6ia<i>fBlGpuXMl-tN}MhJ}!5Y
z6FWA*3m6!0G{3*SK3i~^<ox@%dj#T*H?wrUQC1Kd3bH-IZ}PO<wtrc(;kv#rt0I95
zef$K!lYbmUIho#k3C!SU%0lheTFX>^Gy;1qWc|ATbrg6&xbVM!6}mtY?fm@ji*bi0
z-uTy<fd6^Hksbd10W^LfH0M8G@$oUD5O~(VFDo#N5LEu(XX_F{0-^Y?b_H$_h5MuY
z`)vOQd`%&WgTB6gKaCKQZU9^67vM235WA8?pGlm^BCW)+u#@5c#~teW=?|dNgdE`S
zY@~qCb%Pc&2%8A1S%e$}#{Vi~gHS+HLZ5Z5H1q;-!}mXNvpv{Q9vfg15f{fX4;>hf
zWhp}LRU`VpPH=ZJn)nT$6W!k4J~%j-kez+rT>>dMT5vfJekzqVM(O{3IJQ1AGP0C6
zAS?{-?G+^@B_-tM#x^oCYVx=yAI}xYHI82qM*=@ij~-iJCjg9lB!GR=1R4!`a7f7R
zfdL5|G9ajfM@jypwXWtegGI;=w5;OT`l_m^#3UrQ29+!v!c3Avl6@;p58oj%P*H<7
zHVj|WLapqAjV%J>9Y5Oo`cGSj!m~5%t>v*nES^TzQ2UK)F6xUmBZy#xsZ;y=%<i%t
zo}P}n7gCpjgN6H%K3=GQzMF!?)x5bP0u@9;782MUT|>Ec2_#$`SlQ4V^`e&Z1KvR*
z>;Jfy(b-xgG5#cbp%ncT&=~`!(ZvgUA0J!)&lv}Dvb7TJutEMiv^URs&7q4Pt*<+P
zGxyi*_iEcm0*}rV{+~w&G-JaztD41HQ_k3!I`wjtoHh+&vm?4FAR~H$Hz<CWo&1mS
zM+Y#V`8R&3@}t2n|LeE%rHsQWGAr<`vdK>wAR%S6%`8Gl%#gO>Wyu*aBP)avAAth-
zKi_7vlrc0<D`jFvE@FzUDNjrxP6?e6)F6VBPnU5r9Q?nIK3U3Ot1HT|m)spdtfde`
z3LgD9aH<&EOG7jKe;rGEI+_zNGo#iWV;S%0=-7`fgj54+8^La7=l{`8?f-dP)#euS
zlKaPdP_n}cNWtYoIyL4WPjrL(KMpaoNG-eudXf##QCk@x0$2Vc;$v+_jx1Gx(D?s;
z9TL_4r_&%@@zKNs1NxBT-{va+>oOesi+evl`d8QU|AX%3pSKF;_s9Fh&W`>SPdbI#
z%!U|R*qqCG4-P2Af%Nn9bN8aSPa`EKj|2h__EF=6mI(jiPN}{$ty&oA%=WDR=>)(u
zb#3)UAz@)zl@DRQ&~<fn4W=;=M50m!CX&hIOQ-xSRm=kgD4anjOSL@{6BCPnJ;rnR
zqSo5H4jbzET4%4f0jf@a5TYyFd{bCyDP^7IrGL-1BN?w7u=_W+zbLBvV3w8rPTwtW
z6UdEeQsKN)G4+{@JH}r@(at31`r=*yaCKX3w9l>XXKG~tHlc$zdki?{SFlD;M}fD0
zorE?u9O|DwK8uVQ=F)nea4$?MJevhf3S;6|id&@;DYP&%Adlw)w!~AcF2yPNfl(&{
z7{3i)pKjCHZLt9}Gh4u78oa85A2*3Yp&u~(a(KS8-9`~szZlWNguuQFu{T5jE@g$c
z6@~+`f-yBKEE^6M<1Gf*&BnIQ3c8;gPNAv)(Sn2guSgk>PY!;F!9jj}dk)XJV9RgE
zj7eW7Cz8T;3v?LJYOpE09KKcjeY)0$qf+t~tyI9@c`#MLveSlalT@I~f?B$~`cq>F
zKf-aUsrAmQS69HS+%a~hM1cmRQBeZR9NaTQr~YdUKzGNsKBNB_9-&YOP~vC?sy0?~
zoaIxly(UO*bSby;x1;$MDVT_m)8h0RS{SaKw!7imD)=y4x@^B}S75k>xh_82NerAp
z`{pbVvooVJHJTv#&}d_{@3lNEx|5QbU)-0Cp=_a?Np5m6hII`5bv>ih;=6ZCZsH_8
zS&8X|#i*a4qaugA<Y6bGdNIFx4kTP*s@d{zMxU1fEDF1Rs?l5m^G_=wiMZH!k5+GQ
zrKVdMXlliI&28RlqcjJjO-a(e0Zi)?6as=czc;NQ5{Xz$>#P#Z>aBy>GLpc+z-Tr%
z3}#4qq?K@<0XRimTHH3IUjr55-rriQ=b1p7!4#`q#AZE85r)~RqXNxfx|at$4G?l+
zpEb?~@~5I4%xADj`?af@UIJexciZSK@3nbM<UHw5{`m7<-xCszdopNNb~jJ{FaQJJ
z{dU@uxc_`JKf_tKrO2IUOiZbPq|LwZh2NLIAA8`!O_$#Zi%3G);c$|SuFzh;DWWt}
zDLg7-rY-7C=nZ-tC)&H{D$k*>n^Loy5JrQlJk2)S83j2HS@Z4D=@u*+&5l%J6W8+6
z2KM3M>A<<pev?1zZ&tPbxa=lG@>fL|wm-9@lg*7gu`y#xfpmAZ49Hx!6lRiNxd=bE
zlSL6OH<evb>vMbH2sKGGFZXu><*lk2h0gFE8pudU{s2JA_+o=-FoUI}nw^gsa{LLC
z)7dWfZ?sBnC<#6BtfmQ=n!==337+fEhbO3f%}kAiKdub!9?#_VCPCW+aV8HJ3l49*
zBCFY9_xC`C@e<HRcP%Wuxiw*140J{Svm}nKaTtK+^ecVV;>-B?@M6$Vu$w<e<ScFA
zcf@(oOpCvSI%_ulJcrG$PuQv6)!;Z3wslln-eBHL{7$Xa$vLMi4ciM8zG60_<<;bL
zsH}3iHjxtTp1pZZL(pl62J`D)PBT)cb2Z{?(;k;=?e`l5Hf{M#VQV8jG>9f6b#zTl
z=fuJjp<kI!EiseR8ZiQ|DVa3b35M4V@Z})cgsQRoqTObkZ+FyM4NiSXd(CN2NLWxS
zv{3DBJi&YeeE%kKUe9G{@;FvpR^lkRD9=!Q&%eiLt6v<W1tyfhpzkjeIz)%>`+&@Q
zJ<8Jm{U+SsnRerg39*CKoJLcP4|!rb>M-`t*o}(f+o-q9UbR8>I+#e87bFk}n&Yu@
zAvqQxWs$u@UeJ+wf7Rr;?P8qC6H^O1?ovT2gmgyf%~@TYt@Xv^i=)vp1g)<<Q)n7a
zEc^`nBHZ<e0k^=D-y8v7|Gi*fnu}0^T3(l8i|tIq=5&`#|M&8qb&+Zp^JT0s&=D*<
zz2*?9IN5GDzV@EjLmr32PO{z=C)zdWII=BI@NqkyC^dts?)2w=!*LvEBpfrnNf=-(
zawC9S1;~3?fVFXbwfG?18pu+7l`jGKUL&$}8EPcDJuSb~5nGh@KA5NhPvq|)=j4l8
zJ-<qwMg$sl+KJ!p1(ty0-@am#gOSPDY6LLLzEE^R8w#l;a23my^0}C_K+L<nm@i%K
zpdYTM3L~zMBGjnXy|}tn{@dNvBh!|Rl<IE<fTFiZP#kTxnO1*&{mdt6n`0SdvBwv*
zvg`B<tsjS;l$#g(7e%?aZnCq<4X)$!U)%D`)^YR2;#$$Q@ZP^t)|bAw_to#`uk0dB
z+2YyGk>|U)^aJ~J`~v|+P`uW(>4ebTewdzh{jqpa7Z(mDG4WWSu*o7_PjDf1(0Lpl
zYn&=kU+lgK7SYl$?eC(+{r2%AKANWA2D55*+=e7q$SG{=!3xN!gu-|KM5kT%`{=eM
z^JAre4>J%scdCv-7up~=kj9gLpT359H^TU0KmLBP6HRfsI6E-8pUH2@TgY^!J{U!M
zJ6Q|GXZgH$(vv(;yiuj(w>f{S)6Zj7Hdl=f>CN(qOXhFv`vmP@lZK<;^3hu`?6OP#
zBTx)DG8rUS=7spyocFiQe}neEj-PQxSxw0Hgc5LIT7N82G=vnSkLPWtQvm#CzEXjB
zsd7<d&h;27`VrjCk#2`C9<Y_lHowB-fWT|LPg79jaSfHlUE*=fAI+<#T>?WM$p*uA
zs}UyqQ}4j+G(hII8!+|VdTN;p@5pBVN<mML0E<4lU{sD(q?jiRY~b+sPp`&A2YkHu
zXG~xSUg62+Cc-fX%Tt^!{|^9nL5RNoD3*P_45!wff_IYrhpM-qH!60lh=Jn=B43Go
z#@bXUw9f54hYuTl2=CbBeeqZ(#!u9*t>I`4oC)ry!-x)e@cjoJ|2{k1ym%9%YL3$0
zGe_7O1IOPv{eRQ7xXP**&k;-n(|t^LolJ$u7&zyVF>r$5emr#yT<g}Y@!oqQjiqEv
zZC-w!UbK*q5NzMR4Rhwq$<Ul{jT$w^haY~JE}@AhkAWLDY?w~q;R*r+1C3YqiM??i
zCw_kl(brFF!IoSM@n-f~FSA)-c;#5~)_eqc8ovQApXBd4dh{qpj(kriFv$p<u)SBW
zUU=%Mr~lQ=lmF@%INl&MiqOzdM5~*IY~8X2-5z`p6-t-Evrj#x4W8*e4;(mv(CgQ=
zVs_@n67DTb4^iF==7sCzesBTG=GiFEkv|9Ob*qQhSG<mwR=$L)4^>6>g4vCY(vujM
zh{N*_<D1^!;Qapc>8=ICDNEKaiC0#<f@}qCCqKyy{H`_Q*XXGUG7HnwGlQcu;>{LJ
zR<hvz1z7w2T8H9|*&A~g%Z;KnilY1j<x#y$byVwE4P}~?L7^&zki9^5hlyDjHoA1o
zQtVx@R|`kK06&zhR}v)~*e~`HukfLtyF_lQpEEB-s}<GP)byFWCGuL`&b;uqTB}q{
z1m_M$@!G|$e%4e<)h~s-CG)0yw;%|DtD)GWkT5T7pfkxvzI^#mrc4>_iDSck%9Ni_
zv}jS|la4ua<}{8SJ8CT58!+pl<@w5NaTfpF#Zb8UF!%-p8bZxNys0W8d0D;I<Kfh6
z{#7ON^fE-BU#Itzn3$lGmYg_oQZHu&dfy&YI^}MiHoy|Mc;GnO`M2XIVB5BBc=v<%
z(Wz4>9N4!X@4xpRwr$;tKK=UQgVCdLEBqF^cJ9m;8wL*@g5G`lAV-cI#$Ig09-pwV
zFeIwuwInDwSf6}*4GU{fG;p6h7E6Ma63)y8Wv@=0I-i#K`v<6f2J0RXo^N*&L?cqi
z9(FTKUEj<nyde5MQGeMZz@7>kC;w&y?t=q>BW~ZuZFQeryLuHDFI?2|%F)qLdjCPn
zfb-_di=xGf=?N5hP5GeZ_c`N^=XK=0Yz-bSdp4Ze58?4fM@1t_9UJcr^Sgt`!+qJ~
zU-!V-fa2u9ssq?MZL1#B6&r5*KFnDvCm#O%Vbp!7ZaNV{VYBw9wV40bd_;#vyTk)$
zqQQU$81&;Hc>6kct;{*Q^DKVo_XDmTyK1blkEI8W0&3UXU6}UNG@Z`h78D*usue-q
zUUgBSc?A@%To~E&E3sx=Yn~r^0x#^nfP*UzV#kafxP8@j?=G)r+55|4=(M58T{O4N
z``Db18hnI{doLPGX<=%9dNSL*+(zx(SZM`*ulcp*cs$<w_PBEF3P#r*ZPP<Y5j)_!
z0b0D;!}7Z`gLvQsAuYFV-A3q*P?RcJ(pZyqJk<l&wryLC9{rKA<e1HyH){_Loe+*-
zM#|yChpTq3YRj78$sRan*BmtHabxMu5g&0K7ngm6u*1vMiAhlRnEIo~yYa6f-ndTv
z)$cL9(hzppN;XHH`$ysAg<E>QzKo7d<aMBcA2eu?e*JIatOw3z0?u?(@JoLEc`8P}
z`>t**Kcd9i$PYfyGog=s=N+tF^ABEn`DHxw{Qt0X#|}K)vzHb}10Q`9FTV0J{Cxco
zuY?90$F@!)8~a~;_8D$iF|Ic4+Mz?oju}$^<kZPi`1;E)m5@uHJfZ7D-O#Q>dt=Et
z_)wcTVFC^xI%KR#rAEzK=-I26j?vw>XOFSgJ+yAq2CY>2^pml;5|XvCK(R4wcqbv7
z5>thX6hY<6RZy`~MU+q#1O=8ocyC?3a#i<MZQ8gA+m(=~(6@FCO|XK<n=c=#*QkMp
z_uYrO_3EN**>cEVAivFfgWofD%oz1uSBy1jDN?j39_iZ`B}$aAxes$_56hP=!-{3h
zWR!?RZ7b>x^$+KR`RvOtk~g%0J1e}%@JJk9dsv6OaSDKfB@1fL3DfyIOH^1C_AK3l
z=`T!2bV#(ZHa*#kW=Ef|`k-FVdg;zvOk@oHc>ND-nYhKJ0*=D%wRNwdbi<4_jCnp5
zeY6P6K3V2;*rTO@<9u$v4E+U%mmf}fwpoKBrROI-QLubLheDGRdSBds5kC+986k&4
zjJ4^Zm}xt_ExLW!%~)ffQFTVy6>wcgb+tP`UuFq88iG-^M%ff_H6E;ifs+R!M*+v3
za#=+LoFJrS`}Q5!vv(gJAM|L7O=Ok-ihyGX<>#M&jzx<W+I{{LHn5+6{&_ua%KSWw
zfXf^aXSd<}->)O?c8EHk6le6frHZ%YzlMP0=~g1nTNNL_00dNh5^qh~kDWVrChsf*
z)KKlrFO>$!nKPHM=HKIv3OE|j{(bvZgVtX)=qn*QIvS7m>yN!koHS_I5ECbTkNCKF
zy!-ZBShi#-KK<eg^yt|WUw`!_#((uSs#L9tcSnvy{e}&&as39IIDQ;$+qXlp;>C?`
zNXw;*7q##^e=dEZgQwJu_pB0!js%4z6qEfPe#Gj-;Pp4(#8c1Op6|mN^ym#yN}ht%
zl6D<BV8lCb<CBj*#^QwwjkWIKndhF>=cIeruIjv=FxI*T3UprYuH7C|BKIK_DqPs=
zO-8}JWBYb2UA#mIp=G#v^QQ4m#wc8*C|b5|r388hE$FO?bcW(~YTM3c>@$twu_1%;
z{0lE2S1!kf%7@;BapN&@eERv#_*+k&vp-1IofRG@k^a8d_c}=jeS-<=KaQg+T&*xF
zwXB31?P{QCm7+S7jpK`~T^kb-HzF`;&?KB#e!}LY;C^}i@ce@3QNB_6bo*jf#<%Of
zjnR8X8*9>&Q2|G4b*qKJzYWgN_$UgC@twxw`l;*28pl`)xNXz7;g`X`XfbFFdfqCw
zs)(oOK8=82w&b1X`kCwaw%fOQV)HaET{8l*2jKl9@9W9U9U%hF!vq}bTlL4DJqqbz
z8%55U^K9L^4SV-dz&)nxJy-Bw5pcZMX3m_6Z@!tJ+W_|9JxQ@Mbm-7D5lJH8GDlKk
z0z$X`fROd7&E=H{CM!u!XhI=J;bszV$%2mS^|E?2B>CmQ^S}7u+%;ooPC_p##ao>^
zb&WOuF3t+LWDCp!Y%p(E4~nrLe~eGZe4;&06i+X_{E}{9U%q?^dz6q13JyXbV_Vfh
zaALd!b&-}YU8=orO`6`1+I8#b&rVii@3XOEF+nw$W1<}!rtt5<Yky$>ete?@&X1EP
zTlJ=$QzConPd{VS$oDa4)~rm6H*)#XMf~yGZy5Q`yO{FBkGOU%gI20wC&l#Nr(yP=
zGxh6x1!dmwRsSVLCE?QcOIY*i8jR~O4r5!6#UC&Ifz8u5BlNs&Z)dhV*)Z&<VQBM4
z8wBLBE=b}+<1qfA@z}d`udyZ-dY?GH&efdReg-EtoKR0@V}%oJnz{+0XB>yV*n;7M
z>wjEtQ@oK<4NBqhpC8Y#cq8<Zc6qN0au&3m#gh|*&wG2Gu_SmTY$(OXAo21kB(9u_
z*dJa*^qB65e!mT3N4G`H*S!%p;|(Nkn}MX8O3cS6M>u4O*c-sbc}P0-9g@NisNK2t
z8JCzChmc$QaP0aL+=xD_exmE9zThv9Q(_<@UbQK%hhY4n-FRl>YIIpL9}g^;hh~4x
zNBjAU@YL#!_-5Y`9K0Te$hdgKCuZ@SDOi_n+_(v+PM^l&#Y-^u)6Xz|!Z$c{_=x(=
zsr%=4t5&bZ#EIYGyYDAq+xG3cvzFYtbqmW^ti-;3`!Rj`481MGxc-{I0AoHGi!Z+X
z8f(|BPZ1payLs~#eEHSa_~_#?n4#*VkdV~K5hh|da^xsZoH&6QGycTSQ-9I>HZ{Pn
zUB8Z>ewvC;#(s*?AC1AJ$v@!y1$7?@S%&S@(xpo~<p|*Q;B_|L2SV21bOL!BK7s7T
zYbxPJ;lh=M_f5Sv7jG1E?3>UYIiE!MB;CYo9dE+l*ZF4xuXX$O?NO~-bz_a^kYxeK
zhtRHFyRd)XK5W>q0pCyj79p1}<Nl^i(YkG0gkBHD-d(%3n4^gEQ;L$~n=YKcfMZ9G
zp?HZB=+?cvjy3*6HLQ1T-=RHt93Llm27Gw^Ib(+QvavVL1U;nRPMoL&+)p@r=1iLR
zvcjp8Coy5%IE?@LYn?eNqeMnT;QQ~s)84>)8LtE{+&Fdvn}67h>Ca5Zgzgit=(9z*
z9cq0H7Q-+*zS<G}zU>F^?A9L*vDacT^Oc!6x+(pj?OX-35PD#vV?FFb7<SFuh1h84
zymr@4Uc<Uc>+~45bm)p8`K3MR89TYb_5f&l$C^LY961YRZW;x8@2a$|g0f9*OkQem
zboEg^ak$`lB;CA*`1OAw`pc&g_0B_xnD!xJw=G2cnf-{rb_VfRP9y%5y05p*LiDV+
z5Hr3D;^w@H#3L(}kvX3MFAWJXNZS8C61Km9xP2cZZp&jxyrTB!A}H2&pPht7`=7^(
zBV#aa-%uR7zT9{>YlO!}W5eas_;l|E^jtX;kN&d&<M-{rva_dfF!VZ3-@J)aH^Z>|
z>UGRIdIE2+-;PGp7o+9O75He&L2SNs9SK<wa%_AbJANE<=l+FUxpQIg<AcylwPF7J
z^G|G7^%@)L9J~Gfq#v}Wsb|mbc(6-n?B9O?>o;uFbsGiTiWMucYxizcs8~Vwobz>z
zYR?QAJQ)4^_0i+e=(*zlmMvG?s<u(vHf`|ezyZjUCpZ3_ITKfm@c&Dy9ko=|QQLRy
zM2(s?P_}Ft{QBGPIIKh^2@D88)28=h=+Gf}V(4H51qNZN5_vohA?r}QF`0)mB<B~%
z{QW1S#Y@>pp%!nzKWCw2v8L+nWYK1L-3;+&5_1%9UaBymNtr_N=v+4nK5D%)a5xRe
zFVfrPX=GWzQJA%E(;8is2+I~6jFsvEHcmCH$)oBa7^v2<Cy5W0>^X8EcdlIeGn+PU
z)DN;&>a!Gaix&KiC5skmuSmZE{j~=#ATZ;-+rqy?cvv{*&YqK!w1r`lKm9lb3+B$l
zP4(Iud^na@6<weH{qXvTH}TQfPw=rSBi?xj10Q=#Ra!M|K0u>XWjcH2Ed22OB;3wK
zg>^*<zt2DW9DDbq_a@zQsOMNrSPU-ixQO|$&&MaNKEaW7M|7q}OXyL$@8SFK+UnQf
zpU?UO?b@MhSo-NwgkN>t1g2#gmC;!oEn!1v^Us^LFmnV&+q}2tAu`l@=E?ef>+5Wh
zj$rttJ?|T{N|tL<PE{OEs6iV9WD7vcXOgE2u*K~wx3O>0K4VGn2-N+Wcz7qGzkLhQ
zKa51&-gQWfVFD!MeM^k>iBY!@zi$y@=f95lm1B?;c0T!y43HFm3rUw(&=9z!^js(s
zZtOOevra-%431oyqr^(`bA#i>_g`J2UuT8wSI=O~ZY9KauEg9^`*0~D+<FI$8R|4^
z(X{vK4UFEjAA=Td!Ru@G;o>d(;coU|+oNigDrnNUG4kinkJ`0sp;gP4N@y+DA%eSi
z@6mNx`*!Vgwz^`)ilI}-4to5&Da_8-*AFcocmVb4*3~`x-m0a^PHuYR3KS@SQl(1j
zFgvz~_V3@1)~#EiTD58@Uc9*eE+*|-xpI}>7w?U5wcSHqAJoT1LCMb_G{)6aV3a6P
zTz5!DMn<7rxw5!@J3@a($T}uZr!#mn#?P7FLBVPK3sgex>OJ7&$E=hl5vSfT#=Ip9
zIsQ!+Zd%YO5$EfhfX;OyQL#w+d={3leaR^%Dp#&-tnn-|E#P=_a}q;N6HrP0_!nP#
z5l=kzq)u@0*W9`I>dP<nN$LIY!%Eru>4(f?g9hRIAAit^B<PK6*r*Zu_UorJQ%?Q)
zXWa|-<TFp>ndhEE`SRs;PuHB;vr~Gc?j?Mb!S^3HKna$ycFSXfAJ@Xy8lAdy!RSxM
zXyIe4v}ZyW8yPfnDlz@s3(sTPpEGQh-)Bt6lwYRei?6@J&?ko>pQ_kwv3}h;M61e*
z72#$j&}Yw@Wph3i{-)<%cmd<S`35h)`YIk(_eQVYz0p&Z0gn#U0-gy%-hS_0lvVeF
zHJHhA;erKNxX|%HV{05gb_}1ZbA0Jy+DRZiiA3YS<WV(Fpf<a@_bSG98izmM{1cas
zT(WxZO4likL6Zj|XL0LZrv3BxW7W4tL&F8on^n1;dWc%71DRfb@tDO9=R-TO@dS>p
zv7XL3U)g+U@JIuzqSYL}e{lIhV<|mslohX89QjJ7pBpcuaEgGS+_tmZa{fM!a}+!T
zWk(4Iw<BueJBZt{28q$h*-I((eFB5vQ?vqn%G6eWE5kcmPQ5-Y%8DazB7XNw#IO1U
zNw+f=UEr0J0Ix*zL`2H26WNGyjez6Wl*FX89UJjUnV<eAUJ0=2$2Q~R?TfJS;z@+X
zMDi4-KwHZm5P%wa3!qKOQfObQ6z(fh5P7o&>&{;`=m--_9SgmMaa#|g&z#NJcO@+O
zjSRx?D_ppcv6KvxbTJDt1>D)Q=X9Irv(LZKA&)GRfA~>}v)GjAlEA<K<j$QOiNO8U
zsar>jvPqLC<IAtc;o!kTdRvD7d#M&Eg`(+qurXO&iNfP%W7YHL%db1U3HQe=-6pZf
z@ohYYRjb!v$IhKNeB_8eFPXg_LRvW7gV)^=`aaE`&1K^Zg=D;Z0+6#xcLWuxqHQEO
z<D|*!#*!-D^lOSZdf|MNP`p4KI@gOtkiUIfV5dsYp1pKJI!`3i0xniH)R~0isbRyg
zX7y@4i~ZoCLow>3(a4oE7pDF=MK`XQMC0A}-`Apz*$QjbuB{(P?K^b9*AvF;X6LsP
zzR`yA;>$1VeNZGU{`+tA?%q>>$IKajCV%NY1daYaC188>>S?z$Y1&j*3f5rO9A>k$
zSE^O7o@QXJIex0b;Eel?8aK9C8aHWz2U@g1m#$s)^_e|y9`fe3-gu>0V{aovV8@Re
zhcFxC-RK_Yee#47gjK3k(MH5Uq&!X*j*H~nEEOtNL?0#a-+uQURI6b<KA9nu981Zu
zb{Qo}RivDskjKi-0#AdYr_+c|=>AD}w0fl#s`spp{8jQJQ9aZg3A%)c1pM>mKlo|L
zPdK>xV7eY}HZE&*uBG>#=#{<!=#}V&<)1Fc_W9e5HCclbjkkWjwNAlrjh%n)#E~`0
zp+nY)iHOmjvxw{IuOr9jHF>;Aa)J^EZ5yWe4?S&r{G6AMm!31t$3N?pHA68gwybC@
zrH9vq^Vr45%Ge4|ASvWLVtyQhgfqwV^WCRl2?Vt6i{Q8ZK+yOd2zqZ3f`-pT(1^bg
z^x+l+y*LX2%^!tVZqBD?c0VP=BjMOGB<%V{?bNkn*j$g59v3HwPKd*_qwDa;kqx*N
zOF=@z^n#zN8%ku$g?<&PV_K^&*xs!l{_4;LKR(bBQ(8WN`JLKhf8Xv{)}tMss#O`K
zbLD`aF$D!fC^wwFisu&X!r^O~@Ioo!V>w3q^r_SEQS~InRgRoFbjRcH*IvblH{a0Z
z?YBl?*sv$erawxXY#@8KY-r!Uo!+i_v!+<OY`Lzha^=bmPJ?v&cIt6b*j*33jzVT4
z(tKYjs8dA|w{YQN)UH)ahhO&W(H)OG+*>D-6S59Fp^qIq=2SQ;uMsmBihy%HzB!5^
zca47V^$Sqvndh2C8G!NEw5-D@Q#^6>!X=??tw=O1Z#%O#ZTjh_pV1vGo=T<#+-22p
zo;P={&KmjblTYEr|2?lBcBiy3qt}KWG<xRf=FgiquRdAC^gv=Go$J|ne|*Rg)kuFA
z0|z~(GbKL$=wo>C`4@2L-~pWuew%7!n+E>gOYod9?$%zR$kKPK6&Ma??K9@Z-b01g
z!kNNeJqjyk#VJ*~6xw&{XzU>^{5>q&wro?^F8%ZgCLVqCQ9RVGn-*>M;CHiUq(#e?
z=-a<P@~eBr5>|S<ckaTb4I7O$8G~YT$-+gL_S^3%y{Mi7vrFFB_daxZwF7#O>5ZrV
zcp4*jjl?IXKSAfwosp|-F8e}G{oJz~&*J;u-{a8Q^w*0S3>)=ngkE3wLJ}XE7ElDv
zdTEx<V3}2zIE3P@_m{o3c(jCBE*Fekpi}W%<I=%P*gJo(-j5}mr>*uwwe|hvi13hb
zn^`ysZ6SXd+g|95z~iaXri!s-1$D|PD4EbixVuSG^;68xUn2hKK6nRbgMX`@N}zoW
z|3M?+U7?;5{uZ5RYT2t~E%<hQ2Z4`$3*VX@;pJzRRac3)#NFzdbbhO`;@%-KDG{?z
ztjD|)n-Fh=W3rCRACwJ!E7ZpLCLQrkt;VRAuONJ_*sQ-2Z1oG~!>7&aV)ld0F{DO$
z<Oxg_E*nl?#cPZAAS~K(2$eZ>T<zSoOD77sdiARImaSN^QjarZr*qvpwRIiAL?rZ}
zvBUDxrAumC>*3w>gk8IK9TzWN)b&i!qD6G(<G8qZJ>IQY@nU-X+(osI@bFvudrzM}
zgVwED8GEn|_wB8olM#_odTd@;Sh!B8!q?0nkX)B}=|pf*QMMbgf^|3qWbfWRPI=(i
z&f@jFmjv9ABS$cQ{`@QzCf{)dL1jB2XQi(Ceoq!|EQWZa=S}@h4yoj8{$)L#1V8^I
zJlWwUisrTc9BAIWxelN7WHK$_H~}-gH#u|W)Ms?r(xvFrtGCWJ$VT^Hix#3zUB@Xo
z*@*Ab^+7$y*GHp1#Nfvs!`984^*$J9-c>d9`#(C+*jl(7$g3V!*0`^6BYiz5eg9o@
zZD<8wB?LJKpfinW6Wo^56mUukHlQtGSj)i!2OK88;RDv%=WpBy#h=rsW7W!4y64T4
z;JIPp++h^-zW%<*Q#=p4zS$KY?)wl!XAec?E|n2nK#64=r#$vXEXH>nj}5<W&=Y){
z!+p1Tx((_LU?VjB4#LibVZoRMF7p!A>|E34yl<R1djF!-ENtd%nYIOC7t@DM@w`;;
zSRI8brVWj<1wCH&LXapfa}=~5(wrH1t?WT@ECTNCCt>p{#I9coub^NAbRUX<$KHT<
zxf*Oq7+ctY_FJ?Xd^-$>Po<W;!HhT5BQ#ny#Mh5y?}f4E-eA$`Em)w$TY_1KSpg+_
zpRdsjPgbsvGC8c*FV?7<FDFJdt$|k?R)Mc^u4%rDk6gmr%MR-BN@wA-u1b~4ICJ(a
z=Fa;I|Eyh$`;~ZW-n^N9uW;eQcw*=foI86Czfb!EKTi1xe=l6D>mNdqmp@;=G!6N%
zu$x%Bc0H!g_!G;PE!T-iiWMuW>!D5^JL<dvvuDr2Pd`u9W9(YCXn`_ilAnL{_OXN8
zoC5jtBTwEux&ypJ`}R6voIZ}f7wX6-dJ`xDIUJ=}@uIkK^M+2e;%S|e|28XEuE35R
zJB%g!Bs(dx9#Vfdu~yytpMU;YmWs3I&k<hUzQ|SoWd!7}$j7Bxp&y&7m;_s*Dn@9f
zTBdl@%RY%HmOl<pbqGtBm9kKw!syqpzs=}0Pr*U$Vw-yP>to*E3-J5&Kk(8kFQY}P
zmMEwM7sDpUeB}Im8-xv}g9i@kEQTD++q=&r`0UFsv3$*Hd^LU?+O})wu`#Om8tj>(
zaa%&+)_>qYZ4k$fS|5i*8Z>B-DVH@vRG8GnR$5>7gkHODKhZ^%DpgUYY+0*4WgHLp
z(+@xBY^wK4Wyd+;8gy@fCx3eq{lDvv3hgRb4TmI2F-e&F`dn<EvpwB7F^ZthuXjd~
z+C_{tsU*cGVeh=XxNy)l>8lx5*ko`M_-9KW3Pf?WW7ZD+*PKg-E@A6$TdfK&imfJt
znrLsLGuTUBVj?3Gr8~kj@0=|XViL6XQMh|aV(9pvQ;?L90KfY?!ME$<>VcRsk?)nO
zD12K!5APglpN)wj`;c^Qld<IaoVj%wOHOY>yw2K}48_dwI&IOqSa}5aWZbrL2l(NQ
zMwRhst#T=SRD2T5*nI}Ot#s<xgJaUVbnb*<PYlJ=Pd$ki4>U)Bf3l5I#FZ{x3XeTD
z5HCFcKV6vpv2x`~`h7+eJ^l2PDUl`IzFfI-=+oz63>`8Uk3T+0?~me(6ey4%4?WZs
z|9kFPwXf&&erwdIuFnUlSGNv&bbm;{Hqo(T2XyY#QSZA}t(tiC)tB-7|DHqt{(VuW
zPHnvW(u-=F_S!2{tY}dky4k3a^^Q*=J?y}pGiMII{PL@mv4)N)s?IZ=8TS%Qy2Ia}
zmEy{26}$uUAXmMY;Nu^ZY~xzIsXzL)QIyr|{x2=+l7$>|%QUPMg;upvql9P+ZQ8U!
zrAo<m>&ax=syWddd!C9a!NrV$6TkmnCn#Yy!9IQa8e2IBA4U!CYlx3O9gC^I|Ar4f
z{1DAsJfIqq@wj~XGR~YneNQGb`M05gUA}Z#$9mf=oQvl0VeH$x7wgxp$5)?!fdl*Y
z8+%A5I5-;~eCQ$6SK^GJBDS(l5@syTEal3V(_x&};8-|o^U#@7wR%+y9y$bNm5Ad#
zXUXPGoACRuzv++{dtMk%BPeGO>UOJ(r>8%St{-)US77?zPHtbmjoGiy#>E5aCxtFr
zxhR@G(G<Rc=^I#~Cql7(wrj@1(Zf}vQw=@OS~}Dvp$G2V?$q&)yiT)Tn`JZeWb2n(
zqd@uep|`f+csWP7QcyV(jNX3URz07>-9tjeEhO9uhi}o6@b5W9*_Zv$a9em4C=Z_+
zU05L+Yk12iA@MX*^?UyFC?+8mE6;94XjH1G@==YcAyu29L4lISn#}WU``V~eByY;;
zyAd6WNt;h1)(LxMqmI)DFcrUWFJk=}N}&=K7KT%&PGR-xRTwd11jdg21g1=@j$k{2
z$@(lkrGo46FHi^B%5+loq0t$ugj$M_OZjUm$;%{LU=ki|5P=f;lB0{tlqsX<eY+Q&
zLYWqDOfK@}XP;r>`0@C0(qv4Z_6PRt+N}rY)>94njKWY%CY4ybW{sW?Zp!2z@Xh!M
z_(FZh$D>DM!C!yfQ;q7o0VeL4J!>Z3fA?LRW#qf>;oZ01#v88<$Fom9iC=#HImJW8
zhZn<KyZ7vlC!c;=Cmx}wkF(SBQnHklhPjh)|Lj?`MtImwrxKC0Y~5OC_GB1}CH!4~
zFIa$CGiG>1QWJ(PUOjmgAxA@S{p@u_ghtpD7ra)%xq{K@)lPWyhewgSr1dbR+ZS$Q
zV*iN<3rX8^O<~pSv1TaTps=yV4E2@EK3j&!FzX^JqvR@*3w65H(Vjm`Vk2U)dcx`y
zFX7>phjDb3b&qeM%7yfNZPvuPEjX^w-u8S3-M*H|dAh7{^P>HU?Spa!IrP#AcLC~_
z@$b?f-o@?D?PnJ+Z}`;dqZ%k?oh0ghOS*B2H@>mLbGdl?D)wACoKla|%IXy=i>{?>
z>E*1#5Xu)DR?uVQ2*W3LUATdh>bd9$)+_XmF~m{0_c(d-B)<FZJG1gF#*G_?Pd^=t
z(W-s<u4?Oi@WBUIvu1UMB5)Ws%Q?g{QpX6H!#f~1vQ_E}zZ}KY`8HPSzlJX0Ys33y
zc{Qq*j77(KxAo^bbm(}`cuX@b;5cX7pVMdH2PNJ<|MXLgeCHj!GhzfrkNQyeXk-)`
z&B_%k@xgl|@!s2S<Fm12)xIZTruvTMOP6U+(7h7ccDKOZrz3}tV8gogHp|BK>vg6^
zjxpne-6j|+S-DCTJ=fne|N9?Gmnp3uVw|+p(s;}X%t8+*D}GxNK3+BGof*Hzo>op(
zz;I7fVjXihzufP?{ig2k#dm#&LFm~~{Pg%w__F<%_@?JK_<7jRm^X4BcFfyhbB;EO
zqWd1X4-b!j7&%K?pCaJK!5jGX`Ckzok#_LAUw|J5eK*Le2hS_U3o}N{FqX0o!yY-`
znl<+Sy&uQc9z)En7;O4wlhyHeet~{y`)XS~uU$sSSvaTNaLc5GBwRm}Jm$p}9AAIj
zcx453ni&Ebn8Pf&g>YBmldlkb+xJx0%-++_F<yB}!aIN3X;YGJUqR9>*Munlo16$c
zi_qxQwt=sY4_X$hlBuvsThuO`2W9i7hOh3wd<%OohCB3MvhHFCq@%=ICYaoUNlvmF
zCdMp>GiT0nX}Fn?o|7j}VCvML&B|0voAx{A&6}r1+mR!Ob*F7c>-;WVx_ImyVxEX^
zp6bX}v7feAK5F^2HGkylWz!2Ye0`G8vuOl6G%JCgJ$v3W31>1b;A+&WiTm$wicVcR
zqtAf;+5_|ai!bVF15#7!Iu9Gx%r^Mu+izjSJ8$EEFTSAX2J6<nhaO*7wp=;Ajc|9s
z2OZ;_8BV!p_ii0JNtjO8UT+jLkY$$Z*F#e#3b%wkVgCNkHP*Q;GoN->hMdOw4X$0i
zroF%0x4R}R&|NU&V&wHm+&ptrPtv__&OR&~vkbpK@jJ$M9glye{F81x8=;7+)42|Y
zO&^BfBGwx$hZi2k`d`)?OQ{sATnx>hZEh^3W!sP2j8|DFAV&cDecR8fnBwpL>z%)F
zeB*H(`{!6n2b?)&8ka%UwpEQK=b>QAS;V@qCG14vm^*9PE;9-f#vEE^J6kL5h#o--
zXW{OJPY~aR&EOSmeSB_)@b-aM=|*W9AW5;efJo0DLa^e@I#wLU5>O#`QB+dJm6XmC
zjHV^>rJTN)gan+qaa-A~Ydho4pFgh?on)r7+MJu}^Ltet%Fa~5eXza44qo$;6^8(y
z&6O)x!Amc_EbUScf^8pr?Nd)9(|6uSW5(dUQSamR5hL)@%P-^UXP?2KA%oGXbt|1>
zH>2<~=g*x(!GeYGP`B=Q;>jnKAbbI@zA+qcz5A|CJAW^ocu$8thMYEH@uEfe^0Uuz
zV1IIUK!!QmnmC+X&&0i9;{3U@S~OaN->WmCItk}3{ND@D<Nl^ijWuaGed;7ey*CnP
z&zv>ZcnGRaqNqE!<s7CxF%4tejm5>o7t;wjhD4UXuRQvE)d&7L(}(EAN5*6OAKP*D
z)Kz0mGPJ?QeHz0zXZl|Aq&U|$lFBzJkBY4-rfJNYxVZNsHvY5`VVBZ3pcrykyIXDK
zF6w+XI}^;}S+Rxvx!Er4yMXY^);*jVfkM#Uet0bTO69Zq`-Qs*)qwM<+r(JNI$n7#
zvalw^0I{wM+x<6)iI2gRNb@nuUMQM94+;c1|IBuVU_W0J${wgLNwU$fPTF%J4Dk*G
z9P6{2H^Wk*IQW;t0n9yPTuw+x)ca)p(W84e{nx}bWBbt1(3B2M!gZG~hv@8(e9sx-
zKtadx|NIOShw(d2@HOYFJAUE>PMkccUz^_NUq;HDIB`;^aSv6WmB=O6!a9)S=wDXt
zUTaZ2g8PWSu^$mfr|J8gw><B9<JQ;cWz!2Y;uE|udwn)6T60DxsWI8Pr;}*`#|ff2
z(Q?U>CG}0bY11aWKXN1nKQ;&-e>mFM%6XV5W72ouVZ`gNW6*#B82!O0tY5np-iEOh
zDr{x!jSHbs+7gIy@viM1)8A!OF#jgJZi5C7#?;?`$1l_Dmnl=H;$3xp3s_{3q#>O+
zeq3KaRyw|_0=3-6oONs08Lu)1D<M`&oae?GMT!<dzWf<W<y*W&3B3H;t0-U5dOS2U
zQhxN|hq&#;WS(~;UP)d!z2-E=bs2|E(>A4>B7nl6R;OBMJfyMJ<Q-?WoWZ^&`;4Vj
z3YIU3nq6udOYWuhi>;9@Px{0)oDXj2terZf(Gt%4R<~E(Oxsw4z76aqGr4r&5>9VB
zt?&KJ!sI3Ee@O4)G$%N3FsgN|W-JMJD_#LgFch;s^+l!#%%%P@yLS^4kfa2VCvzg~
zOqxa&lb7TR%%SUdSHitz5AaKwQh@AM0?tt)6VB<jZ2596(g>4cO`A3ymo8n_%M@fw
zmoC?T+4$SDXD{Z>`zxhGk?W@aF$0@7Z^4WiGxg^QLnN8vf6YJt;ONn#SiEGZo-V*1
zF)^|D>#zCvN7a>_k89pv3$Sa~?i3+LVK;xl-#B#WFb=D-b?bI@{K@0|c>I5<?KyS8
zkt4^jMIBGLht^+rE6kMszyCdt>Z%QVuVQKs+bbqaug(9Ozwas%V{Rhi$RCKibv{+J
z>A%Kz@wIC6Bqgenye5T%XZ^8wM|S<W#fulYY>v8rn@kHhKDd@FUW`{>d=cF`cfqSK
zzJyuRXXp%mb?etNwsIb3FnsR$=TWp+G5w&MGiw&!dUFK2s(ruo{0lgHMpXcAg!7Zx
z@*JH%ciwpAJi2jbc^4UE%NC66HnJ(+N$C17U!gpjHfyRqQTECMEnDdM_@@0iBV8g9
zRxDdKZ%!E_*Q{AH{nr{x)jeZAmd;rirn!IbUaJ#pGM>3aiIUn2ml61osZpZ_UVQ0g
zJ^sNGUPF!noI88AvBvXIk5b$`c~egjuzK=pV{Iw{*#gk^g|^66EdBjRkJ^gyE7A!S
zX0>FNNS{FW$B~z5PzIITR<>Rt*s!lQFSS9=LYeQ`FWaariq^Cp+C>lN`XARLE-LdK
zJ14iC#DOITj3w(7u38uc%1CyzyB)9KY`VA#FK<6{`KIo3Hrzeii?ZqBHf64<2J7eT
zgR;5Y(<z%x3%JxXc==W=VyuLco5Qi$@p17ff{o$&=P#Vc86yINA(L$L1O)}^j!$N?
z3kwfR@zC)ZdH%vVY}vX^PaD9FZLZ_w+VtAdTi3R28$Hk4>NWq^j3gm5X3Rjm5`_Fr
zyLN5S^}){Av2&-M6r51Fak_&}9Xp^?r;cdTrWJDJNDkjUaq<+hWzUArojdC31lqT6
zhupc?At1N{j(3|dVFDUAZhTLB-<YLw%$PB%d}6bF^2u0TOs~!Vve;_}W`@|SyAW||
zk)Ezaqr{pPY;0<${56%-<%no6EZCL}=dLFYMPNdkrAwDOHA(y3PNoGMCy@N#)6XcO
zHCsR2*og1evj^rbn2*6jhT!KPr)bZNBO)Ur@ykzB@#W`V=*hf)oBjtzkNH^7DaVH#
zL+(PaU)Rg-g!7A)EoVK0*zR4s)QzC-gbcu5vfB~q!(P}B&7M66a%3Rf&z<miDCC?e
z6mI3qm$!PJHm=`*u$#%@Y)?J?l+6^ayr%TDX5@Uv#Kb7!w**Iz95L3UrF{7cdXjHj
z87l3;WGpRPx5CgThan&^{V}tGW-qFz6A==DIj_vY);U|%Q_WaukiTSpJUaPN^(;((
zO+t@^V(-$u#!@o8XX-py2f2%9JVY_01m_9X;gFmk&KW2ywfD`M;Byj&E;b$9)TRfI
z_wIqk2e5F|!c2{YW46#0U#>vtY1`9YaO_{NPkY@Jo_fIyA0K$vRrh0`J_zisN{>G9
z@6;V$nHcwN3nnu0t<x92wN<Iv4?fj;!z;HF*%5t$;8m(Gyvq!LZ<+q^E7cF)dFrY+
zGF{E5!CMK-G6h?roGKLxwm_u<El@glopiPqml%t);m5J($`0%b*@+z?yKo{rZ3iqr
zaq;#Iti5;y|6DkV)fbLor7FuWoWRoaC$U(SMdwao(b-d2c=j|FoH>p8r_bQ8Q)e*m
z<QdF8aTc?WpT(?WXEF1rDu>Tu<{?!Mp2yzHH<4ggzr-@^(Rgi(K=thB1lyd%oA!P2
z(j_#gUmyGS@7HackdP}VQ#LtK7vbOZmg$8Dnm5y7m3*HODf{;wK;C?LaplTYT)1#i
zd+PlBd~x<{>QpG^FpP7@&Rr<4>R5g!Kf?}BiiOb7WC2&Wa3Ne&>-X*3kINw;X|g_Z
zqHlf|y?mEc8$`y|WgWH=*zS4isi*P%_mfbhNYO0SFP;FS-deV7rAo_8ml7pP7<<b)
ziP1L@w&P1A#6~6CvPq!n*KZwMhyVc7#7RU!R7O$ur~A72UZR(>+vMe~M}n|wPihAn
z?Roj~W!h2X>16uJUbsjRwC&IyIdkPgvlb8NIp9Wp^pT#V@}0Nd#;Et-$M@fUs~;e2
z0J9-%4%P3~D_8U&;6sNG;kT(%b;^3q$I+u#FDzTN60g1S1}avng!>vd)`=&afe&9^
zpp2Hh>Y>~4zWa>5q~+YXvsmV&V(0fW<c`@nE$2%kd>CZUp3S2l04-XzGG3*^`2U+X
zLiO*@U!VY5wP|hhH%^@JjTZh4TjBP$;B{eq|L?#4idldDsfC;+JfC&y)<JDmG7B@0
zb?eas-MV*&|Ghk6yfVRAFU`X7t?4^fs<x<#(k-lnfvL+g{k7@pL74VB<tv#FK?U#3
zoJn=M*Fl*kjvKJd*4XW%ZdRus$S76YRYl1LcIKv1r(^kN%Q64m`M7aD?HCnXQ0!gU
ze*x2;n}&T0(uby+pxxBzRR_gu7B|)icMYl`?c1>ff<Aa3f$yp^;sXRc^{NtB8T0<z
z!>dqL__QAdpSB;ur?o20-cjqEc)t1N1Zs?eSIv*%UE^c;)))<+A|2HZT_fOpyaUjr
z{7AH@@IKn8za7fIhbBb_8}Fua@Y)Xiyk`P_**h6O@0)@td#B?1?X;O&qvB)n%c0d6
zyK_E1+OZHHY+sCbw=Khnt;;Zc+j6|TbroLRyc#cTT7&;>_y_-6w-(R*vlhcvufr27
z*JJSV^%%5t0|qYMgnoZ-LZ1by%u{8~CQROb9MN$ZALBv+$KH9?^C6c*w3mf-6z8C&
zP%2cgki&YL-Y*k;CWPM#$0a2O*;r&bcKjGhl`3U(yf!<1*=9O-{zA%UPM$igL_-ne
z&Ye8ou6%_GXw>LFoH~74HAGin*|O!T9hDr_QnF+TJskogp;oL|iN%YT=rMFc#^7~k
zR8G^T&G78A&*Fm*M&Yr?9!r^oUAS{3CB`Fc`xl70c2wCESJ1XtDB>v6ybZyYyiN<Z
z1obz;8yBwm;k(7T5F4M?Zs}n8{rBI~=@L&P(@%DKJBB{-1YUph4UC&G0gcqd?u-&&
z@4fwwo=2-v<;ps}{PwNe@WrQ}YH!@Rv*)m9&mJ8|&ZHLu`}N1}-McVo@Z(CPHPTZP
zj8vcBv|*$6_&oK@)0jSMCi?bseq!(o7tZSmnXRS7w^`P&*TRv<Y;B_GsK_Wiy}_K>
zvvKO=DLoLGo+LhKc?0l)&lI#*uU^HGLx(YW(j+Wi__tNTQL1z)eXcy2Cigex6V6yl
zhD$e$!PEgtY(MwH3+lI~_E?(3`TpL0`)!^4WasuB`o5s2m0~_j?Slzzm^@^gx_3VP
zWUQV%+%z`99Nr^Ut5rkKM;^{p=2LT6K|c2Q<LbBFuSMa#%B>5xu>9-gxOFY9SJB7U
z2W_5dqZ+8j3NylvhvCwZOU6<%+`f8;J2M7=-l%S)yE*j2RqI?0rR%$9EK=T5d2#=s
z`|W-wMeVAuS7GYlso3=MrgZ1joGZt!V9A&zm@;q*cFx$TK5V>Ug#r}|pu-y-j3q(v
z92h2fG2$#D<B~h)bWwJq8(tQqzGo`Q2}0CA06UC~^=hM(_q@1H)gStEdK=wpvna+F
z82I{lIW^YKG@k#sYOBPmy1ih*0w`0aG<NOYqsLws&fqwF)=!)lx2URH?!Ui@5(D?6
zsj6Q(cIcqJXx5<jjvb1H3l&P<UX^C5v~JZBg$gB)E%#UTQ}ycA(79759YV=^^W>?M
z`aK?h<;s=Nv1123*rhXKRNLg}(PR3(|1xZc*ee`{kvn&8J*1+0_a1mpwfDaHW}==S
zu1}x7+5>m5q@MRQqE0SB_>uWam?hhGvYAu7slOC!>TjYJa(vCd?ELpaT&x$qUy>Ua
zZ&+{7tzG*MR<BM!ti+SZw1DHoscyY`=r>>h0s{h6V|yz;{9qK8FI|d)>fto<gAa70
zn%<Mmn>OR>m8+QY<Bxc9$WUCqbV+;ePMti7-+uX38_v)thUv!mwr$(=*f)A;=zZaP
zd8~|2vc=!?7wGSJw10o6%b)=R^@QQIYuCX8En33cilJV-Z~-5@`yO6<<z<Zf>TCQq
z^%u<gb0+4_nWMc!Q-Atd3DwW=;`7gA!q?;U@3n-zH_abtfhJ9@XFl{4N-Gf>U?p6U
zzgbtZ>Ve2H0u<o%)LJ7_3ErQ0KRoe-o<EQwwBM-*=C>0k;)~Be$6If_fv41a)71TA
zD}=FV(PHSUu3z1{3=_&aRjO9ioe50N!~6SQgpHnq3lHM#?)2F;OVurfVzr7HOQ|Hr
zCSvo9&BjtHP5L&`4YoT$$vP#`a6m(2DLvWqXGgtW^;|Y*8U<?O#~Pz*2m4bIa2(u`
zl}9k`>1h~Qc_hAU`z2<+I#W+<zWC$CnD*>6jIKW#qw9^v!uJ>I?3wmp2F;ewwM5aX
zu1&8lxCpiuqN0(c8qK<dt8!aCpV<BDDiY!V&X;B?G2v>R6XP}5a*0#h$K5oFdYzDV
zF2}5qgxL67h%rjEDzOP^lS--PFskLNhXUCOBiKJ1viY$DtLLd@o$7@^UqAI;fG*it
z{Q1{kzs{lRmz?T#PP4zc_?mwMSp1M%6|T#zmian&pk?t#{@?&S+q5il209+E#7^mw
zB}(A8-+tF2jO@s}ze!`&&|9lt=gMW~HPd=9eJ7?l6jS^>$H5&rdQ^8@(^Gc(^eJ6O
z5(-NS(p$GU)j=Y%De>01b0@4?y#~jQ9Yd75*BC;1{`>`Su2T|n<qA%oJf%C46XN6b
z)H;Cy$<IA@bW(s)aB_dqYMXmHs>DeeHf)H=lYdB4e*AF?e*N_~%$_|*doh<TU8dK)
z`R1ES7<WRsa^-aU^ImNWcmnaaF5u?AX^4-DNfvCnDBCgqnwFD2aNbCy=S}@(*2(32
zg0c0u?e+j2FcT+!lc}uvcMDm*Gx?C%y?eJ7FHHErv1HFY`z#tZx=(vo)+mKrqGSo}
zd5TaK9X%}U6=E0xXG1@9;2=0@a=rTXF=)sTz5d+UGx&7OC%AAv<H<v^0*|xTBM<9=
zs@9B_56m6gwqxp)DLOkLGiDBd`4uHL-c-js2Ggfa)7cn#PAp;XP2>CTN1uNEJiaG~
z^R02dH%sVMHKk{zSZ&v#gBC<3N|sEiu*{*@qv!3PHEXoc|4xaw?<anXKh#5Y{knCy
zX(#l9@zor+)~!c(mp)*dHfyFmaU3LmuM&4F4r?Z_F_u!vQzQ?{+*c-jOGG^+uO3t4
zoIPM><SmgG2|gJQRmv=_pKpzPWvqv)m2FlQm0DIxf9zSnH^3JIz8`=p?W@?$N=Xvp
z5)gSK5=U1Z#i}n?>7+69-=2?k->t*>UFQ)S5vxNe?LkjtlOauT|Ks<|*tfeCRq4k~
zn~uoALlF6-D$fo^%=@n(=~{BuL06D;emml3JchWLs?2#5@e7Bkb@nrsYGQ97Y0G0s
z+|(cOn+G6v>p&!&ou+o^8XJvQVsO==SFl8tzYo2Nd52%a>a!D!cT*{uvpj~?c@0n1
zeF4wZ{~w-j@C=GtBrNgw@kNh{P4L!zJ@8(mhcK$qgZQ{n7kt*F6TWQR0ppsq$M~jg
z@lCTfnD{^&eBZJ)Cbw>hAKSLX&+S{{*N!dlTbCA?-nBXY?A8o3d#KV&m4{X7+Y~eV
zH^r=h_hZ?U58$mfmEh-OSoMAP-G^$`s-t%8T6&pYw?>T`p>CZzT9BE@nIi}4)vKE_
zw1ag}9kmUIVwhl^SEELCv}xTMJ9qBFEF~2FUbqma&m@ntBlNh@W421QlT1?&Fa)w~
zo7ULAV<+a!ou@NSp1*KGwJVaZV{A+ecJJAZg=$~RSFF_85X%|k(%C58xBr0po<&%+
zcnK<2tc0pnWTI(?tk$eqGfm-nt5~t3&MHY~gS9q_@b8h75QFeTa}gDCC{?Jb!sfMh
zywl@mh&KJ&2%Aid_rm$Bept987*VnIuPMiw&YnHnYP6gu;F6Xv9~yl4um|hmK98Vl
zw{D1vjKt4B{;0(a2SifXMMp*J6UBx(MGPka4-31ArAwCJtP*W)+qK1j#~wwIqD9f^
z!Ol7(<(-7b)a3pqdj7fE77y&Kz=wN>P962+<jnf$>G%iuqjJ?M#!@PqHf~5`2&|lX
zKk|s4Z;j{E6`_f|M$C#ie8dR!?cdL4vXsoi{Xh6nS9Kpg=CbR5H*$E{Vcfo+HpvbJ
z3k8yofBFuF$g7cv3`^VFPA^r?qIWJCPrlOm(D|LtC{!hNEQ(){pZ1b<9?@A(dhKes
z&;AqpqjvY&I=P9vpy#dat8MYnsE71#67Et|OCpKMA@=P<+yPY%?L+*DBWzV08)Tit
zs4yg6+K<G``;Zu-%9VpiioR*QYlozGB;DAr%3dU@l5j(PHj1IoX**srOA-=r{nim&
z3qOo2>TifD*Q3s*h!Jyg_~%0L9HmeqdkK`tQ36%+q@AjTp8FiW0jQO?1Zw9kjym~@
zp<ceCsIN-H{6)|xe_=E(kX)J+ETqf*g$kir;eu#Zq#&9XEr=FH3!p`@0%%#h09ut$
zrKBpQ@}pI0Rm$W?%d+`Vt4K}+s^^rmFif$3|GwJuLin8N`d}B;&`*2(e7}45ZYlK-
z>-Zi$x}%`Q6gqm0cAeT7pz5kVk35XNefwx}W_o@xT(W1+9y;8SuL<94*suZmscj$b
z-3$Hu^+i+FE-`%<J1@JdHdb%-USHMTYS5s5N&+tm&Tgvy;^+8Y<Hq;7yp9xvJ3;K#
zBe=PL4ie&`lLZ<@9Q!~i;<VR|%c&xcVUrYa@$o)bxGNY(FFCG9<KyG7V#RVj*3=Wp
zvVh}*i=mWkq(AfQb2_Ar<H(jTTc*Py`Jk&(wVH0&N5`lOp{~NEix>6Zrp=q{ht<LT
z`}OvG*!Aeq6YsqDo}N;`e3HyN1s`^;+O)xlci+K)M+c%nfy}4z=5MQ3qdMMr>n*%G
zd^l>?u5G;I*-(r>*!975g6z!cGwHmhyoL{U?TU9szK0=CJb{v>N@c1!L19%@-A6CH
z^b+2D`z>@<6<%htPC5g<hrIT^l=yT_)93CZHar%a{@7$JCByr*RGm^*h0u+QH*o!I
z+Hr{V5;4vHoq__hVZVlWX~j!;d(Yc=Yu8(Nd-vNI^4ky{V(Bh8nRcIXeb8j6>n3Y9
z$3Ms)T|exKPH%T|SqP}`U&Kq<q`6pOiznV_^<<rdL?sY2`9N%Fk%T2aF%g#|!;Gc7
z3zN7qVYzIAb&@&k@Uyio-=jd|a1+aISl?J1d(ZlnQx36BV(pmtzU46x(|h-F2c&Rs
z!?5CzHDgpeB_vs-Y4Jv}W)g16<J^+Hamm6hiQYFQ<W7b7V&Tpp^}Mofi<X={dlrip
zE%eA~1zh5R<AdpemM!tv;6ZwF<Xhq482|M+?U~D)FCS_t;Sw1cl@cOw;XK0u5_My~
zQk5z?#(VY3RXS^9?mW5CwtYLDRKga1s^MN)eXeG$S}rLyYSl#PGN}zwKKwYY>RBZs
zznwe@uf6^{nmzD<&aUVP&L>w&?dzd#-7#kDrx^R$XBha{qiOb^5$O7_QM0BN$(E&1
z2Bub^h_?KE9rYXY=g+u6<@urbe)RD{sMnxD%KmtrIVjX}mV3^x$L|{U^ivo=aU$Lu
z`99jVYlp%`3fm2TWaY&Bi5{^JKmG_~KmQDm4;`Y%O4_O@xxR{e-~4x)N(ptp@z|Y#
z!t=$KUqbgDJ*@7Fg(0z?3McvA{?~S6DU|}{3cxRW`g0U<Jpy5u(>J)v*0(<OhPz;x
zVXnftP_$Z6ou!eJigT&~SB6feL+v|9!5mt|4VgX!MQapwsh5lste@O`64AF@IbKQl
z&*9~RByTfs3mZL5CX(oyRPwi?&LA<J7<5k2mB?8%&E(olN)jFup@rPt0>_P6HsUPn
zGDmP4fQ){IutVG0&<jEMuaT6PfY7azar^vUCCHMJJB<vF8%J@NM4L&((cDZTF2O4|
zK3$wUUDio^(Ds%rS%QNH4;pJc2d{{T2zC5CNlE&_W6G$=v1pm){JC>_^6g!_cA-PZ
zj`(`QcpN-%KxcB?zHJ-&4j6#<M}43lSQ95q&=Vy;(zh>qJ^Zj!qJ+nJ{n~Z)*HtM%
zy>wWht%;!dV84Cqw)z{P2MJSva!wYC(uDW~CE9%TM4AN(7DUmaMRnt!qBeVu9Psz^
z&r;)(o=c7!OtA8Br?|7$b7u`^$c(p0nM$$4eV9TwG6Jt1r%T{DFi&JbuNc$s+7eiV
zp|rPdtIw+Yi~~r|srw~VUC$^j@RJY}6pR8&SQal?T#rQyR=?c6bx@UG-|(vf64IbF
zNP~3e25F@c1f*djA+hNW=?0bV5)kR`?(Xi8?rzWGci-=OKkswS%=zogJahhGnBf}s
zzGAIwt#5oj-*to!<`zAW51|KRCso+)a^oKP=NhvVXD)OG=Dt$a&3uu^(`)_l95;l=
z({cvUn}aVM9M0Q6${uhKY&vM9DtBYm^lr4qL7}5bV}V3iS62@a*85hoUJV|6_eWb%
zQX^?na3C2sXJpwH5pnlRm*dPvRON<^jiz#FI$SsP>SF|A*-z0v6#TnBJ&=QItoak3
zFXUY`>-L#3iyF3F?*v<N<&PS{N;eiO0aPMlEeFh;suW2>liA_X{<kor`^NK;+tbBv
zrPl!^lYEjB1fMzhP!M2KO4=g_6%L(*Q&={f@R?u2L`Cz@pPPjtn&~aCdl5W9;#-m(
zVxFH1hTreWh|Aw2r>Ua6^M>_MbaPw2sSmEBf&0?BLLHcTnN;hvQ?ru@UzE0YWWk5M
z80`Xz{a!zCV^7&herSI`@l6Sh=wtnhd5pgrsk!ckT=eqT4#}he?wiJx@(^Z&hWd}k
zqF!1P!&SXb0YjhJU3RN@-S&xBBi*oaq>qUujtv$96#2WFgg-&%a6x%rK!ya$F58js
z8np8Fg>+q|1y<TDV)(L9_QMGS>CR2sKqdU{>F_WST}ES95`c5;Ufb~%4r+g=E?f?u
ziI3zoo8%lQM$;_+IAwLV6%m>x00$93wf&sfPFkJ}wlqW-LemrKYou{)=@KD`#H1L`
zfE~(eGQ8F~noPaA$DQ=|*1mlKUFi~kSMnTHMUETx@UgGQGcxpY9I<}f?4#S=Gfc(v
z_w`(+!VU-X@%3e>%}6yv!hb3H2nMvhYLZJDdR^ORIVLv&*qP9@ULMQ(EGgcgnFG#G
zX37#HUHkUpmr|KODV593I6W|VU{4k5jBUHjIc}w4gu}g$pU$+7q>;y<`s3qsT43d>
z_R^KYUx&y;*OIz_iy&XI#QS2N?N}NBp~sh8`0veYPX1Ip8OS0$7y06;LC5>R=b(=-
z&P~mqPhu-cdR9-sD6GJ?mM_(dtwSr1_e~pF?bQ85kiXU+w_cER5h|EU{7&{z`~La)
zN7FQ2wR{z&2*3WU$`zq~znr)>i~M`lSLnP|o5y96idNqy+)Iuw1-$7~2Ex8o_YU4Y
zyR%bYo7p%19*>;Gr>n7xr?FdNPNPon_jFA-u$J(1{(JvtnMSb*bUKjzdBgNgv6B5u
zi<|qD)C*a8Zv&qrhLvjP2O68$?t-qe_*m=;nJp@+L8M>(tyGlcU81AzJTmFheu`DD
z+5tV%?-UDPM_H;9ew<SU-PZ3WD2~uhB+Ts?S1H%Vv|s!+f1p#(O+$YghF-PuS=oLk
z3of=iw#=rl_vnS)BJ<E5Z)OC*gs#SQ1~a|MP`96opbZZRZL5K7eycLn$y?GcwS2E)
zqDn5-L|s-rHaMDg5nF7{v3jwmiosryAMD6{&Ui3QqQj?sO7eaxobvdHq5@jcEE$~o
zg)!FjY0=$w7fFu9g7_3S<@N<KRjzGw20Ffw-)u9(@U;n=S^FDnr=z=~Y7$$tpf61{
zUU5vtufO1Kt&f+@O!eW-iJ_CT^$-Q^1o(Ayq?s3H{Ki$8__;PzhqFjq=ddqX2{LCm
zG2#*ut5i97zg6gKm>r*7mKgSY7+0})I*BpKYrDayny;1H)e+xzy($Xx&G7xgz;~+s
z_myM+VA6U{YWCKl13ZWHBcETrmg7ikAn7`%#E>YZ3zVwt-u5RKE8l|leeRgOHV3A%
z(k0VX%EGnY8yJ?emn|JbdS~=qK!S2|&bq+hS-zrYjZjdZ4$_-RT<0C3fHxDnyVe$i
zNUpZLOO%@?g;}!~gMvfzL0}9F^sbdS$XXr)fZDgvI#<Scf*%=F8xEb3bWYb!>EZ*}
z8gB8+56q;sfmd5+iyGX$Wq;&$kHb(RMs1V+Fec$R2QOT+<v9|e1{m7lyAU|*z}X^D
zNIk5F=i(44Ea!B2w4O(9Z*p>qM10zf)z~#;I>0TvBwY1UrlNkm-5vXm_ryN9Tlx(7
zt=<Ci5q;8240CMx0LIn}<uyzKvLN1yrGBU7!G*@qB1ic)KTVzr#BgdyD;-%AAtb)L
zhlP?<T(Z-Lgb4O5nM{v+ms*J3TBjUjFZ1091-+c)qH?HFjomi8&3gY+T{2xr!xO(3
zm&ZU^W6=EdVaGl{1+T&`ceER@H5+Mb6pS(rH@ZFC$wt1@xJ6i%9yT|>Uhj$2OqE%z
znkN%bZ-Xt2hK&eH8fPOPL@4hi@!M-S%9mORl57^);;nkwk{&mI#+;pRMBr`Of}^4l
z&$}60w^rT^EQ-cTa9uqsEm;)S)LA#X5b~C;0BNyjfd3s~I`*Jg*RS5ioh+B?aTEXQ
z?k~-t%kmE<MNIezUy5|AtX8^@MJoO>J+sv3n*%F0C0{y5VVYHj{Kt~sHqW;&CF-fg
zU^qh(>A`+ySM^B<HrawseO$}|QjH$)H6e2cSsQlnW;FRNzmN0S|9pJ;(Dq9efPhMW
ze}{6TGqa&v8)xq{#Z$l8KpE854643Dg34{sB^10qU;+HQ#oQjQLJ8lsM0YdHSFVm>
ze}6h@H0i?$5!`<2opvwXNcFS}e&~x8I~q!;z~}LJc0tCQCV{F?={%6&S?qeL!<5fE
zH2rXoXqOrPEMTtG(Y<fdg4gOuFIX{=%M2ZCofA*AlQu18$;Q}Vi*69%8E*tn=;%6`
z)xO4;%^|R}Vf~Yn>k{nM;CiWl=L;*=Uz2PQZYf)KZ_~mod^4ZQAv*91w`1}mF@BY?
zgokDMWcGcaajDp6`P{Yn(5aoxxXy-cdbleM1(IdI<<Wgi_4@cljLoFI>F=jkpWWO%
z-n|Pft+>9qbAGn^$au1;I#);e%j`CIklcjlDR_(6$~-gJiuaA_NrA3q81tdSd)iXJ
zvp~eb<w<?211GHG6~r)BDHzqw6=m^B-x15{&yw$_R5TsXzdb|NcPFClEic&KeemSq
zc^<A6RvR?rc;~goQ}hLcxT~#$_B!1@HvwN|TL@RB>3r#HM%O%?6evs&Kcv0?szg?w
z1JPVBs9Nd(MNVJuu1~fl7q=oh^`>#elt5afA;NLdT|wd50CIi9IG;wD5vU7m3O`sN
z9bGJM1670DrJtQvuK#Y|$AgGjM9NX(7}`-CItjmTe3{YOcPUcI4wRQ?ymz{ux7rvy
zCKIK89^ta_Cz0~mA9Ea-NJypFdKBw#&UXl1?IDj(wK2rXcO{bGz4Rv1pzT!MWgGn^
z)5`r$K4PJ4JiAM6aDFSFkMPwpTmXSmB=)Mc(V*2lZ?)j{)%cn(vi-IC`?a7`_RB`<
zzi4k1NhL_{NE>IAamYqr{k?(`0_51<_`N=Qz&WV7q$82OhLJzgd420pg?!`ZGsgsy
z&!%}b+r``Uu5<cIzO=r{d7~vVY{&q&u7rkBX&G4S80W`+vIXC+oz(HNXE*sZIUC_^
zxW`uxyuYepXYf%><MLb?qdXMsqF!n9PT8soBrYc2?~7wnAb}AM1+CXeo348c=tM!U
zgn4**-?<LI>yBoSu5SN1;~!SY{#lv`s#gE4>8;0t6}R&iyJ)(%pP~vE&2|5>M?AhC
zaxKyn0-|vnsM<U`_3CN21PJG%s%2+lJ{x$msY3fte0E#K6XKSGHap|=dA9>5Gxg4q
z%9kEngTmHp!@c9R^7kwC=UU@9PG+z~mx2ZxnnR-fX0q#qhG@xpEQc6Ys_1KO_Fhl{
zzy|HKgKi&4KGK|9NqiwwVPs~Z+$>ck0)t_I&Ei)1Ou*=X6@ZB0>fx5PH`tss4U=U<
zBSf#fBCwqA%&5duL_R#@$k8y{qs{f#<+b$RZ$G$aSrP5F+4t*<Zb0qDJ81EReabcr
z_jbw@AzMf9FO<O6kNHzTZp>yiAS=sXOO#_7KrYD4T_Iu|FzqX@6%^XilDBeYxP~>)
z<NPu>Ogm6vibT2Vz#EP`6NK8X?k`Q_jt0c<Zgvk%Mt{DS9>o#W{{wZ(4wSF^76e_U
zjWg`6pGsyK7((=0tvx4VwVa1{yIr`Pbyd$(m0QymIYP}SN@<SXFN<O=dncJ`X^&Dt
z6=}s0Q}In^xs=6b!U<nQBA-@MpZkh71wXq%oxqE`bGsO{J|}iA9!UN}%AVKfRV}<f
zy4X6EBU7=|<a2gQ|D@0ul!#|(0I-0?mxm32q-*syy!WW^j-vkFhPvto4STf}%VSkQ
zYw4cRl9<5fsH|z#q1!-Q>n~!J*wpGqbXK+90bUX>KOrJ|z1J>}!>{ME6wlyJGBtu)
zx?e5V%4Tz&M0bvH31q0qpQ1*+uYQT+1G|8=e6tTZdKi?uTG@Ka$m>c-#H`y{Xf{>C
za`xOo_eDpxj9STAKea5x3HuN>8zB->x%{B3wp#GfwfMf(UJE^<z52Wh(-a!~LUY=z
z&^oYZl1XzU+un>5Hi>$FdkN|pNGhrZK5a#>O;6@?qi#<C<PQs}$s#I~X2six1B`Tq
zRH^b!zzO$jk3-U05+WIyk|#7{Ow-E@91{&ssZu%A)O!f-{#b!n?`Hm@+^AQ6uG};+
zo3fq2WLMU=mCf3b;~(1i1HzWO8D}w8O3Epx5bwoPNm9ZfECH#X3<^W;8nhBv2k4_b
z4IGoaR=$?z>IdwU`|d0-C%oOKOJ86uw5%4Aq9_5=M{7vJJUCO~9^riffX31Coj0d9
zo-)(=qTNXg?UPsBv|>X;lP>G;Sa(vtQ|^yle`+_pBWBfny#E|&EHIns)c@#cjI^R(
z<p!l2;X@bZU3Ho~s}o_n$4wQxN8@iksup&QG4QO;L?<5iCXig&?UFM>oRL>X&Zf5`
zcE{Ap-dV!)vN0izq;OETU+m3fZkmqmGBQ7dYy!?yC#V)XpU=9zIA{Yq1aA)Gp`FV}
zqM#T&`2x1nZZ=eQkqRNu|CX`Y^JkjP;QEt?W3BxcK4<NdU2g=GEmryP3WM(Xab1tL
zMt9jJW~3C6^$vHWg(j5<ud<+0W2V{iz)UA)?qwWbK`xgTVNYi$<?e7cA}an@gAVq^
zYw@}S4r7NHQKf~j-IQ*?s}%Lu6AKk(=5=I6`AFxBT_LT`M6rB;y=Bo88BpL1^`ofL
zxZvr#i<7ALo_Oeo4~xZC(h(s@((UWR6QU-Se7jJz5}TPntNlB+`zHbp&RV$|GxG+a
z!N(xAqR45{4^FPvuhqg)s+$H?a2bYG@3lOyey#3XY^*3<78b>N27~l8uJvb|^)AhG
zetp@s07W4Mk*kDMH3Tt+HUp@Y5^}Bb$_g*(EB`f~HY@%+Ze}TQatdalFrE<7LbsVa
zwt9(=cU;9-cWy~$tE@euJ~!uzKdn}x-q*#)8Ey@?@I!?8TrY?zC@6rF(b4W;9vSrG
zd{iJW_PBq(03C7{6xu4`?4gT8K5Su5R&K4PTP7vpeY?Q?)MfUGvP}SJ3`>IsIM6S3
z93D)k`=kVnx8KVpq<s!~bz^U0LdUFA>-%tjlOeM?xHWq&qEe)T7V?TEhB3-p>RS|k
z#A%4Iq?Wji@ptX0pqx-2WopfpFWuT@buNW73gM#CvCLt>FEy-B@ls~%k50vxm*`|d
z=J_?Z|7PY7rV&2%D9~&zl4H2SWqG<J{Sj-pfTjLqorfC~*&@7Ysk)#%>eZCouVSoc
z5QZ<JKnru^@W!h@<(5=j`_!$o&jkO<?TN=sU(1wm1iFCq#g++4c5uXv=?oUG3bym<
z0ysMz@r8t?*Xm4(X@PmV8_w(K%wc;^5foW9`8eN9E&)Kwyjn3;BWxB9N@On{Q97Ym
z^cE{>@cWCJ=;33vRSpR%LPXj0SL>TTZeCC6VrdZ%7!oiV@?zR~l6Rn+%KNOfa$HyZ
z^oz2ryQF>uIR2Qyh0-q>vfX%SuRp)rzCk0hK3W#po36ph8A=i<$&i>hrt(xPF_0MZ
zRk2y*4)0*Yt3i12N<4A0Kru`DPt*{b=IhNgc8f0P@GUz|xnFcOqvGppA<%T}xAKEQ
zHNV{p4zXg>>M*6Dwj|;EH+cYg(h+R2P3zh}>1DyPpOD=8NpffVQlw;%a^=siRt|yR
z92`b@_qh1>=1^)EU~Z4eQrqy5k0Bv>6MZ*@2cX({B>||r^EnFkP^ypGVA2uur-TCc
zH90&i691oxIUmuI;fvgUudF2Q1obe!KgN0&$RBp~tM6T4lb2IPQXGDWcnV9Z`O$G7
zTvKx^lH#+0DW2Hl+;S=!>Sf7wYw?Z%%0$nn>&P|(<H#GssSaHH*mx9_R@Cg+`a6V+
z!9mJP@ksJvTbb=fU$O058a=vKHY1+3c+thkA*T_G9C71FA1truD)YMUlv@rfnUBn=
zR(y%Z9hymAAN|wE>#|N~nRlDmlpy+Dry494s9?*_4=Qi=fF;oJ4Rm<{0K4)`0e?#}
z6#x5%PIouwvqB1B_E0Z1%x%=|M;5Ln%-L+rN|7Mw<KNxK3&bW_dfQmr5Xv>!;u^K`
ztzP$+ExYJqy)&EX=(_CZ@_xoadCEui!+aoM)>;W07{_~^dt{KpvNRCC%rvahG8DUK
zlqX7((Gzj@gg6o_YX)^4!CdQpL`Z0h*|Q(Cm&}JbT!0Oe-tnr0OQE1<;MY1=c&-1S
z%2(0M^X<dyx%kt?bFhwjOy1RaH~p;vpf#Ljfftbaq$z7aRSx5Fd=4xXR(P+`Wd8XQ
zfMxm*l%AL2UwfT@gZ-;_|4j~mgvb96AjkiY(rm1x;8|r#hKc6LCtKa$*hA$et6k3D
zGy+k~e6@9(x~(H77M2%~_F(}!pCm9t*stu)lo?af&~#4sHrxR>-Y=~h_Kp5{pPxUe
zpcd?oyUsX*WV6)~pq-}5GAsm-h0tsa`{heN;DnWgb?q}DV_<ZF7+i>IfyTCl>;(s~
zm{@3T+&$bLx)4eEEk%B}Kxm1M+iNYwrBi%@0Xh5TA>Y;2wQC`F(b76qSq#it=Cz*5
z3lR+IVG{+Kcz`_$%2fS!uZ5~`4&2#omzH`qi?**yKj?n;7tqfGnu|DAebHK%Gx<uf
zzcPV{%sz~om8hWZT*AO01K<TWfnaxQx`a~b@0~3tY*CBo1w$U_PKS1XBL^LtYGYiY
z+5=#*d<@3X+JfVf#O1-l%AY@<>m&OYoECjuLjWt>M6<XPT#^zXi-giMfef!Ws8Kiq
z?1NQsQeA-JUw$9OpoR`SozGm(cl@9LXsIby^cPq#KlB1KUe45M(X|UVAxCM!>*@pw
ziD12}qZQMG3J{Xf;VFNPK^mH?oRht4jsvgIg>!J*KdI)(0K7p?z)*S&J|NSu=f_P_
z(@8HwU`R+?8-{S`qb+lPfA}vU#j+qV3N4}J!`*d>+qF&o!_983aX<s<9<sg|csbTw
ztpt$G-!#9`sxE#=0n*Pb9GLgAC0J?u0WHwBG#fd2i+W=3#s1uP4r6(h+z%b<k^MmM
zd%POL^5ynZm1XilviV$PL1n7-S|{jboTwem2^$HKCFJ_@bH86joe%4QOLxfK&`?vv
ziVMu_?P(%Dqw{9K35vcklv*S(`-sib4&aqhRKx^Yq+c&|HCMOXMXGVCZyFuB;tPtj
zg7PfRfdsV^l*6%^O)@UT!}vG%Fq7Fo2Q#P8NFO?gH`ANfj4(}|9%e=e{f4}EduugQ
zjs{V9w3RxHm32L|`n}L#-JdB#6bnF~*!^_n0xU`{p6zGX!07q}nCV6(<)&;SY2;Yl
zZ_aSlnwVZOYyE;C0blcTuof`Nm-83>4Z0N`6_pMXQj(H8=kv5}NYf@iLlT#yVypgK
z`uY2>0+W>3#P5&FmDg(#!lN~<Ge{nwjMn@?(rhz~*Ol0N8Oh=Gu!FitG3_w%#mz`1
zzEE3=2u``eqGuIssT1@yjLl%=>NM~R0VQsOuIhug6-#rcK4?|9&w+0XkYg1!7|WKM
zRn4Ce%f1QrD`F}EbqgM5BN=`klb73LQd626$-#q@JfAx3WV2nmeGAURcX(QCi|v=d
z)l5*<i)PY%1%|J6@RPNMetin9CjGYO4TG=ZbBknoZG}(N`{yEKl&IQARo|=58k$3G
zkkEEzZR;Bv>~l;ST+WLf4|Tw(4)+Mz#%G$F0uF(4LT{J-SuXdz`N=c$o;}B6G|wu^
zXUuR%87CA=j!W)wJg^iUZZ_u=!-d<U-J&KC5_?)J<+(pwQCk&&CVuhGDijUIOSl2t
z&rmmZf`bS&iC`vf+-uBbIpdmSK7g?u)qxA<)P!(7efsojJyOB0Cx$7pG1n7Vvjwl$
zBX1m8=YK$GoKMzAmZU`9IVl5Y^qhK=N5h2>D4+n!Y$t#n0T9NfZ3`qa6=JB#O&H_I
z$cU*&X2t2oo;8wp8K=dp%2HK~KE2!5R6g71XhhtclT>XeZs3yFdK>)8WKut7Ew=$R
zOY|?3Upuk8J(QNL?e69gnbPov#}D0;LR_{|PC@l4Uw%N6*U&u{rDPgxsgWK!qK;uc
zU#-;SP}x{B#;e?foZ?e(yGU$Y=nS}&XAM0LOMv@##y^RHYlT{8+!a8{uAz%%d?-~(
zZrZjN^^>IRA%kZRJP)0(wtK!Lg2ccVUQm4TDl10pbMPysV9=emniZ?VK55ur$v8Jf
zeDc=ov-1J(;`wm8nA5TsVmTZo!%_?P-9k80HR{{%DvfT=3mhVFTzGQ%TllAQ>0*d>
zyOVORbs#=i<oZ^&M?zhnjs96z?)^z0%l4{Q)Aa_C;Kel5Wv;04Mw%!&*SqXFtQ}00
z=^!4{odT&VnH0*FWa#Q>IG@z4#q*(vT#4b-P{s(Nf1U`*fI~zdpF4iqoDvLjhC5K>
zbj_qxo_onxgves_a!n(%&bAl4@GYOzKOHZYpvtVcYk_rN)SHjJ>XE2oCesW(L)W*r
zL}p`cI+`vB>Lm&F)85Ou?VX(lE#3$>pER31?m1I7-L6jn|FLoG9Mg5+Q_e^>UH)`)
zA0rQJaXdNDoVYYRj??sqrD4f~9?6QVI}}5*Hcrqn2D}+OB&{nwa0KC8FB_1Ji}(&1
z{7r23*yk+jJ%w-gINTL%iQ_oh-hrFJ=V~sCD}^O0unNl}QRY=r|DZQRk!Mi~X29LG
zt=Wpai&kqg+=zT*rc{!_hgUc;`(~BoGeX?Lj(6zj&U&{An=E-tBAZ>{4)Ef&NP9=8
z27%VlFJi#ll5^n}pBA`+HkjfC)F}97UeD18f3m52`1j$ORJz;~*t9A>sr{8q;ur(O
zOAylV;~qDp_+{&n7a~gUeDpG-3k_Av?RduA2Xz=0mt|vG*T8Eg>A-$fh`>P%YM}r5
zaC0-qH5;@z!ra6(k|5|JR<DGtX3P}oIQ+7Xo({-ky5w4PTyx}4k(M*f;xkZOETVYl
z=*dkFqfh_R_whp$u&o0-_hbtiSpYY~N$rXE3!|`&c(#i-OSSuI(t2>5=b|{Kyj7(-
z<<K}Pe;n4@BqMp~ibGJdQLkG=W+^oydHCv(Yq4+e+sIAt40O%m_VRFQU)HuxpD6OS
zLPB-U@Q&GE`gDHmQwuo<t}O0vR84L02m~85XI6fmY20ueA<*+Uh%-&OUF919#Fm(*
zHXO{Sn>0>kV+^Xl6v?4-Z?w8nJpJD$q_yxew8{>5^r?R#l#q0l`Xq9)@dr&#fn2-u
z488QyAM$aYWnqIjM-jC8EM&5oPqQ{On94pw^N(f{)0x(U>X>xnmV{@cS=kYLf5XC`
zH6V>?mB(Jx)q;fpj0b*EzSvww*a(dvdA=y7tU3<2+o0t_?Z*Ha@GV-V+2i}i#Ri+#
zip{^V%diuvn}6j~UYc&x7)4QDagj>qoV1d-r@0d_JZt3nK4LaV_3=#gROoYzO!(72
zB!b+cBnY>1-Dv6l`<5-)`{-zH0s;PQ_qAiIq}R!)sL4PDT>Z+PSf=Y{{AFbarivw$
z-w`#{P@TF6`I=?A2#FU1AQjZryunIsl&!Hc?KbYcIQ}!qHT}x&1TM2L7-V89CcpMb
zvTm2)8LTf<OT4q5vKz%g@anIba;TTpg)jQmU-=Kjh4x?@M~GpvKZEWMw0)R9?M&e+
ztnv-xS7k)?ESY*|XoB2UZDLl=Cvt?Fk4z+F##cC|Gkuio4f#VevSsdWo6{o;&auw$
zywUL@;k1!6jA1SA&8fVzarZm-&lF{sl>7ZU>S+SAhG!mZ)>vJ!N?RYAk9p+`J1W)5
zPOGny(zXn*sESyo5^Y)Lz)5k$G0)crc<FFnh#UGhH@{{2EqkDCTU`hW>OMJ4l}bhO
zFlvtxSDT`73pxAP?xBYTQG}mrTYqJ6C7WjbqxX@J0WnXb^)T(J2hfo7KTk0DCON47
zMYVCSC!<a%)9?&TI|Q=&^n3SEST_C7R*jC1Va&z3B#$;XY`tJU8Wq;h&Ctvr<U{3L
zX8JsjLa|ay>U=nLjD!H5**ut5Smuoo!+GLzyy1F+@)2nHf&+B?v1fjsHT~y<KfeF#
zVQ(+p@8M2s=nwv63JL`XJnlgo0H7+1)-fw(_`cEzHL~`zh#w?rqjSy|bOfq+X=I}N
zf9@!PE6sG-AklPvP6S&i>UKA3KgD}{1vu6v+K<8<@O(R;tzU&RrOkPO&)p7<>}0py
zkcE7UIxr(7FVnM0dlQ)#*Y$<tXCwv|j(PDYFTRbQHx#>n;UqL9dxTx!`*qTrI~}8O
zL)6LwME|lbawG!3725p#IQ00H@3=aXdPRRJBLp+rs^Vg%?TShRVxsX8`M~;C)Afl-
zUwtZqNH?#n>7Zl^vcGBxsUbTufF7QQ<yloW4oTnBIMINyk2(aM?4eC@5_P@3itW=%
z#Y!r8e8yeU;<P%zC2>t$q|-Fal9MDd5{Sb2sobp*tUgPa`jptSJ6)o%lqrS5Kdm$}
zU=SrdgyUGZc<Hb2S7;zE$9CN4b|T(ll6i5-Ng8cPe9<)J5b4{-A!NLP^xwuqueDn$
z>ven$wmtverpgBZqdBHK4jVN?+0t-eTv*>sOX0=x6Qw~-F*yB7a#9X4MJF*1hxj!l
zUL`h!BvaSL@lXt77*^Gv&6)yde44J;#V3YZJUPN?49PPwai0Q%KGD6M8EYAqy|7|6
z!?^5P>M@s^kFmXv6aW3^pSKu!=1~f??c3#>LuFTFR3|c!Us0u{UE%n8F|_#vhoSf~
z{E|br%w{9a9;n^Wafnz);E0@XE!_sF;PO+aBM)TV*WM%*9CCbULcV-HsAC(9u*iYS
zWIkrShnLf>m)vshw)E*=*RhgerqOUN(z6B)0!_O3I)*b%!C=8oN+iIKEFcj7{YPq+
zoYFkI7fLc7jjLm{#jzG!dn9z$34K8L^j}vqQ*RkzLy$A3BdYo9j7^+Sd;{*Dy|Ds3
zOsEfk2H+`n`RGi{lUf#ajF_`Ivc4Haz`+a%6+!oNiPNz`Z20La8`~NNOjR{}Pb57Z
zQ}qK7z$Y9GQ0*lX@K`22$gCN)ML}5h3Rc1fbUqLg66N7G^pm!G<r9ZAC#G2U6JwgA
zP_G>>r|F?<xgXSQo(VeOM#@5E;MmXts{~<tCDt3f@Z_0sq6|nD!y|Es7pkK)pr#pq
zIB(!K?((%VdehxW-%ZN8o5~=S3MNn-xHXoEMWVfTFezQ>Xh+J<bxJCLY5A3%H<X<x
zS{*KLCE!$+(cKZ~nCwiiRSQ%ePj9T<4*GvE`q^l6;j!+q#hO+XG*`T~9<8P`vahJg
zJI7s)U=iQdiLTVTS7HuTetTSZozk-y#9$t>8Eo+VXr{&1bg;p)>CyDIBP27*Lxgz2
zMw4Wzb{ciz{n`-y!l{Hd5QFey6sz-VNU}4!8#jA>@R~K<iFymNBc6e=BP}Y-U@fdz
zgckY^LiCT-Z^OYz`ovJq{DD^RQLh>Ahao@aTR^e+k16x2>i;!Pv24S;5%xY$-S93E
z#4>jOe=OYm|4twJj&`}ezCKjV8}#&=aZ>}YWue5QvoPXOkw|82F8$A~d5jgk2If<s
zCv2eiUMAd|`SITrib)k&oX_M@6@l%r#v%w9{#^fyTJpQaCla_WY%(%fJ{CMV$FRTb
z1t6&ZpLR#!Yoy*5v|?ObTtq@f7BL5u+wj*&1YZC69X}Cf{@)Mhst2*{A6FFg^vFR$
zLGRwZduwOM+1lD_b-E#!B<v~F@wz7LHPR#-L1I&ra4;TA_<W5mC9uUIp`!XmL_}~)
zKRQd}|K%+G*V*p%$9$8aTvj&;<*WzxKyk3iZPGFZ&dzV%--2tbkGas8>FAJvmGFiI
zZ4B_}fMs*-KbFl%ECG(C$t2EagJ-b~^r3BrD`qlnIAXZIDvV(veS3Vz=jRh(%Ubnl
z5Dg{&-v&_y$-RCXO&t1X;c=*;o^lPe8@uo##St|vC2^N32@k-QC_4FHt|fs$nIz#;
zseUwtPz6l(h#q{oqJc-x=G5!|{U0MB0%cSSoX`Gs*TB!#g`fdU>C>qVPXv+Tu+Kw5
z6^poF1a=etWAXe~ln`n>S)9<`uEMw$FQ?A@qScc?29ARu)%iJjpM|VL$K$mMFDWOF
z)PB6VyT$+KZ=cFI#&l&yk$9EUpR&WDD1Wjs26F=HET%F&2T^Q|2=?Q6NBhsGO_y>+
z7JT~A*(>Yc%~_lzE+49h$&P3dz%8!HJ`;`n-;XZiFf)<iSo+o*L1v^73}%YQ12g2X
zE((h=|J$+Zv+)q}*z7!CqLJ5YtE-`0;Cmp<Vq5giKY0`rSN!L8K?er&zYk6gV8lhG
zzDD}wQ)xKxct>08|H~nLgN*MtLE%EndE8vg<1>Qz_;Db@3xcb}Gcgk*+2GOme;y9h
zdH<0Ci&Wnh^8>jc{FMGc_T%OLFJEIVR4(XWO3BA*?|*<c_@7aQtM?v^Cv$VgasJ1h
z>2y2C`w!IILRjCeg925l*HwuJ7eXbw_j$D%NQ$J13nkjgB`u)gO+|RY8>nw*+`MX4
znDIlCycW9;4&lgPEKmj!t8jn_daYS$esp;mP~~Xj`QE@_6#_N(w&MBsliSbL-rlk1
zpu%V=)w$fCrMSmyLmD8j+Zps?%IZKGU$l)9vuJR44+AmX-bcDK$M-MFhQr3%8XOA?
zi#B7;CVbKjK{fs>A2{;wymr(mQbm$0dtvzE?KD(*{D|t7*jt-n(g@zeX@=<YYU~2N
zn;7hiSnyr%{AiK>?Jb`Ev=J1c(A*P}KJ9w<%z;Gt&lbX-fHf9*deu2!xw-4}t6PH>
zS~2nFd&Fx6l<K1`ct(EQU{879mH7eueRml#8u%n+nEi?LCfeH~jq8!d50(v#Na5d5
z6#mzAT2dB@Q#3n4bdl0|_D7)PzMC%zko0*hC3d;1AmINxTB|lwYc50Hs@n;67V<@q
zRccM@RME*C$4FpDmn7Gdh8^-~cW!W0g;o4}Y=1*Rn4R=79y$K={<m91&kJt-&F`m3
zXc0Dai(sjLfc(^vf#xYxO}9B)d4=D1lyv6KU6I1^N$(#F@s4LAzf@Q)=;IcKrbqep
zm5$XN)}K>ABM~V4w6(nNu~VI31Pq=Ec5WF927Hyng!7%V#se~~5Ql?*-p|!ML4n~N
z8*s))R{@@B=YMGo&cmTD{fPyY^biI<Z#czLMN1VHWu(iG41OCGed!+;PP$_m>Pn(t
z|Lj^5qnl|=E#JmAI;b(qf8Q~x!y8jzcJB5I@Gy(9Z9Eq3WC&qzqI8;n{V}#aviVuk
z$<GJpezZpU@*AWXu`gn=J;+R{Q(!VzgARZH%^$WHL86qgn%ly)t(S96@pCFH*rAAd
z8*)1O2*J%oQ`&FM-A+E1)~ou)W~&X{{YooL%%LEs{a^W!z~q_Dvn)l~lqTdRmN_q5
za-Oh??G?<VRRr5~%Z}Aws+CB+FZ&{}rJG+4CMpM2>M?CSyK`GvvmJu(K_+OP!IX}`
zv%+gNIPSfR<j?y_y^--XW4B|Y<LK>-mZ^9mU)|tRC&LSQzOz+?Ti3PC#+UXEeVZG^
zQtEcz$KkE3Wt`(d4ZWE*fK_&lj9@#Iim@6TeQM#IvmB<{F;bz?xc*XA-FlB^QPYL+
zy?^H5EIms!Ehd@6o3NAXXJK3^0V+=5Oy*6EL3Do_bkK5J&IdASRxY;(;Q&3Pe<6qM
zdbbxa@Mh>VxPU?t<el+cAAr0kNVOBjW7cX0YW4Hu{rMW4t_ZR&5LG*a8F%P{7=uc_
zI@bB_)Kr&hrX(6Jqx$ewx!KC~a><dv0rfO9KLERdrM^FibS)pPv{gGDQy3T+oHfLN
zPTj}w{zRUd6d=kgRebrl-W%O_ReoFJe#;F)M*;hDRlBWgsAy;tQ$Agtw`Cxna6|g*
zW8N#k)?O&tLCPI6FY_3puRl8{CKe>|-kNrTT4?YVhAmb|32?skc|-k;)|l1i^9$e9
zqKFc|*LMN%^Gk?g$NT;={PTm@wn}eVrV|T7GSv&8K;<;9t~ZTRn}c>oItk}+he*C!
zFZ8V{zN1>y_$eM(q61AAM=`u?ixPVIopO1Rb+~PZ2P4_Jzp#7Y<$4H9ZA=Bd#`R1(
zN5sylF~9Dl!&I)cPW{PSYo<8J%iJT7A+2s3R44N|9pG`=&Wa7?3fHHetAgpu#<4yH
zeB5%4ZKIcy6H`UHT_Dn547L!!HXIiY19_xc>%-o)wQI|X-!E$&55w_voLba(`BR)$
zk=r}64tW_B)1GvO5@Ztb7g7lck%HuIb014n=s(6<lD4(w5|5J}+M5)+F34La&xhkK
zGN<DXJo^CevigfzGklQjd<Ms{$oA&^lZ&NPhu)}eBI|0MUtkE&Za>fRnKGaE{Ybeg
zU#L@g@+-?lcIs$7L}P@c&C1Z1=4YuWVUn`mYsaePc$)E)^mN=QC{?>3*;h8#{K7w;
zY?atG9oB!zuRw1KOOF)sAazd|J36loap)j2Rc8;t?tSR|;3B^e*lCTZpCf3DyC6c!
z2$u3kvY=)t0K{aB-qm9T=b4UWn-(ETpb}LU8iMULAaX;%(kw;)SW>o2EkhicseG%%
zmJ8N+o&5^=j#;gB?Ez%f#-eL%H@{%5NDF}&rAnSEns%*S&o?JZT-7D_OA?@m&KNpw
z@j(o8)auHXODvb#2UleX{2i2g(m&Q3OzC=(C@Ed+>bD5R7W%C%ybn2UiOSk;Cz|s_
zI5m`Nsysj14DeyQTx+Q=(s+*r^X@w`f12|KiKf*mJEfWsD<_02uEpj&h;=^v{gSHz
z`nhhh!OX~SH_POHao2Bq&3x-`7_e4dBk;|;_tlAO4gT`nGI;f^v*K1)sypw|tm5qH
zme;p5nkZ7#c_5(-w)paskFfFD2t2E6wbaj1a`0PfO*xw5eT5aY`&(pt)+qY*ye0@V
zBY<%{o^BKW2b3nyx^@lsLR>HK%{VaH)~Am^DIA|_p_b_cPBhlEm2R<~*Q=*X9=A*x
znIuvvyf4IFfc>Hj6K1JPk|Uvi2ACAo$pktqRK-mU4Sx!C$t7^M;OV-h7wM~}E8c^2
z-bjtD@!mom&sZx+UFB)~v=KhQjQfiqDhgY<W-&55oGa+Y2dR&<7*>Lws-p8wH5Lb{
z-J6#m1Y;@#!$P{^`*@12mrU;(R$dzw&W80(wS_q%kV8m8hrjV|tQm{mN{b>GkNso~
zPa79qv?k&HjCN(i_}yQS74U|rQ@=46E^JV2u;Q+tD}=W}yj~8ez$?-FYhSUr%3<`P
zBG{-ckegbGF5tdDCZ*Lek$GimUkw12Slc$clBF|4!>rAc6y4i1#-<$(R8LUW+&-mT
zN@m$CpB8?Gcr-#B1Co;JNW%7~EDbvTm`yvcy~_Bp`jk2&a$Jqtw959xK`=zHOGjt1
z`_EyBq_a(?+(Z4ZQ~}zPn+2lFoTH_Vj}D!kY+|U|$PvZ^30&2GA8sFU=v_-?#tMwO
zjq~bjE~L)uSa-lbjf7Mz)ZR?s)JP!u{xB;DXvx<=cGst<2$-r)k8U(@5<d4vwWNy$
zEXVq7;?D_}84Zk12{B|0kQQiGb%MzFd38^Jsl^<(;88Q222~x3f7-!pg^VjI2|0Nb
zs2Dx>*flnv_0(`oN@!cB1jSjw-^KV}^9~Pv*&<qTm-MQ%>r?uO4&6;J14{tKO^)}e
z%EA*y$y_(q<-c`Du6&PlTOMCmWN?>r;S9B`+D+6kW-gQc5&B^$yR7eQ4c2f-hKO?H
zbq1fn+_CxmnxUS=9bEe(Wz5Ud4j#WGJ?e+^O-@iS0#XKf@KQQs%KVQ?pEejl>!Q4x
zT&~oiWjA+H41>A0#=frzHv3LA+>aIcIDyx0%v#OQ@jw-~^N=rS0<Vx<X<zjP_u^D`
zlaN-9FEL5FgJ5&$5Se-guYVL>u-5NYEO>?Xmkyo#alYQ~-)hM{W^C=YKa3Y@&#A8w
zpXq@tvn<d20-9e7$Y^5WM%DOgPQnwf9FO5dzg!WWr(T}Ea6W9fESKtyZtW(Nr6|>J
z^OsGAy1)Zd>#xY2YQpdU;IW1LR>?KUF9F$!<<<$-dUi|Fjk_`o<ejR-w%jK=WW$6$
z{7V6E|DK)GH<;rUfkgF+?UluhL;j32aTR4`rhyY#0ws>~-+wLH(h%FM8r~8av|xwZ
zmeGvUamP8eSt82{1sUF6&!HobK|Ic*<7(8V9qB(r@Ysn5ym7V&=8-3{+WUfQDfww=
z*pR}LTX_7<j{$Y;etp=I_gd_T>!4A-P!?f=w7M%bRj&7X(@y2OZd?q(%l`3n?qxw&
z;Z(kWS436%OHA=;I#;mPK^Yg_BrI4y;`H)BEQ_u*<s|9Z7c^2qd_acDC+}pfx;?04
z5nB!GYG(}W=U1<>`JDW&<I}_SCfV86NZX%hUfu^F13no~bSMe(&w(Y-E-+y$eN~Dw
z=2Sv!Go@dRgK%cLf#c=1iAXVsJQ)YSL0lbx%N|!EWRWO&;DCza%`2#<cCHxlKnKy`
zSLrMiy?Gf3+j@(nYRBC1soL>1nb|OGon`CvfF-W!;7Kc`fnBmC{o^`SP^US%hv-+w
z4-Vh?;5M&5tj-S`%hLr+#+KylgAYzMf+8L!>aC}ccfJuZI8&KckkQ7Y6Q!3@k)xGF
zs7aRk3q7n%>w6*!xwOV9cC@D$x~*Fih~ID(tYh9x*N^i}7z~&vJO23H-B7<W;L_Q`
zXZ)E{@0OIfKAGzl^MG`kPS0oPB@}jGALo+1NUK?_)UR@^>v_)wGL+e6Z$IoE&krW@
zU|7sm%Ej`hq^3(nte8F~k^SqLaid^O_myqOwz%Jt-YkO>o^fb~ldLSg!bn%trL2|6
zkaUqwLy|OEllct0dw!w+?fzszA}Ns*&oOu<tz2)WI`XPUKvvF}8^GOh>7NGE046qe
z#t_SkGLaKfV7-bE6X`OgOL0A@K@G!LzkEzjg14*_nbi%7P95O2QwR!b;VtIKp0yrn
zn~^Hg%;{&#8THpkDw8=;xTVO)HINE3%?AIPE}ts2{IeRz_64(cUYBbj>d6S3bsVoT
zTx}B{LS_zgR}7r-n4fk++igJAx5g0hDP)JED^;5Z^G3RuIfqU$TN6&@s?SV<8`9#B
zJ3pmP^Za5FpAR!*eqUVPoW)r{Ts2+I_Kx+=-MUSUYkiNloQUg;mO{FGS(q5sxntmM
z<(C56rza325fYidWF1f0XlD`66#pJ@S6|9z+u$q=dDiIR(g3+S_)6l6l};ETx>23z
z@&VL___)~bWLOr=UKtM%LJRc5fV`EcXteG5#O+~I(dH*;TjC-Q&RHSkYT1XMl4|oU
zRN4`TQZjT{^i5_8k7)W8s6rr=nN0tsmw}7I<?=m_YlKj?7bx=V8c4(tNLqY}%+Klw
zX^>2Z(?qiC6%M`zX{9)C5I}plf#Z4fG><J8ZHTNWr{-NIYes-I=)k-o)eH8JQYP$A
z71j+AF<vRhxWFE$zuC#-54C%&R1q|-SGzq2-Pd_)yQtYoF5X>(Y(b#NBm6<Tp2pk#
zN=vu;`1N9ZITF@LZv1CwX0s@U&_f+pe6HSC?`?I9-z>K;Mugvxu()&KAu9O2mJZ^R
zxgrQpTw7oG9K0bhl|6BELdnE>(=+%>{r9(j?WGWiVGpESCqy#ge9jQUoTRwj0YdWx
z67--}eZ+Wt`gwoOdPmg5TI_;b=9$ZWE5?rzgq5@hG@kny6VUCw7J4KUqsX6xlX1lt
zbZHRSHbSCKPJ`8Zqz^`r3TF~G+augBjOroU>H1d%`W9bv7QF&P6lB{Kj5%KIwD*QJ
z^q`6uti%QWrI3ms?ErSpuq+kX&5oJY>l~z^%<G%bw?lL5J;Gb6@Rz3}0hwWr@Melh
zGv3bv(G2|)X1vcai3W+KDhzwDlj#9&N2ps7DHA!II^%2E6hSkqAW8<9x=o8BD4pDr
zniDg;kU#Fpamybr{Zh)D|6!1VV_=(45^=xe%gtuB{8p>cZALwkbTIN_M2c+N?7g*Y
zi#di%3kn`{J|u^DbK_mLLW<zbcJT*!BE1(h)#;l9I^9$V)H&#SIZrwSyQn^d22Ty}
zp(oXcI?}L6^K2BTq|f6+@?Y$z$eJn4TXs}ZZvA~Ia>*y&{V4P7{$8<63k%gm4FgAK
zJ1A_a=__1Qlh+IxBwQiI9g{`q<cajJ`xo9!2cWsCwxg6>q1YA~AHR{uIQ2X~Iy?<A
z*z~M`T9z$GI$duawcZh1G+*Flc(?kk4uR1YW6YQXLYHmTN!<PQmucsTx5&K4Vg`R@
zl4?o42D*xVzBIondgIT^5mde5ce*Mnj%$=!?fVs;#jW%b9tHj?-Az%icXQv*=+0?_
z*ivBIZ>>5$0kAnV{=E66ll7%{mq&*vMSo-^w>bTN1u=8DMy$(jCD<Hw>X(LGDv@!Y
zCM<k7b)n2vp|@5N?Qm>2iJEU$cSpj(D1XwEz(0A$JELo6FBDfAHS4kNri2E1q?4g8
z==o4zLIXLAQ?#<l*EG`mfaPygDgt}qe_hk|LP|cF-w!m$fL;yH7cNh?*~$<)hB1Wi
z(sFRR`+cYCCC{8}2$3~e&wiY?$Mc(;w-_U}aapv;rV@Cql!IECvCH4faYunBch!0v
z;YsfW4q;Yl!jkCwffr$Ka`<ACFb*~)=2-V438w4+%GQSC3x({eF?%VbIAba=ZAZL^
z35K?4ABat``&H0}-~KS8<ZXi}<sshQrq#h6M(`nuQJe`{68O!~#*#WPiawuYF##I!
zJNM<<HpM-o&$hF&I-;t{NMgTy*z6>1pKtZl?NiC!4VA83O{la;T4Xg?7*P!FHE`%F
z(P0xKQBR%K8a#cd6|E*FNaR<Qzpt=Y!cp<Fy{mL*;czW>b=VCj_aU8voNF0LbaXy%
zu$Jx7u9K7lKHT=J_J%&IiRFjuoYb>J1zA}Zw;G|$jw{0*eg;VOg&y2l8-~<w=QD`Z
z(qGmYC#hophUYkk`vM(cZ)C^ssmc1s)rjV((pN{o1!cd*O3^Va^>LdWq>A5q341#N
zE%FI^FIUQ+41Y+U0(ITKrf=tSU}Ja(U253lcOmu6pAL$v9=`~Qa4BpYtwOk|EO?=t
zI_`i0fi*GBcsVxe29}X;ce!p15QJg`*tf0@1*~iz$SIb&DP7527?VfdF+2-j#5b}w
z{P2Q%r)V*}?d3=2fG!QDSaVz&>L6~>6T;ZuAajqw`y_@P+r+_Xg#cJsrdp;|UyI?=
zwfDUuZ-9jYZr6%6Gv|ZlgVBS7Jk<eRNdVIopZwcFcuGOC$-%Kdnk6pSwjL2FSMyi-
z9K`5B;@uXvts#p;DA#ZnC@c@<O&5Hxygr{I?EK`MT(;I#Y5L=)9=c$#f1~K6i)4HE
zNhh{0dg7MZbmcQjr0h#ru4iO}m9(37o!@>eOwT&D^D0vga<^DYx{=%v`YX-}stE<^
z1tPpZmoH*N)h4eUR@*s|U(w;lMGwr6iIHE<143T?m#D=)R=>)iE2os{v*Pccb^1##
zbyT;;idfJ|`9ewR9MeHfVy8w6AuwXI>SP6o`V){J!e7vAZ#1-szZ&$F<rnh<y<!jM
z&c^gsd(OF~D73XkHfa853-9B6+f%IE_DZ^r+Hn-{Wj^@?LAQF!?E%5Dd63za_3rzS
zs-_h&RhuQk6i!~srtZKMq8cnaMR!ncE<>L0u1Kwnb2aSVGK7buwVMo&a$RyZ90R4%
z|D()~^Md~+EU3PwG<SDL%k?9sxpmiuSEe46^143^pXm*qmZf+{o}?{B#)K>KZ>64~
zawa7yqp}s<*NJ5Q+;2jA5qzvPxf+|)ZJ+*4dM8(wcwpO!Lbyz6e*(2?6Lr~(;TUE(
zzMGHab($IF`0nC1Kjt%Wva(GhfoVuiL*U@Nrx8Ue<DhllknQo;;=In8(K-4_ZIK~*
zdEqxAzCEg^Y$8u(*=*0gaLNZr?w-0pD0D(%r{PZ*SJx>|KVn3@wHI{uc2lG>e$u;v
z_BOh8Mv+G0Yul3W-Qn1@Rj<d6fl-QE9vUrg)na73bpT7hK(2VUb6Y#FStpq;8H?b^
zFQn(=Ouq=<^z8P}*wGN{-rKSb9C9Q(+VXyKrKH}FQ_N%(bRto%(j6fXFL3W{-;&R=
zqx;2R-BHh1m9cg-ylIA+`m^1OUxD7X@!B<0tI?b{|IG_n!(tpT-(Gx(7CUd{W6QN5
z5iE0HanN}oCfKCo3E(J>x-3yc1fg)h$w5?e)b{39|Nb5=EUUa-Tq@J(obbk3<Po{S
z(s=X1cm~RgDVFJSxs^)<dRB%+^)nG&c4L^XOwLT)S3sjJuu$A`Ip<IQe9v19Yq~)b
zG8ry@nu5>GIMCkIuf^j$YPhE)+CDdjIANwDwd8TX$6bq@YC85eE_@<S)s#j5c(vpB
zh!P`Hexa7v=89Fv)1rEjS!$5@>ilvED0ZYGRx8wWCW^{7yTg~V<;GSFh24aaW5q<z
zl9a^)An8O#fXg@)X5k_yyWLQ;7$yE_2Nz$X3A1Jp)MBAs^J7peN)wxNxVds0KfE(u
z&@g;~&b_D>+cBh&+NUkxnBh32<3iG4Qg^vOk$Uaii<_@orE_@^!=i@n6aTDU{RVfr
z2vB9@iG|a3g~uC+NXZ1%o3b~@?e>LkPhKR`f1ff+;78QwX*+YY-<c;M;kUP=qa#4S
zyoEKbT$QZtRJ+@6O@H-*JBJ8A>{_eW82Bv~e$G>MTFDxRC-dz(+63Crn|##^%uJz3
zlJlJ@vD8L)EOQ5;B=S55Zu=dT<ejBgvOPm=GP1jebRrT)D;1x&sLLcZd~CawoT@*p
zEB~~1!z;?<iaTpnVt<JvxHP%pWee|#L3^96*1uTu=iI(S&`LS#ej3|E_FZc@y;_wg
zzG|H`Bp|=V+62@?ml9?79e*M7xJoaHN&ywZCPPerYYj&2H1DQ1yb`M%Qsu9Qir?XM
zpsct1s=1{1-A~xv4zw?>;jt~>>sl|X%vw#Edp$V)v?r;*HX({)+Hm_B2Lcr{&dc-6
z%Ujm7b~mqL$Qp7xh#(3|*2uN`$<GCt%*+NXA7UJ$PnK)4C9v|_kuP~%m$+mX?LNIA
z{&Ape#6TF;?KP$Ga?CcIukDYf_15LCK7-T`7rTewEB$NRzr~aIRz{Gotam?~3%km=
zF33W+`Y$C`E<6H0{8F2Ln72+6yc>c{oi#G&xl7QE#2jd4f4lL&rooK`cpzOGTGB%8
zC(af~EFM$K1YLI6IGU_x>tAmW*naBkCz4h#Iq0-tB(i8C*{xL^)7IVGN4luJ-LBt;
zSk(sz>DZ%tlD!jf7;%G9!HUU@AgU5e7&LTWx_%86s$8Zb_YHkFsCjnO^;Mdg%yTms
z<#0TD4qwn3g55Cj-R%2@AD)0phgd~0gHl3sJ&hBzN5@>4?DrCf+#*TZ;C6z?z=+3z
z<h-J&SW}mnq9<9N3igl+W9|@>L<en`g8pR3_|y9&Yy;&}NhHqj8RBdu*!+>uegCr9
zpWzY8Z9+6e4C1$+sG0k_B8u7muH&=;yJcNYiid`AedcS<fK-C(7R0TIIy}k7+Z4<7
zQ@8lGL$f7-v<r_36G~ZfT=XFF1kl}~5^7vVTzwd-r#8!}dgZ>X{O{&YA_R2(=T!#~
zm!G~;2|&t6wn)tO3~gt!jL83dpgqWBsl=xw3Z)Cw@Xm`LMn1F5!OpIiVl2V6(gooL
zQnHsu--DAcUrL-^!DxQYLx0ZsXxpm_vY!Q|JBg8S<bEjm5FBefInRgBb>dFa`T2ne
z8AscVXWLb!gncr2y9W`?O~rVl>`sf@K3_rToN6!`5)cmnz*FaY6E9NT_*I)|II@R^
zKd~1Km`NN<VqJCy@{HY)x*ARl#(nJ^q3Xm%l9d&Css}SyHd?`bg8MgSGxCVkxb=l=
zN?d;_D8Yx&3=qn;ww7oNe*5%P%4tC8{%nl%lOWT|xxL@lHM{4stNLLvOqx5VE7IR(
zr@h+wZp<Mpac90W%oZ*3jQ$21LWlz^4NE`Y<LrNj2KWeEu^?A#VvU$wH^GW`O}<R3
zI5-JUmXJ3(r6&#&ny!wXTiLm;Ntp{ranDlmHj@nr&l4A2n0X2Eu}{)d-#rF$_|yc9
zuwAAXHEqx-V^cc(LC)-6sBPT*1^Q;1;xBDZfrqK`chb=0-QT#VlIeSckoNO>BdiNC
zxPkb*_Vmm)<`jV^_Q6>wtd>JRX?-4ee~TdzbQ&`}MQOX+3Mxm~J*%K?Wwn}Bx+~fJ
zE#KuSgWT4iB;z8+M}G+uDAe2geaanS+OtB$f8GSKd47`y{UgZTLDUqIo-<jWx1m@a
z-3x`d{gAJc%OMV^ugv|%5avXQO0?kKa?BgfJaMvE(W7j+s2bh)-TjCD|Ha%{wzbuD
zZMPJ6cPqt<yGzmH?heJ>2~Ke@Qd~-Lw*;301&X@`cZ$2qyRZBC6K}q<j}XGj&RTPx
zbByHTODoC2L+L5&OF{6x9sZhe3t_%@O}s<b@Z{XnO3lv-!@OD@Wzv@!o{^uXrsBhX
z#i60+U&E7qd`^_Pb=$Bdo6q`$exxpPwnuf~_Bo){@DVCHI0W)t7yvw{in)p<^5=<&
zr;J3g`K__b%|9~G?Aq>xH<7!X+tWH%Qy`@kfco}IN*PCH3xH3KC@Hy-xLz}yv^tWu
z2NSnfo7Xwg9Qs<*nfpl-3ryZ#0oY9Frd1!1o{QxhaqmHxou*aDc~nXh<~22yXQ<i@
z%skk<7(^&0MeHBm4JjG(C|t@~j8a_=x>(xRHRshH@$XjjziPrs+1na>4tSks1i*aK
z_qpGG<*m{Asa~sN&7cl4f6oz%Gsc&dW5@|#=z%`a<b;lsZ^f&Eb+f7CCJ`3Rww-)1
z|C=50*^cLn<2kk19*7^PyjINOW*qB0_;jGQFJgfjBjZ=c6<v^fyWyTpEb}1JA-nE>
zr6dv`UhOAeH*t9)>v<UD-hlY9J>;Y`9mVInFZu`)@S;%8_K8YTd0AfQiyDOT)2=OM
z4r4tt6*Sk`Y=4f+hfq*fyd=B?@Vx4Fp++hj$#^)B;Y>^EA-`F!Oq=g+e_Lys8ksP=
zBECAO8a!VbauJKz##c2T)kLZ2l@Xm8?Y<Xx^`9>0{`Ne5+d(+7o9M6XJ4@7-$-6Si
z+dS9Q&c>g>!B!4uh^&PKKhi+Wcix3lTVN}JSS?AFI=1e6kJR}^qbb}pRWKh79|FJn
zP~vWB{2bt@^W{h--0bxA2mlh2xqEH-f-!`^h$5iy#$}2tYZOw?KR*$|Zy?PB4TXHn
zy=*g*wdOON|1>^23;2y}!tE?QcTRO}t!CHo#cV%<@71MO8)z@#wxQKFVTOs{YSqsT
z5=^wvf70L$mq=y#c3Xt^W-L`=AL0$iOAoRYf~Nd4mddF}@JIXf-}AUx%YM-Wwu92W
zvL{!;73@gV++%)>PRm3S{R2`DDEM)-8%MQWy}w^!>r7w$SFzj?oCYaYbRxSWq!rRl
zhb}FNxNcKY2XINrV+mQOgZ#vv+4siYP)D?Deq(Bs6d~v4@VKYlB4pm1O@DRJO(*#_
z0A8aEyv7F7t9*)?k6aOG<Q0C9<D0M9?BT?>>>aXwTHDD~d1OLTu8)9Q`v7YS07p+P
zCwT$scu(d#o~G5pJT=Arn`ko?(=MDEVZxNa`_J?cb`Fb24@3U^dVwYew-LsR%WV~n
zYf<Zo>{tpBKaw*#KZEZ3V=fCuke@HZm<SaBR`0t@q2Fw_?FQV;-4Z}==Ej%zRmmsw
zX1@Drewi_T*lH}>VhkEoclJB5N*96?8i_V_2M;ejghT6?fauQiJPwQut(6Nb`yAuB
zU3;)|*8sg=-;ZCd$VX!`8u<vxaRXaWeH;G?G$I65b|Q~+y&r5B_-5Qvn&&%QV=csV
z9@xN>$?q7wChTo?Mlzjpf@Jk)r|2YML*%cj0iAh)PU7ufzaPd{DXYl0(NQt|IdgtD
zMEtl_nQc6^(QQ9s=xbHa{cC`lIVvAH_b<I<GZP!2ZFK%RiSb_=A&1myB!r9TY6OKl
z$VyoLhAceN_zjyaNQCh0tp(0G;Es>tWrJ^)<eIqRhBDMzEIYh=fl@rGqIuWtwb?Z`
zxRBoVw(Dpy->D<RkpX&`{x{+E1_e;0#BA*n^q<>}nENQDZxO9k@UJ;&yr<Uur%9VQ
zS8sqOIch;oHi;9%7sGGO?;;=(i7WUE)cO_XbM1^KZA;g(j~u<3YebfGV}ar8H3e!!
zC@Vi!%#vS?0A3DWh%=bAU_!%<v0TBHG>-*W%t;Pii0AXgz{4q-2<78EOW9zpUMMDE
z!j${YzoNfY8l#VkTom*9?d0@WW!en<M(^<g-+Dt6O5@55gxhC&-`0b-;)WD{9^%Bb
zaHTsJn2w95vH)PJ6B=R7$}kF=7lrkKj7t~Ej`=E1t5c!K5Uys^*+bX^4trd$@RdL3
zkNDVSim&m4_?~IQk03X@khKQ!wVf2pD5;Y`jS*MgKSa{=Ey&vttdCWHcpX=w(v5wv
zAr%Y18&$+YwU~4cZ*F}qcGEqGXyLI%&^mqZ2KG$?Z9B5j2H^tkr))Ge;<0#Z^uZ1V
zVIw=KE{wd__eMK=@uc_i(Q>%)+1x$DJe5<W&Xg~0tvIO8T|Dn+tQ4F7xw1!gUlnpt
zT2$dUqrs5f?yxIre2hjn=DoLHD6bxz*&WZ#()?zoZkXP&CQ}+5dSxEFmMB~`IHy&v
zFP#qzLmHS3RAn-5Rtm3=q_NJ`=7ul042+6RJ%=D(i`vgumG^(@<fOYjl^aVQys+Ct
z-*E1JBFGQ8fY)!f4V8%}-R`L|;@GbR9qLkYG<_ME73f0%gCwww6Sw5QBO<^TfFED6
zox6+wY<#U+t)oy8eMy}$bFB&9Pr_xkwt6O~KdAu}D0$m)79Y7nt6gpW7}H7Wn2J6$
z(gNuv4r5Dn-$?$_JZk>=?<s}tk`Ag2nSxf#1q;LEKq>mR#F72%xDNg#Wt%z|<dxaC
zuIS7WZg@0beU=qHduEl8*ZECkkgcc{(S0AS*Z#$z+&bt`#Epp)VJ7{ox0R`ULaNK4
zRg=p9MI94w*#1dh?fgW@71=rPshL9P?i5p@Zg)fP&p`jt9W$zkb1B>EZ%kBPi^&69
zAG_oHjPAg0gJY?4w3Uw=8AV^m>a+hzh4A<^>U_cqyMw0BZaIjrzPHg~<jYDOFBlB1
zcOC0SQ)13%Q;p>|NYssK<Fq5S&g<#ecjn(D-gaK}<B*69g@#i6w6b*jx5G@yEEG#D
z@3G>t@xaluZFOG^ful21L%#R)i_##*5umFp!0EY}d*J->OW=jv81ZzO6BP)-?0n_A
z{pn(_IeeQTg7VInirjMXSe?bBt%!||f4A?#{+8Z&|E5NrpMTXbShiMr>#x>R%G9Oa
zC|b#@x2sGUjpyru9#H9w)%kdSb-10u2}aiLoF<vzt_^;BsmITyujxtSDnPot>B(;&
z{}>&*J$qY!y>TiSjwA35t18fXMDK{hYmh_HK_53;a5ZrX!n8Se5{Wg4fgZyjnE04a
z*$gkeaH+*KRqsyEVx{O(h1mBHfgbSwh2mdmB`^VVRump@>1)7jyIKOd_>@VQ`nQp6
zTf4x#pIc;?K7n!}Xd*`-)~C-x(u{L=Kfj6&!ITq;PTm!+)8YEHm4co^mEhbz{`G*`
zf)U>;y{LsJNey$7skSRi*`@(|xx@(AOMyDFqk(RdW5@e8(Q7Pdle05(aqpAaY#HXX
zkGvAPg>m{8t-4>`Q-$9SU{snlb1eD3kXG#wdEH7Hq3$;Q)pw2)B^;*H=6VajZj$=B
zkLdN~HFFgH;w;pKW(YJ%Bypjfp+@8gqG-{M4lLDM$d(H468EhASyB|=QO=MWBA6ez
z)hl#ZYIt1vY86L%rV_!%Wjls-bL=_i*}3pj7Ye!-RyI)<x^0x~8<x)~&i!~e`Hv~1
z*(v|@D^C&&!#hfmC1QN}A{kj-i?M`lqWd1_d#5#-oDQNxFxIv<5N<Q;d4wf;yrA^4
z*n0oxkJZ{0n-8m#E#$Pr#lWv1r=SDJ*_mN;UK|<I_d5d$=URtcI5Bzdi*QN;|CW`<
zdu0Zg&A&}qKgNnXl_*)oVwFn!$RjASQWRcJy?7<=*Nl7-l9cmCKBG?qp!@nrYiB5?
z>=6C|su&kcUpeu74w5&&8K^ev0owt4z;3+yKnlobn25WcwnWfY@y+{;XFf+@YWlex
zMzr<29fW<_8(pP5RPAs(ZIy>V19JKuvnuTICJK2n?=rgf-~<zl6Rd-v-<0#K|MWES
zVY><uEUVD-HKknNmLxO3f7sbSg7Xv`6Y|9#v#K5Z&eJ~S@rqE@=E7oWa1yQ&ME=l_
zxnuquhP>G2PA^KtBkwmdO8Si~XzHcFN!u?yGqo?vyT1W3hZtIAwze1(r`R<(o~z)<
zPN)%2>$jGj*XdV`oi|^%P&P(#dnRYEdcSyyvBQlkG>;>?RA?OfI8X`R8O`)vQ$k0L
zk-S>-^Br;MQOt6Lmc)oV*Y3EFt+U@YPN>(Q4_+$~iBTkC5Jd^{*cU{$na~AtO`ZPf
zKaI#kNs#rsU9#r-(U5Pv2@G=EJCl)))*MiE>-a8#D7h+kQQ107tLo7Q!#R$V-3MAx
z4A%I}N4gBdJNK1jYS?fORFN==*u@j8M;Lq^&L+f;lu^92k!;PrY}Zf|V&UgP3vlo@
zBD)-qid%srU*Mx3m(!ZE<F*UYNa&qT{|jW8#C}>*1v-2(IC*rE4@Mjp?z5EHAjc)4
z2(PuS>95@0ry4!x4|_v!!geW@$W1gkGiiP&ztccTor7(D(uzL%bAc(*?p|nXC7$TG
z%ci?D=E)q1vXT6hV>#_xuezpgp{Z9;<q{#IXb!QM41Xlapj)OS_E>$?Z5UC$>;xZE
zmdtN<aH<~bNzeM+ua27mNVM3Up(n$RJ^omPd26-F8<YHZ{;Ma{-E+ww-)xy0CZqTr
zJlS89#ajJe#YMa>oPhc+q=oZOwmKL}O-PEL$d&HYlb!1vaWCj|8-eqDaiCTR=bt>g
zKidRzs4t@3$UPn|dl<$r^rTK1t@Jy3a=F}GrJ5_6FK~P!<TpnWmyYr^=_IAtGdH{+
z=J6nPz+;i72hiZ3w|!>Wd}<<5+&glNX0Z1LgAyAGBltu}65w*Gw;TTqE*MF$;rk5^
z|0Cp#9a}FWp)a{p{Pi83KtnR+OyCklttgxQjl@@+kkz=Vl;Fz+xctS<pRCc$ZPe(9
zfCX~@(EM|fh|h^EflT06--DuA1#t)kzi*P)*_vZjJ!ba|f#UI&@-fJ1thTVy^mYkg
z%m{%wls(2qBle6ps!Z9i(Fze)qC1t5{T9|p$WRB%+4%eAfWQmq+|nOwE}_-q_|9iG
zHz7_0p@cjI&}kA%|5c&+5SRkeNX5r)p{2jf`kkKC7gFWOWs6)I_+Y;pc<p;cPur4n
z)g}l~aInQ*JGJPAT6F|{;r3l(gWbI(c1(jPMcy(iH>&*}BP^VW3l{6~o2)V5iE(`w
zLuI%uk1R!;M#J(G2@fmR_iP;SF(KA0M?Z##RQJ)Jryk)wfnHF~@8vZzr<IO?;tm@<
z(ds+rsRGr=g*ure2dJMVYRp8x>~U%S-klCpj*{QI>iRP`8rCp@Q?!i^=d`Cquh*u&
zIvwLeVB<(F`}2u(LywB#H=nd2Eg@tY3t&f&=+?dtNYk5MLM@0Ee{z>xcVhM#X!qp5
zWc~bH-*NA;YUZPn^Y7QG@JPU6K*oG|L^iCMdG*xb_x>r$prS#ZOqU>7s-+E7JjEMv
zN-?idA4_s8Rqz6-N)En@w_8pc&nV^$Iev?6x&uv?g!+R>yZ6a-t>fnli{4AaG!`9#
zl%QuAp}XUGWRq9GX=4Kg+s5dcep!0U1I-_D!+PT+*yad%*L0b5z1$jz$V@IheVZg5
zya*HWqEXQ>17NI>@Xkpw9hX;sD4Opce~Ld1*MEsa!PW?B24exQaeQRQ10#s%3j@Jw
zbZ321CjJnf8(R5S`kq0`pqx$aSO18+!?`1q=EnAD;Na`J<8a7tw8jP*A(RYRv8}}p
zCn_ajFHM7<hO~{o0`Y?2yiUv@;|Q1732dt-W+R%!nPTx%2gW-$MML3%F~e9&!Xolo
z-@Qw8Es2@b>}GYL(EC<Yp#9O|eY1DMz4>!x{<)Dkt^2{(N2<+H_W+ywBwEfa!(;c-
z^9rYC@F`RBY^PkIVA1p1Kg;cUMT<PzQ<7W-8c@XcocD$T++12ibL=PU%zn#&D_h7@
zg0J{2Mjw?NG+0f(OS(#9bGj#P;|+JL_i6Sm!6CMaE`W)$(ublen@<MMpZxp^tK-k*
zl<_yvuHWfjZ5YKyK{7j&7_TU+b&%M2*s@qtJqBu+Fea!5>Gqna{dse`is_k}WnnyD
z?FmPm<`m>=H<Or>d@JB&!POb`W3VX;@n#)p@DRkt47aM{qf_y_??SS{fvWbd#evy!
z9+|QH2pIb)*BW;zguLYKG4z2;aU(!GGM^2xDIZ<Bd!6sYwO3qTols(T>j&-^r9ej<
zS5dpW))OD{jHqw8dbN;ES~*UI;H{$+*Rya7^)uBJ8%CoF5Bcx`b}B3LMuhNXzHRyz
z#p8^-KzE75Aly5omYslU)WzqJy+@u+epv&HR1Bt`l=-Wy`QZ$tUEmb{=*zJ=g38tv
zoFDW8PulFDOq?<p(ayE>u!5eLWRVzwPFh0xpm^9*M2m+%9_76jn-jH!eT(D2*p_Yt
z6P{EpR{|RQC+&d<&Zf`zSC7;by!<dVvs2dBPkg}B#M6~L6WcO8V~y^c?wIuBVaDgQ
zLc2O<voG>UF$GN|000<Y>+;Q9O;Y4YbpS*Atd!0N<z#B+J%&a14;5DiCG2ii3XT?P
zKBKy=LP|o>)@_b`^1PkSNmKRr7E2h%BCbQaDe}SjY->b|2$3b7<-<skmS=2zQ(b0I
z5L4^h0^KtJFIMmOdAkde{1IrO)_O+IXn8#`g6$ZWxm<V0ed@P$lzN8{#BDTTE6ZQw
zbUnc|?Vne>a3-hJ^rx+0q*AQxX6P)_nQv=3JSqXO*}UV@Ir)4OVxiV_caW-nx|ztn
zdsaP=m%QnXN&JFHg83w<^kCa#6}Fx*U_tqsWJNhIe<Q2WNVk5}BK=#7h3G)DFay_@
zYpk8=XB4#L))N|it`w6#P_a3l`gF@rg!2!~m!uk*sxD#YF*q%#vG-6%6kvF3qt9D9
z=Duq!y{KE*3Pt1+(C4&ukK(1k%_;e59`Yeq;IdQ{|3a#;_g@JszmEWpnWBL+qSa&7
zcd;+1vi9eETsPg+F+10?#qa02z8c3z^$pByVuaJcn=-~?eMxvIymy~Xt2Si#TXEjG
z3}pZHeq-DBR+=Z}=w53ZjDlKe(=eQ4>oFJlh7_9F&G%S^UyiK&!9HRiKW<-0PDJQ(
z716$)F5tQQ@yP_1@Xdew{-m@<h$?M}LEHA;fyQg8$&)FIr`06U)zAE|Dqwk!bo$;H
zB0Vwm3<@g#H`QG`6)?bbXG*fK+ot$0RnfUh_whQA=xCu@VTK*KlOjgP*q4}ezEqj#
zyVht*@bOB$hKm0&2GuT_Z&hI|2jMnb_oe)B=U@BPc3G7Za?TgyHQ1-SlfK_Durol!
zE58+=BB4It|69b(VWIa#9>3Klqa{)HgC^d5^5k>&xwi}2G!RqqkP)X4lVm;_1AQT2
zGd^a|E)+Ri1F$h%xvSIIL2ob3V^~TtxubEgqezN&5^Moh1MZdxbfyjc85|aIIyK>5
zW-4LX)&XNEXg9m5SU~A?;ak%B(9^J*ENlu=Yh%jqltuSr|EzuQ1k<3@PsuvoquHkO
zL+(_z%lu2B@6TN-p+H>hHoqfoJ?-p^4&UTn_AEpE+l?JU5be(Mi~li!*j%6yWSd|6
zL{W_8&q<kX$J@60A5!8GlcNQR=rLWbE%o8(*adar;O397>uUkO`LsHn-vtRBu(BB3
z%6yLayY>u2j1duoN*sg2fE0yNhJ=J6C6!tpJd6i_QTmHhIt^_}LA>Nxr0w7R`u_FW
zEw9ovjglSg#DWS<>yoY0X~3z#zDT{>KYb6V=TkzSTxA*PK_>1wU@M1UXZnmXV~|f$
zXS$D`ZFr5^8oazR>LQf$X140YQ%W8<ZMUu^A&)m&nU<{*`?^@ZDjE=U)%$~C$@{p9
zgwQ63{k1M2#m>$I>nLg0LO0LJu6EaJGwGcU#HI`?;Qgn=k#H7neaZ9Kt%?~Mpr*8v
zgCFPDK$!DE&VZu}YTo%RkHzMP@5`I~m(J<U3Lcj^s;5<)$=!>gtR^P>)qqHgAA>Pr
zh+nh99vhna0##8tRN1)`D^#QFoeH68Nuua6P_jFS=|234?B{M%$#VY?_3E;dh1*&J
z!^Et%6sUUpSK}f3!Y3M(-k&@?Oe*$fvmFzm=?QYfjh|ND=T_i*9EkS?u`u%|rZPK_
zs1$X|QU58p@VJbP|9aglRJ>`>J)8rHEOznv4)^eQXvTKagbf4IEFR-aKTU4a&>lLA
zoEXu;oYP()-QHK1G(0>Z#RGTEM4%5G>ov}_sca$MRP(K;LV3=94F5PywbMFH(?P({
zT=)!-%|ET5$uUCh-+>8CYxu5wiYZpx6y}DticSKmi{%Ps)SH#bAN~ue*ssFw4#u-l
z$@eU@Hm5x@SH-_vagU(431z@)l}{J%erBm7V!zXy8Bi+vyz1)~wCv@k0y&(n4?t{n
zduV`i)O*~n2xLC=d)#Co&EGZKGM!NRe-R}S(njchZR2*DRIRb6!eo!6)W>$?(uDp;
z#hM#ui%DCw$lK~A@mK&us1qkDfmo(pfIB^0b4ba}+;obHXnFn>^Jr2N0II$r_5rY4
ztJm(Cfc4b?82@P2F$tL4dv?F?v|D5kV3qUk_0jNH@3%IPeL88h?vKHLY+YVnE41b!
z;~yu*p`nfadQ9tiupWy;njUD1)$O6hgqLCt@3;W3TW6k4kj6>Lz(|Ds(c-BoebRLB
zHdC$T>+sf%+n)Iz$LoPr%kCMj+lJ3!7gL)@1f1o{!m{6Wj$sE$aT7knZ}U8&=h0#Y
zfzsyRREq^izlgtC_58hN%->^KuZii>4Xr7w^d{5ZKVvr1MRhIaJa#tI2wwm}XU$t&
z&s=X_M-SJH^gpG7MZ9jj_JAc#6tY?4kKvj4{N!Kd-*yYmI$zSt`096k8MG@-fR71N
z{D{cSQbPB$2@%HTO<)iq!9un;9%$V*G0<v(C&OWR_MK?TIBafB4};$qG_|MW&lwj!
zlqIq-VH*EfB?#B1s}W=Ar+#0Y4pa*uZ1lC<EUb6yKTHftL`vc#|AYa^(c6uhGN_IH
z3W@2;hqWMWeXXcE;-7&pPcd0&s}zb!rUF4^6+upd*t8saqZg5|Xz{~4b8Nd#i<nNn
z&I6#IaacOPOj}kuGHFP?@{-t3W^!7OgQfb{QZL~l3ir=oTnW8GPIz$lh?d^d9SC{)
z8+3F`mL|yMC(VkYcC&(rZDP66er14%N*Y9z4X^7c1NOAIi_P-Wad57pd+_hy&(g7k
z3O>dYf#cC7x%$VdY_IE>#4+zTJPZ70GiR)4wPDW+KKE7a7rNo?sWvhn#Rq3&<zE>k
zuXKiDrX>nEJrAnacG)JoopAMstpgq3FHCSrI#1aaPW+)syOWO`uERUvGmI{_ComSj
z=Ov!r^=dCzTL|2tl*xM|Mrm?@M8*%G;TizknqoYC1H8ri6MWPVAYXjA&)N49<aX5)
z38c87<ZpX5X;iYaWonWTgsjs1bGR@PgGrueU_fJmtrbz<mLqVQAndEboYyMMx(b+t
z&8Thm^m7Ct7ChW{a*kw{Q1G$&iO2HYl{A9Gt$uI%1&V1nqL1%*T5Be}cwC%mus*rP
z`@Gbo-k5yCNH^;8rq}J5q!mOCUK0AZ5gmKE5HdUe7*%I#tIcI0J(@yTQ~CbWL2JX&
z<UcgTgGrw2OV4X#`J}RpSyy`m$ZD0o3~E^UvbOL*N#~%Z4R;16)a-$Ny-K^<<{$!E
zuIG7U#4XuF<eomA&v!g>?(5QBa+jL^<s?8ibfP_v;GDwFkmqE!B=1VY!G|dkj$aY}
zTzuERf_Au}E`ruSy2&2KVG?ujD|EoU!tP_m2lG>Q0~6I1#5wdS2`*3{G4bOFho@Mq
zCpB!9&Z6yGSpc!)KJQTt$q0tw7^nEV;e)#}Bi#rvLwRq;g;V!hmgkXCaCf5J?vu|L
zV`b@uy-NA_WPA@k|C_<WY4;u~;@f3pko8h`!;wdvZ@w&R{}9;;B<^WDwWIMlEY8sC
ziTk*DTdyf%3%6$$Zo+QjW|VJUWri=$)8;UA6Y73@zxVSxBS^K)AZMPUJ`g<3{7F>r
zk@=Cn@5PsupF!bmyzQ7<|4aVNnq)HwkwqDiGpB7r?86A)*-XYI(dw2IOUIx(y(}g|
zM!D8;@1_bfBQtzG7jG?rx4|Dix1M|zai`TD#-Hr1@gn|QUDl)~KO4vZ>!Q~kkG=J*
z;a}t*_A<X75{fXCpUqcJy-ySxLg};}M)?owdHC04xv4^+el(8qzG|T3Yd?bmW*TY#
zuvW$2(*VD@qIDmZy#;&{l27jY6B<|f#I@3IgO)xkXM{nu-*O%rUBU`gPAvAt6A~X9
z+28D}T4Il!r#n8wcbND`h&|sIhK1XAUzAJ`oxQyxQhbj$;8hSMy|Od&4Pk+L7Y?s#
zajp>38xCaf`!`o;)z+-{34KU4Crf4_LXG@IcWe5~(x3o;{mME&Ks#Q~EpoNpTj6Z2
zldD<`!2`8_u`EZk*~v%?N^wQ%ixRVIih^gr-Zf<uo_>CJx=@=x_MZ8Ed>QrRH0*oS
zH(}kuDp#+yUW2)Qdt%H8i(`Y=j(^LNn#sbT*BF|N;WrDhyDn(ceuC6S`!9HEg@e4;
zf4w~Fh0Bq|%GHl5NuEN92hH-4F43TMr!4(3=6<@B)Rg_23fOFHsJ*k%zVeL-*Uxrp
z{V^QKLbr@x0-AmIt-*JFb}6t6embu<hQxc=B8RE>(AR*&JL|qv3;la}wwkbg29pl)
z2$r!^YHQ2&sm4S9N<sYMYw`z9LrU`W4?(Ab6-^vO+Y#b6fg-dhyIKX<pT|kWOvHG+
z&X?6zXO!J#g{w{cj;^8;66OQDHAW=2zc95yev)~6A<U7m+Vz+>5m3xiPT>R@st>Tm
z(7p`UiHm9BP*^}>WZ=qBLNW@`f;`~(;^c%VJvA_lqkf23o6mk-X`)!rP54L-QHZEy
zL_PgP_A6lOn#m!mHIwFy`Gs%f0%X(g*XBiKtHr@7NhpK2VZ-I95<!hSzt}zB2A-QQ
z5Gq~&`(uB2Yj=5ParN53@_CR^d3DfZyXq`GFt*_#bZ7f^w>TqK!-z~Q&@dbUj6_VT
z{H)vBkk=`=sj_sVGVn{#r`W>SiD7PdPvb|P3)p2ZCY00p#!Rk9x%;*~{a3)71~^F8
zyM_^cm@wJqWKc+$LA=D_S$5A+sHUdR@q+E~>utHlXJ1WEe?*jO6n<i%Z;MHgCft~H
zLJLF!vD>DBj*P+W4c@)@M3_ViFxrEC0oCsTUB(X9W8qB*mEnfx=2>ue2nVR<0rFM1
z=7o@KYo&!u*aSxzy8RP>NME(zo+F*PSu0_EYQg-K`TR9*g5s{<A{Zo)w|sA4M!yQh
z$Sn+F62=fU>2O{de{Mc|c}BHeZScK0rL9G$s2b1S?DAplP#$>_>dQUS^Kw}4GO}A}
z5^bhHDKHHb@S9=VxRUY~@>^w5lf&-UtO{r*3s1g1P`A!om_Q4P1zqixBM*^>Pmpfc
zIzV=a*41PIo`rEdi^o?(0=gU7AF9?}eD^{dFU!74(y;_TfT0M5dh>a|kNk%K6;{K&
zhWIF(cGNn0BUNMjt**;Vr2e)`4IW=w^>~QoD3e|qOi`g9f#!I)${4$T#ux%q-ia_N
zKj*!2$LT&)0`6uS=qA6nDqAs|Qn$W8ZijH5&rmu9Vvb@VAPUbbt_5B6fa~DWA*K&I
zQA-VOu_yC<lBINWJF$Fu-~Bgx8qV)>FMBiKZD$BfS6?5iON%UK?9?k}{-Rwi!svqo
z^Y4=t4LUv}1l)FmUlvL5V}^D5+P>Ue_G?x`TR`0-QJD&JpeQ8NWHS-y-|viK#qm9T
zY3(NaC6{XJx6rd6wQ-q^@@rX5G<{vJk#yc8*99uW&ZKW?PG_S-=W9@&vMOWykSJ9i
z(8CP1b>%YgGx%(yaI8|Hb*tQn8PZJWS@~0%)!VysA_gSVcP9L5#jiKqvFk-0*LgTo
zS9Wc$Zg|Q_aX6V{V?(@j>6|*c>h)RlVH@n<a#-Zwa%n=``00k*>4q`0{-UZ%z+cJp
zq47?7;7J<mc%dlc+F4)m6XB3xN6%*Lg<g}?YO%=t_cYp~liUZ+crnQ%GwscIH!(bW
zP@A?HBiZRQMln4wp5%_~U`X=(4*^i96vr9V8Bmb7N)S}t(|Zb&ViekTSIdk*&%=dw
zq9+!Hp1MzeAePpoJ!B`87+F7){uJ5T_J)cII-+~EVZ+toeoLt)XMDyo+sCK(6Jj24
zbZNLPgh!GcaC8Xg`)%LnzUkGHSU(dI6#tZMq7bpG9KrYd3f?Xs{?yqXFr5$iVB|Qs
zUPCg(P_L{LyQ3ky1N$K<7a-k+yJm?xzZMJn2Y=}PVW%mz#mcx(-hGSD!^~`qTK|A9
z@5-7_?%vZr0>fAWYs9!NFpe6?$^{PIXly{tr{9xpY^Lr2+=+9RgfM-2iy?*$U!#FB
z-Z3j5SjEegt^mIm^>HQuyh>uQI6RBKkgE#8UPW>I%tT)!8y%=)dU*jbPb*uY4kzI4
z_F&ffw4?Mw)kTCmK8)$F@U`0~&Ff9X`XZ~Ia|Whg-p@;J<B@%*mqf+tl{57J2E8R|
zMn)eld>_Z+hmr5f(6Px;na1|1R%G>~q)#7APwPn2(16HbBbsyF+aH1Gsld>JTO-<H
z`OJ@p9ltsgx^Hv8mo55uP7=BVPcBZMinwWOB^q|V<fA>+7su?RaC#TrR){n0I%nUO
zhVzwqTFZgz%<y@vCWHNIp}y?Z4ZH4`b0h2#80>RBLsvSGpP8=8B6vf<QV7&IDy8*j
zUAKd|2^C<{P-)Z|c4OICV-+UZ`0xdluPm9rL5UG`)RkPpE~~xz2^~pd>SD-Fw3dk@
z+D(L2Zft1pFJX&EmpBLlLMDDv)#JIXR3*?rAgY^)MTxMkF`(@Jh6QafFY53PvgJXB
zK;+{-gZ$@oTb8l+<}0;68h&gLXeJprn`0x-vyBPO!6^@KUeHfXZGw6aCc$C~6gbqo
zu>8@QVJq&dZ1?3+<~<c=;$wHt!yd-kQiNgAGw^0Lj7bv1H2lsm*RS4Q*d}_z+XX$b
zZmKOT(fQC|_1XbaFV%Kjg1DE)k#ots*pJ0_`YCv5rV1ufM+*p~eQKYS5D0qp*YZb%
zXA|@~E*sz|_%THQcKF$E2h7MnCXZsZVVyUA6OXOQSmXfj{!OB`3*^;QlCsy*7K&`4
zuvj$)?c(E)9{(5_wEwfc<@uaGIriU#R>kkg&WT=pepPk=rrf@XoNo^_-z|6kCfPaU
zPz!C?L`$I3=-&a1W(z(;&x6U&huMS4f=KI-v#uv){QDm7MwNWgPjSnB0WA~%ji}J6
zHutY_6Evr!Q#dih3i2rk6}<liav=5f2iKVBL6XsDRXT-uw6~Y4de-|J3`$t2dO|Me
zvQ^|a{fYN;B+IfcuJS*52yFR!`0Fzfebc%4_D@>(oaG^4P^{r<l94CweXm3)4X1M%
zG(BpnclpOreZ#h@@7))0-l|qxR9JWs1+%p0kR#$W<~y^OKZYOxY$7ig>y%u|W|(r*
zo>+eMKrOM`1tpHuA`=LZ<2@5%HMq_HTjxwkM)ZL^Yca2RDtLnLq==xbzZpK!s{o7u
zgS_1^Pu|JeJ}xz~dmt4p|0Q`b?5kWI_V~3}tkz<yjE%U!vp?0VoAsutts!r%W+(M*
zvA`Y{NzmJVmcuKb#hn~k{%;~aMd1XjEUoAuys7)$vyloC>VfIjE1_bFkpjfgwMM~5
z;d@t9)bk$=@ZXA^%Gu{2*RyjzCtPvr;fCPaG<;&hEgVrb&EgJ)CsJ*;EU0<=okQHS
z=Vin#xm+5q&ep6Z_Q0alHnxjcnAP*iWWd~~0M{t5_D9Q0{p#eHb>ui(=4>(5!yn_7
zPT5AMr*7Y<&6%2Wk`woiRgbD@A#g@?kWzIc`U|$MN_{#m)>7kqs3p#b3(=sg-?|26
zC5t!V#fqu#W#C{UdAc@>eo?^Pd9Kq+QMAXw;%5{BW(Gh}p+PkvoVT}>C$bygyf(`Q
zifEtbQj89E3MGQIH10lMJZQV0R$wcC_jQyu!l}ZBv6p6;fz!M;`-A@IG2}606H>^u
z8-|Q~O*?P4k6`(l&F5UR_u7CqjFbzE=Er_wN<58{30Lt0O5t?>O)RnrVAL)d88M8V
z^Z5e>?44}quBr5|JMx6`ux%9>B-~%Rzmis1Y@H<x4YCa0yzjD*2_E#uu3e&tyO8uY
zxWc1Jh>D3(FF?<)e+YgbgFqFG4zyt*`Xop1M6bkN?<K=k6)i$OSflEbUWhd8dmTvF
zI=0)fNM$&uYyCYCbgLOPa0=pldlw`|eD32;d|^n+!<GZ1^>IDpu`w>Y8){xV)Wj)$
zHF6luCMU<z(X$BZuB?fbDU-xHT5PEpHA)yd(kKepw!<k6;7cp;`*mw<g?{vrE$}*b
zWwJdv!oJ6CDNIzoS^jV`CM2IZZGxai$xj5y;Oq`0ZfvtI{%F6T^mZ`;KAJS0sg#t|
zvgP7uNMzRQR>>pb5MqhjYP45{fC-RPO5$7lN|zvx<e3k9xVHJ#5Z2TdI%nOVJ5!i(
zo0nXevpTi?*_jJment%L!oM?VX-fVQJ0b#8+;Ccu^IrseHT`tM@jfh*t5QD|<W?CM
z0zbT~=2$ErG{oD~H;qNQO@G!e>m{@4zeJFCbu1;*qO0yuxpW(ZoZoJq(OV9dGk9XX
zi=&JZr0s)j5V5{3yUUGrS_3TrxaT`ch9gy1BJcBAgtO6NqKC^;*alzwJruXY>a6$r
z5m=>cgv<-dU9Vl!<;u4Iv}>z&++E%Wwso30i6~BNL_I>4cds+Kgx9iCR&UUnvjsgX
zP|R&W^@ybE9+7_{@M-rig!Mkx^}vmc1BlKa2(%mI;iq8f7(BIn*k0O!wxwm;=CDt|
zu*kS(esQBP{>HmiFGNfVW?Xu!ykIdQ7>vaK;3N;g{BZSYi$U)?#rC#*Dnmir%a%x?
zC&o-9(Y99Ow}oiXg6g|rVxRa7tTp2Wo!{Q?5H;Ru5N<r><B#S?|Kjygxn0448OH^v
zqI88_&n%Al{W!OJq$@@;T||#zD_U)JUEKKJ#<rb<a_3j4u+m*L#`djF?3b&oKzi&I
z?RYVYjzf}!{hy2mY!Z{&g2K9uHvJ3SZq*Nk9oG{MsdOO0vqyc-bDz!QLYLd#*1-gA
zzu>QiLmr&V$}!l@e2VFTS*v)Mw8D0aR&)5TcS&3oq=le>ic<LQT8OG{{Z}fjsN&_a
z)&`Kt<7d%0O9kc6_(!uNNOksfcPY>xGwnC}s4h0>0o5aK#ab7CYQzTmRy8>MEOg%N
zjj=aI&%G7qZ%3!vzTti&jY-?Uw7c$;PS$xUfWBevy$9#2fW!1aCLXAtO-b7yO&)B|
zEN^nzH&))XqBLhDOyA#4ji{8x<Y@o%?xWdzAtCf^A$9fw`G$Jms#hHgd#KGj2o=7#
z7;6VCeC{-Y;}a<(-c&LG(Fik(+i5c}C@~SHkrZDkgiPijH+me2D=be^Nq#-c6M0OI
ztDE{a*WP>`Sg(>M6n^?0SRbpXQ(A15xfVJbJOyFxOjlIq*>S32a(yDmLWqt~u$mEp
z5&Ci-noDp1UKk6U;<FL@60%G1pf7;QpY!3oQZA+cX){!zRXIx}W&L5a@P=UB-MjCx
z)p8_FQKLY&@7;cu$=eFsiAjE56PrD<v?^WKWt4}lhQgB}*J-w?XwwXVdzabqlAj4n
zI_3Py&~<KqrwOPdv*W7w1qUyqO?iVCpTbWkAy=+;GT!LJ3jU_;;6kn4k#)oDserLo
z?Vs1A3w_sidi_-ZR2rzC`(`&p0NM<7hI6O2j<BBSAWLyA)H>VTo1+z_QiC7_wz)X!
z$NnrH;29noMh^J1&V4(nA1pbtTxF&r&%`2JqLB0q$`yO}BiqL)n7RSJx-oGKns2Kg
zc?tjRtNy-xMdkKFP;UCa?D4V<=O0Z(V)E{7D~!9~r`^<?m5oiryGw{qD?#pudROQ;
zPT)_-uI^a?1dEXDfGtU1w{K|J8765)Rn=l)+{kU~p2M*(=Yv^+nfkwRdAmG*ykCDA
zPjx3IJL{~*vMTt)+BYB{T!de^y#fsl{!KQ_6v|@qoDONIvS6<=Up%x9&%lfaJH_n%
zBWWPrcvqcDM9B}p;RB+d3fgJ(HOP_W=Z}}0RpP7N!Nn758N5IYOzn~OiD<(xbiSe?
zF)le<dmiTb6WCr?`t|PfgyQB{8c?^8pSar_5-a6Qw3I%0@DQi}H^;HPbDly_o&LH_
zXc?np8MB+tlk!*|+6~DgPoH<34(`5+C}&g1{Zu?FDzj~jt9g_eFTzrJUSsUvlH^|z
zu6ge?hXMLmc(#+=U$h#$KBV&Ko!phkJD*<EK(O)Q#xLWwb(1U3<fR~RNJxNWLw!N~
zm*MdU%RXl~UmVD<LfZ^~jdfrp)v?rR5yQFd>67%Hjn9ZJQW-pTXp#R#hdRVxv|OJx
zUvoxTYh*9wQ}M@Qha~nP!hv&{(@<A46;F_SgpE*0pKRRs&$XSOFd@$Qg_~nW><LKK
z)C?wP7)W@9vWeQbJnbZF?^w(kq{VEfX-WqbE#X-t1}v=UJ9cT}^s+fDWSZV*(+U%y
z`2WoQIDprN`+7^%98pebi0&LboO!T!Fm6tb<V-{<o2Nw+lmV`=56Ei)s1-xkR4V*r
zYwTXfN3_{H=e9>zs|0M{bv~tyK0htLyh4ff!Dx4L6-9Kz80g5ULM(0061pzInvB_k
zm?ur$D#KGV>73!sGIJvp(=P+%^G38>9h57tPdx22d%gB*mA_J%jKAbIYd$7?;h`H>
z;}8Cx6IX7duENNu=WW}q#r|8Zrg>9$FXV3b3Q=zEW5K7moX)%tPZQR8G&5*gv?ey+
zL7WaBKf>x`O-_ez>>iStE}9Ky`~q12<*o(^nt7ur5@d9%GE^vGA;(?{(pdh0f8i@o
zQ?vVM>)kPs6GkAXi6Z^ZlQ4{5lU&+02^h_8Ek0r^vG~0>9F9uZR_ux5j$EVHC=-E3
zoXk5uhEFsY`6=f=g2CFi&v{`JMDU`_Oj<G#@I!y|PBeeJ&&IGITK0T8k{-p%WT&Vy
z{xPZ{@iW_Mx1r=89QS>7h2b8?Tt7B$8ed0yoW9k(I;s!VXVB~w%Tk?6&EeG$G1e>{
z+5wGwq*$&O$+eU_a8&aVKUgUyZSc5)@uawFgOq!xm#;G8=e2$Q!KvP)3lo66)W@2d
zT4@xkl<Z}4Jnr$QRPO-^RA|XKM7l$tUoFPs-V9|Ep6v9({<m6ZQ$mTRVv&yfapLNa
zZ-kSl#Xy`|I5k=L{@^&zx9n1sW>bTByD$J_1{RV7^B^&@bw4*$a1#>{q}f`t2%QV6
zUR(lL9^fAAdwX1-?@k$qE_J5@R%~sQ#yEqA7stxtV4cjIAVR_aRMcki%dNPy_adH@
zZ}#pumaR*5-BT7w>?{1|7>LQ`k6q?kFlxamrkj1WT{VV?h4P%e*=eN`3UaGTi<7xt
zSv6e09vBD&33$d3D1?BXcOp4Niam{vSUL~d_$^(1)o8$f(fvCz!IdU@0fU5Z^#*io
z#-%SxGu>yQ?cikIn=?%#Cb6t``T0}9UnVIi76*aP=K}ce(U&oElkd8XsR(N={d2v4
zeENusDDq2Uy&QF6XUTit`anBN*6&3GI`?DHU|zGCqwgy5(Jk}&#@qKI#iPaX(0-ux
zM}B9~xF3^jyDBHJJiQ+EzX@O7h`y+|(d~ZR8PlfKT6!DYKh|k(EH>{k1(iD5GG>eI
zx2w2?%I<*j3@85MsOVd&Mo_YtMq*NGUzM3<N2f7^V}AaI+q>>1YC8tU@uSwlR@)%4
zXZVm^&6(*bD#kquW6?~agvCT=@S)LbPImm*I16!g8CYLkWrzG3&lS?z{qp?6VFX9_
z$UAUdaKxf-c+u=UT@?La^Td;7z?*}91G4cH%^qt`b1%$l+)-B3w@#9;wA$u5XWw^+
zhtF-)f0B80?gvPmb1yIN&vrWq_I}U7GWaknAPFb()>2)q|0`jma_)a8+YG13G<6y)
zlBvHTFM}{lS}k9WytaP7Yg2NBo<VtDEF)d~vI_s2g528LLwzY+V=a|8c*E@XYa8b)
za6Y|!t-|MJQg3^|y?$1yA=l;C92tfnwB8`!8e0f6&J|e|wE~ChXOQjjGz;;`Y}*9;
zx9(Prog<ul+D~cn-g{E%B?XYn*rtYyOe(zTU5VUY%bGGisS$I-e-YV4|D1ben*|pd
zqIbKJS6ER@#;`(#lx0VHP%}p+-=8j2sEdM%<y?`G{~Cg#qoSf1=nJ;C2VaDSC!MM}
z@p)VcBuwMw`z;TT66J1C)<i%&0*NpQZk_aY1Zi!weRFW92@F`tvUy$2Q5{nsEf@^|
z;_k0EoVGry-%6kxwQ`NtqMds6wRH+S48{Oi(SNJuNz5-T>6)x5^=&k(6hs>g3#jK;
zNv}$=3YGFnI0w!`-@_3f<zi!!IE$o_K_yPu$jBQD*}w~PBh+*}p07#pf7sKw46H>g
z0*zf(pcX;um4l0kymiaJQvJL8f}1INjr>p)B+E$j?;a1%!`=)V_Y#F@pCIKsO(fgE
z(3xz_P*zaQP$nrd4%r4ZPfUc|wi~v3qiz`<${2Nl{`V1<6DP83r5%vrM3iviQKmHw
zE$zrIDTmwEF}_~oRg}Z(TP>N0pInXXV03qWEs&^CsYqj@d-HaNpM$1}Ie&?0c=51z
z1xF#nk|iO!{<psW@0o~`_W$pD5O{-t$M6l&2JlA#j}ACkvj6^l*h%T8!PNiz;Ux~5
zi`4%f#@}EP{&&@11@_<ny9yhGjKKfDt3I;zyp#RkRXeo*zdTtBnG0aM$>ewa1-6-z
zcz%AS;p9}O@Y#UT1KTA10mM|zIMMO(KL}0y)pA`#+v5gFdF)c>Mx^r6l9NAX^ExI2
zti#X4KbT6p-n1$-lNOBqlmLok$@*b;dQJ{AKsx*maFx`Ix;*|RuK?kg!yBm3s^3|s
z+s1DgJwR&H78i8ldhI3EnpC*V`D=Ly;5pAUAvY=C-nQ3?{B8pSGo=A?g4=Dt(C9Fb
zvAHQOPfkwW8RJ{mJlgorJXzeDSijo0|25){%lF!*+^D^&txL5);u4@M`~}9OYMO?=
zQo`m$0&Xwm|0qDj0&YbB{8oO=?@WZ4+vfX92S|g$@0f#F(4$OBR`#fwuFdm9#~45Y
z0-}`EfIlvY)u^rPOw8-Gt}ikeY&AymGXGW$cr^Y32}ou>rxn$7SXkKNfT!ETo2{^$
zX0<|@!?3G?O8|i>CnXiuhB;Lox6JwRXgHlMze%Z1SmgD7`;hnb_4UxiemISV31T;w
z-2M8fV3O=~wKs-CM#lVfdy)d|gbo!nq@|^Y0ErOw-HLnS&Pck7$szge**aUI=wsPv
zj)2DDLT#;RCBUN_iN>Wb(kNBtbXwt;Q&gOFR+?x34O#?joXkRA$KQb+z%Y=Jpar}&
zX#jd>2%uY)bhYq1{%7*xnX29fz6&n_9;3oSjnQ0^G+D@>J4)Ge?3GWN0917%UMK)y
zklI;%TZqN=0?ND970Ue1%#`e=!7HwV<d2;V5$~c3OYYPEX1f^3d?kM$Rj<UJ?}CLT
zq9o{X@L@vqsVeYj^uOW@0C>~v6o?Cr3cdo&E~$AUe)53zNh^a($bB!aO$9J9X`-ez
z|K>dhY9wm=>CAe+ZjsMHW|T11&Wi))>+jCu9V~*c*txOSJr|7Z1}8=r7IaSA&qO_E
zErA#QIIU?T_zJYt=j0QA{Ktkn9B~41zZXMW9Fl6}Ql0=tSdZ5_4Z3;lxvWON9KE-S
zpo|AJM`b0B;9pp+TaPzKZkxUE5LZ+Hu99Bz36NSG-nt%4QhAb!^P&1v@;NMiaPGWG
zdsHr5{;&Q+Lq{jW;=I}h(WiIW!4OKZ0{DTohgs3ASuN<N%cUxmkQGzm{|t<C7Mr~M
zmyw>X|E7y8q{pCwn+zTMKnYS+gw<GVUS(u|(7vyGz;YX4CNIvvY+UY+970H%d#uBr
zHeUy}%>s-71LC8@)XBg-nwy5V)}`{e)5y8epK<yoh5N3pudVT%Z8zOl<3fji_Eqci
zO`rr}3R4kcQGXxk**pE`DcoQqVMh9bev(zvO~CE;18}dEUCqP6!F>VtFuMTc_3+95
zw_s!%-?e$Uc6HX{Yc}i}z}%dB^xn_)t{BmCgvG_d*_qQZ(R%XU8-#JVs$+ThFR^1;
z<qSwdG>k~%9g$D%TaDb{jS-ukYy_A!O5%WGfp53zI$>RqcoPS(41wRv@?R)UkkfXl
zv{|>_yuoc(o|xa|XBW-f+v}Ye#QpZX53{l`TUVf7@(%b^9W;Mgy^tj2<}TQ_VNiB;
zbv=xA?#dMbwqV+C`n=19nG~ev4vGQ({@2A)<BULu;c?0YP~qr*;X0bDOt*{XdJE16
zI&1%xk|C~{B7RMFoqgj(34hIUe<%F~ENx}1iTvh~)ZbkIzw}(E{eRnNYnP=4OS=_v
z31VKGmd?%p>e79Wf2kA8oJbb=&0%8;GyX%u`2*f9yH$_><l;rfBVgN^Ss1L2ZorPU
z=xE8k#??!CdN)Ao{I!kpwSS-8dYtSZ(9YIh#~wBge6aKkf)%wto;A1L00d7OW;moE
z|M>xH=^Yg7X3KkwLZ;OE_$<Ks2Bm^D#XkbfzS+lbPV+>OG3)3fj(OCHk>XM7(Vj>1
z<*d2)<N)zE8XDc-CYg$R?iYW?g~8bctUq;t{`W$%To-7z?0UH)7a(U>hJuNyW5it_
z&>KkHbrl>OK5}0@__+U<7wa?jvQSlD+A2pqWfW{RKNhTwtt{%tZ>9AtpEIP=RXu`6
z6Mp{uFb9j6j~St}(V4L+Vg>?qVK=m2(SduZnI#A~f6T@NQ>QPwN-;>@aRLRX0`ZXd
zoB(Fy-&$w8U_*4_sV=Bqtry@s2cLdg%qYM|mtA)4l|i2X?gPy2LU9Pk{CoiNG5oXp
z6dw513|1d-B?TZST}{$c_Monco*PM+N{II)@+SP&3;lkv5sF!1I@jcA;Q0qrY>Od`
z6F3_}F2s+Pzf(!hHUJ6A%BM|^dZ#!_F=L}dnDrL)c|JHI_rIU{rI30D(2uZqb%FOz
zOP07^ttSNCf!iqL2D6s@h(!W-?F|6go=`xzyRBC+g?fBefr6UKa)t$UTn*i#ZU2q#
z`h^~^&EyA3_PzvW3fOAUJz;*oEM-8EmoK(qwBhb(6Pi$Le!y*}Ad-zk;ki)`;D$Ry
zv2r90zad=~UbzLLYig@;Q@v;jq7(xYru%q^LN4cs-LSfZlf#j@DzZ(nQcEG)79Pld
zV7`R$`-_pQwRza$Ut<}*TScpnmt2tdsISs2Pe##|Im045mppO@5O%Y-!&8nQ=8c32
zZ?Di6-}(YS7&G|}nr3qX1U$&;#zmYXvVQ;6&)}eoN5k+4n5ujqm4kv%Px4iIom%7E
zO;e_&!FT&+x|=TCa7ydCFm==s@hSj2X7}y&>`il5XrwUnp3_2hI+$F6EaY>Fu>MjV
zS5&S9p2~0eC^ivgBp*ObH&nN6jhR*9>VZQl>~Q~1YhdyRypisC2k^%Phn&%2eA#G^
zF5E^^yno&9fTRgHNX5ad-}BMXNCBYgLSDfLKp9y9i!~qvcZ8?^F0O_K!Q(;VPGRI!
zgTHq_dfxKbGz+nE`=LHbT1&xi^o;m??L0eV*c_>SF{$5ivcErgJq<W49HpyoeqkOU
zHHWo#K&pogj^`R&RrBI+Wa;a9@1Wx{iEzhdn*Hrv)ERYKEHU$f^9o{mJ#}=hf-EJQ
z^IAuRJuurBR5LGhtZ32M`&pIj1IK#$U);L4Fl|2YyTX>l!ewHzbE$L*57Ix*qJiDm
z!{<-&X<XMfpZVm&1Rd^NHsGpl`0tStoM12bk(3s_x!cst-!NHR4xHj@(tXfvR{PpQ
z)0{0Rag9FJueVr^5VZQ<RJ7Gr*5!Sq*}9k#4{;ht%0;TY59o1UvfKHoiRL2kE+Ex)
z7M4`3isoQD4*0|f#ubw6v3=71;KwB5g;4L>iwW0tzK5F!ODZIrXudHBGqxndC$Guf
zYf)&Kuu4BfUZ)WrczvH|5s4<Ge?S-2b!$`H8-mQ`|Hh(MgUg?IGmN93RX5c~#77qJ
zAN6x?;BP~Bcd4%5NkW~s*m2m_O`$q*Ol3xaA8NvHk~*%~b(juk;Jy7kKqTQ&OEt_Y
z+ib}{8Ff|xaTU|GYE`y^jJaQ5sH!3vvDIHi=9-ah$|?wXi3%Vj2%{UJPTi1KD*zj#
zF&9-<CS61qQ|u=b{_hEEw2Q_c!7!iHwwfuH`Ht7AJPLNvm*E?$nnHXi$ZY;R+I#%J
z>fSo4%B_7DmXvmZv~<^^L6DH{hJ~bnh;+AvfOJWR(umR}-Q6uIAR#3PlG5<ah5NVP
zcYJ@HbN)DE9EM};F*dCAJkOl-ne)D{>$>kNx62b*E;1d77cZ+DXD51HbOS`6**I(s
zxp`Qb4{+FW5+4{oJxKFdL`ypp5wD}2CtkimvJkZX-c+xLJ7$LbNaR`ctL4EcTCOBU
zVZ!Kg0Yq8V@o&Kd#E%4o8GJd1png(&0hBhN==%{>bW64uy<2G|Ce(9Qjbw*V0<%F;
z1n$BJZ6cbslyw)4n!X?TPVzB@&GC3T7IZf{I*Q&Yp|QqnF8~s1H@=k}U>tVfxRn|}
zuIkeJib&xQKi2{shbGHCta|!+t{R#|f}_Ogb5k_T1~nQB>N=B5EXfB$(61>R*fS<%
zWTmpp1DQ`-8%9!XsrEvW1gk!hEHYM%hE5A2b<i}B>+cz@2JY50p5j*Qy*Pv5NaS2H
zlM->_Jh*S)_XJhofrKJbG`n$eINxl&#4}DZliFW0mJgyEjJx?S6wE`o@(G3y1rP)5
zSOl!zQ6l90YRfYJB|!4F6iNWq(;8+pE9Kq9AUo)NAnNw5{7bMlLZC!ss^hVkj9oB0
ztA%psxCWUaUuUe^V1?~0WYrxd=n<zKj^Xz)tHZCVPv^+I<X*n}GS7Xy-R3@3=}KOX
zbsxRr%LbD<<%6>OLKOm)?@En{ABWIhGIdC-*`X=OjN?Um)l6&rLJQ<8mh~X3dcWco
zMZtz@@NSpE_bV>yL1VOQ=bSB71I3Rvk%kT0kJMN>FdTn8$l5+x&lstB78-G(U@pOU
z94-X+o@4p=x2|=+X}4dGsMsnW<=J96Pz2I)2>@Dw&EaF`VMJ)|+7?HY=wOmz?yq(a
z0UM<X=V0G4R2$&~+5lSO5tr1CclRWy8>&pq=krzM6Ud2og0V58NAxMqd)x_3UzPj!
z*;SB{WlPQ&Mdr4vOXgbB>y<2wH=G)EiWAz{j6aDkH9_?b29dk9-VCvKPZ<z5TUm0}
z&YGAG9~38$gD_1E8B-`arF^Mr4{$LshH=DYrMzddln<cj7PkDO$wAt0p1J3TRR%1X
zIFI99Y0n3oAhoV+Oc(dZGZrD?_>~8b1uwlO^Jtspbe?%(nnu=>nKOc*x7G%Rh~i=A
zO9wF|0tVcq$h~+!pAKL(Y^Jy5Kw+4`b=<yy@(pXAv`IF%jo6nJf`=;{H7yaSRV77Z
ztjAg!bQ(=TYq2gVM!!S6ER3eSHJgfP;|)+p1tB<$cI04e)`^OC9qE{`sP17vw!%43
zoKyUy!{GMj8b;3%`Pk{z4+|_SX#&Ma63%2-MkfM(2=V8z1MXQS6Qz4Cm-#k90c5j~
zEYgqloFT0HNbj&|r{4m%!@^R{t=IktYoMEXi6jAiMSRG-i8(|lr-onUmcT~IQKE=c
zkHBFKZ_%uLgmk~rZ`296_gd*8UNI~O{v>qJA~YO?poXQb{{rXjd!1VK0W!@u%U+?b
zcrdmK=Wuc{=RpZ=G9O`^R>(AGGb&5Xi%o)gDHmHw1pFfvmRO;z6AWp^{Y42yzSkMX
z&E>+`a<bYYRFGm$oITKnU(4{(fe*hVCW!=jjU&^ZnM;|A*`rX0z=|T^WIU`@g>&B~
zl6_pKx7<()lYd&I_^r+>>&Ea7GaaIecj2}opQhVW+_frsai(d8Oi2EiR19}Cgh@yW
ztO(mhT&pRuzQ8F@zy)DbwkpqtYo`KsexRAR<M?Fs4p}N~7oJ<RqY)W%do)+emH<Y0
zt{9rbi1*KfTt9Lv7xngVo08xBv0otJoVQqrxP_A|M&76Dv3RmqV~8O;j*Y+C+{uNa
z(Za!2UrHrV80Lx3)r#L?s%`kWyuDoq!9}tCdMsVwar@3C2%si=uw|hY2;FC}CRHAr
z@+dBcGvzjPq3PL_c_D741OgC<&VDlf`~d2N<|%OIcS2FpGwUlj7~3mZ%T^V<e2Th*
z-r(@)>qFzsF^iv6PDE8XD}I#0*;#(*)9BWWL*rrduTaFXN-SpWQhPs=M83tZqqxuC
zmS{^A+;3cm)qy`vc_d0ZVhSdWblyCWOIUQoR6oFVoG_4A__5xv3Y-^(xMaoMzk|=S
zurnTBA`8q0WV}T4637<7K;-&kHs@rQsp{h5g7IF!u_GB%7Hv$)ib6oXR@6mA+yuRt
z_Y8shRO~S#@{1~O;zd$UnZO;AdL%u{a)ho}4I57TXa6EPfJf#nr&9ZXRO1hINZ?OG
z{jojk6UTHaF3nX%ld>7#`8wiWNRj})AT>b6=8=#VY_SX(#?!HV5;liFeD3KJ;30e$
z>V%3@nuUqeQ~%=XK~G!uJZVEn_J@x=1lIug>1*U_TkIJ-LHLSlYz}djc8tVQ-wO=S
zAojXPeESu&<c9@e-G81&exv|^pzl$p%b~)drl@e_3gAtSA^>ZQ_Wz41HFW<!0U-UK
zLR&PNen$rf<D<B-LOS}N>oRcbiDr0cMt{7PS@Vo8*0lL&6b|vUk;vYCg4BmJyM}==
z@^XNW5gd=*+-gXmZgWJTJH$F_=st<;;{jjdNdf<oL94~eC#X1|ySk)&&~fV#cVa3;
zbyvYRQ~bLS=JZF|%&h!7`vH&uqDe+{bo2{8sWq%(A)SzNcHPoQ@Rtj3|D6=Com4(i
zV6)6D5D###{{H^BgakT}oH_yU3?A8~343ze26fqn_B}B*!$d=i!J}7@1HKGklI7IY
zV$ROb3lj0*4;lUMSISQF=!>&U`gNkS(rP!&&guZiGxggSsh;#}81e}tO-?Jq+I7pz
z#^BDt|L*233k0{6)(al=2>zfMJUgl9HLpkz(+)VIKV?xe0A!|q)lr&G!MxsSg*!U!
z?DDec;fVhCU~om}e}~5TmB^Q-5^89he?`EG(T}Y#PZHcY^XDrL+>`~pH|wuu5=8zT
zBf-iH-FWIYf6wk81F~!2!FWf25CaMZzm%s;AO2@|0|XyRt~OjxG;L8+l&WePCvw_>
zmSTPv@CTK5%i63ugT%%yn>X|Et)f(oE-L?(b$uipB<+J~N2Cxl4&Dr*4h~Ay@(dck
z3gmZ2q5i?&(|U)Kmi9rGBLQ{Kl9iP}?<Nt}C#Vymra&@PSBxTf2dO@LDQ7O@CrP5C
zMAN%3vqt)N`bc&N;LxkOTS4Hf@9n%Bhhmsyxb-+YF<gu|oPB?P1two|rVL1UwiQE}
zgv!}TOy%KddT1r9CZ+r@3sNYmRDy_3GV%Aj8#UyvZo`eebX>f<s(ZO!UZBtisl9gB
zyU#52Tm1tB=PdMY6AREdQI=}LA7G9BKkNAa9zvYCwY7yGwVVK!C|Eo>(D;cw#hu5F
zxoa=w>%Ar8IG)FMMP1T6cG}5*J1wK56rp7%Dh&grQELYeGbM0W_5a`;G&V-AS!%(q
ztd^Ktf@r!K-_-pH_c^{#J8&IM`IJ37I~xxIq4M?p=B|HvvwAmGYvF0=4q@_fb(f(F
zS4*$u4Qijoxn0pRKg&60{j#CSS#~y6Om(ANs3;Z^y!uSq%eLkFqDScHB`^f9wEW5d
zoncS}+3gwt^)^PXp}~Jq^o9N(2<+=J#a%e(26%*}^4PW(X3mM3WoH`?l7$WR<n}`;
zRv%L27_ImaLiOn2_MLE5wa*`FYU)E!Vlx<uOFKCtx=8vLZSPALCR~Or;1QN3);%_t
zJN!9#p>cY^YNQqL-ADV-#ez;0!SgupyUFHA`b4FfV!hLPcnckY#KPIZkM%k8OoFA`
ztRTnEU~G7&L%@{v^~rexSr;8hCok}v@c)tm_3^TZ28VO3c$YmyPR<9f<?AVOIb5G+
z_Hp(f$(f>4<e_BHoFC`aFM&4p?3hIClN@F)x7VkGz*K}4?m`%sb_9Bz_b<1<JyHBQ
zkibm)=IS6Mfk|D$NFw^h-$mx`<i(?U80L(3(gu+~dtHCBPl(6V(v2BvzaF>xtgP~k
zuZRKEl_d$2lPY9{Jc_uLJee|U{RHc-)s{Yhx`&)e1zK1_k5udjrkLbY4n9b^`xt4A
znA;2wlmH7FW`KH+bC|jPn&pC{$sB(<j67$1OoJD3U2mGT;vU?viUu87f9{(^qzj^v
z#*hgub2~)GnS7B;*pp@%Bz7{8FCZ`KDjtWE9`~{2KAqz*h5R>PIoB!t-R#4vQYf|N
z{ZRV?nu~NNM(+{O3;fGB81DPW_UuoukK?yhQoqh3Zu<W7+5k^EA_b_8ZV4o8Q%(*)
zEFw8!r$UeFtIGJdpE8kXNk3kq;Xu&$i87nAzcEXB9|c9IN?8Q=DvP>H<0acCrub&&
zRUe<%*2zk2HpT8VYO;f3Euc(gIygAEh|>a4hPt$p+5)oBrC=1o$o2oMl7)xMadGtY
z3@U4{;^>U(So(WM<R@0*<07XkJ3rH}uSv@bzJY0!N-HWD1SGX+91A_^xxIOU_vFZx
zk0>qgW3KtSOuA?R0edz1kG30f`n9)2RlI9uOEm7UPZ!-`#vSHSqZC4Cz$ZBlZbg5%
zerb=$gp_s(_E7K>D^yS4);+4v>`3WN%gi|P8MXeG5`po?4(-k9RNNTzb$9JC`9;Uj
zD&q|mSDCVAiHE3(INlSl{58uZwN`$2iJkcd*PS&0|7&H6Z1_nc!~-nvnh=nH|9QYl
z!!Ek@3D>YkH`R2%MUuXePZ+K!&-V`>iHV?JZ@V_GU|;Fa;I&ptPkBZrYA&)I=s8sY
z>uSElHhueMdhiCshfw;?=IsaPFah*E{?TK*eDro_yzmlZmcX5vvZDRpc4#$d4IVg6
zvZ{pc?WRV}uVLx?sd(_`TbG%$_W#<U=W;trXEHE)f%y0U_W<zUnD%eG%Oo6qvTZF+
zBCH;3;m!P<BCo!@|Ho5Kg5g*ZnzBmcVlKt1>vn#5ISBl>K7y`y+uV9)V?h(#4kmlG
zy%b(bu6b4TEG!DJ!Yi<itR)-y#$I2ZDgxh8t0_2A3@}?6WwhtO!Hm!T(ak5|wNY~+
z3b&ei9ztD2SSPesCkeC@^omJrvFAsdgMj0n+-Y|PjGY`<rFqsfr7S6{73t}NIGMeG
zdx9m7fC9e(V1u;=jM*&Pp}@DpCm7fX#miE7O6@i5uqQWupvfl}_D~~a)1iV>VUi&@
zc~SCR_&6S;V6Te<NTOEZvodzjEZoV`PPFEq`8Q*2qqh^HQ->P4{x-1xHd6BDixu9X
z?$d)J4H+W~%0dkpk!+M4TJvLiJ>rP~quJHiZ>DuRk<v#Um>hkPBs|B8rIOb3V9(E!
zk7w9E1Q2JQ@AXA0B3Zz9;3k)6)atA4e#nJz_Bqp+C4z`U8>&&WeE!6B`(p>O#0WsQ
z>44&JvP>CH0t~eHTpla_K_bH_y8`<u9N{JKwh`}x(*?&s2hrg2lLin(7kq!H14rKT
zDFzKIf8aNpdmpMcS?6d<K&%ew8TUMbOzey98?@fuG+va>zwO{Y{q9xfvbUfM)+qxD
z2L4!?Bi8?Ho;!w2@p1GAmwbiqKMEBj)smD6l6~L5QWxo!KO6_54K464DolL3zM%gd
zqMdV}!b1$@f!7K)M1zw=Kq92Me4R=7JJ4w`fsr0QJb*Moc*j@IHa>tuepfPr=99L^
zdg8Oml(yoIM^qlv__R!`uHD_;KWAoxOH;s7NjA^$lnJT>zs(Y&M_hko-$V_{*OKEI
zRkvp#cZ*jz8Oh*qeaFeUQ}i>d!nd{DpxQz+y5lb`tNJ}RDUVU0!67RBCwk5Fo~P=r
zf=EO1r@PxjgUwiaGUJKm=A#)-8$%=^9oWlmw^v8+T!2;`<>RlmBN{PMLo9NC!fG&K
z@=Gl~Q)j~ZV<^W3pEKe>A?OqQ4mRpQ<un2Hpr?`DU)HbAk5~e8q^Dw+U!g<4wqF1F
zfD!4z1`kYfZUc4@_SR73zqv}aCiwVP?<I_t-H$LDo^*Vn+kT9|)hwE{_C{q#ky}x(
zmeF-jkRzDu6>y=`G95MW-w(cbW+Zk7JgGTgwQ|<&N5?<E7V=?14d(ne&%xxI0h4xZ
z$&K~FWZ2^gz$!i=^DAjT%9<*3|NY$xiyS^Y>MG8HVC@RVJUM;;=uDf5A^WQlZmSo|
zmSQC&lWJVGl`+iP?I&ijVpvr2hgO?O4#5XM3f0i)iZr!<l&Kj`O#MV)P)f<d2&EF%
zd3&G*ybHA#(Z*MU>>`*NOg2WgqdY|MPeBKxwCzvlz?mn1um%XhV75RX!cv^Na-Ajg
z1MtK^oej%Z6TdkANeicKtO{6l%CmfxWjutdN^NJ$#f{=H2Gt`s!0NAj^j*yb#gOjc
zoH`3!Yh+u8PnaUSjw<2X#fQ7;_6*-au)DUqHu!Uv#7;x4i5hBD?R0^6$yu{2b7>M+
z_0!(gUeCxP!p-NZH{uv`MAD<9j@j!o;Hnf7SzyOrFN0}_C4;eIK|k;<MY52ww>k7R
z0f^xdN*Ths{Py43;F7oYAL;JjV?oJ#f0O5tRxF;oM9KwNuv%GUfMqR24hvufw5tY;
z3zx1$JXtc3iD#i#dyp#NVS_KbQH#ON&87X6v=i{DVd2f*8n{pCZCu?q0%k`mpZCpk
zu;3DbaiLB+>Algf<tCD_13YLV({aG)191;P=oFqnri7ty$q>2pj`qFpqBz0+^?S)x
zNBO=Zt}Tg*@>^Gr=~W}%gGY|zSc=*5kEc60CBKL(rnepR#eJZULn&IU7X(QuZjZjT
zE^GeO@H-nL`wORXTb^R_u_B4dfFGRfgbiS!qmL(l>Nv{r>rY_T<Tl)7(bAB@B$D`G
zmL-0oq!l0)q!1`<t+p~D{EUo6?8U%6Yc<Z7fPtULk&aLr4PuD_=VYN3LRS@P5@dPI
zY@!_OXIP2+*d7K-xPA_&z!J#(;&S;qr-NR?VEBD%Yv$R9-;0gU{^k$~JO(%pPTXE4
z4_dl0<hHteSB*wr7b!E0JK~*mG09&!<`(+>%uf&)RT_wl;RLQTQyQivL0E<u>K@38
z_i<6vg0v~BT;>A86M)g>42N0pX0Ny9hUa&Kpz@=OowB0J>m>gHEJJ47W(Iujc5^c^
z%xu2Xr8lisD4k8#N&AAi^iaST`+S<-@avrARq`|z8_cYpeymis54hB#Bw_2{=$TJ5
z?k}?A;zRA<?%bTV-u_I6Cmo?q!Gj3w=fOM9t>;N>Y}9D>!@tLqXOq1za->d48Rc1P
zcNEj!hP5oB6QJXAtBrLKBIC*qZt1@*K(rxX7f<m3rjoGJq2jE>0*mpS3;G!<CXs`#
zhZpqo`weOdUDF-{k&;NrEJ|ax=%(mgIKy}ycpD><#GM{>z?(|erzPLm5}ScUcD8c~
z{q#%VOxPu$<u$<DQ-*2CKTeJh{ft(eQuY9vJuUG>@AU5t$L&BJ!b)LBN6_bL60fl0
zgm)ipD^|uNuvOR(#RN+49HX#1n9?h_Aa$kiyBvIhks%`>na+ThC6B|ALa*2c_5|dG
zG2krPO8zBz=45u2t_wUzVHJe3z`S()mHW71qejHY6>y5uMp6&Mqn}LSX*(fK4h|dU
zqQEhH%tepTz)K<M)U|z$O`IIMAADdxy)h)2tXf65YSc#D3=||q<q?|_gUmO+DWfCb
zkCt8kZtHY2NNKwnF8t?fnDNUM93P`Szf9N;YYapYhxytS(+xe3ZH8>LISV4LV7Fs4
zA|oqW%!#kWfFM90TDgKXlp7q+m*2HzQLFUy=~KSv>_THfW8*CEJw%a}tRuy&SkIP4
zOi1r3X@75UwT9uNn2?~TLN0y{KMWZa1wlj52EXmMd^5CHH8(2${{Ds15NHHiddd=Z
zyqD>ou-8%7N(=Ebonjp>0>ry)(<yEcVT6%R?dc|QCQU5fReuEDSUt&~fRntMJcFpX
z+1ghM9chUdcr)jL4cZ+`cmut;h&d%x{#iV}S7%CwM+W{$l=>x4|L6r3JO25T1TjeW
z#L{2Ch$>B-mLMPl;fh|hRbGP$P1RgOI!On9&qa1QdV|gjQUt9cDX}jn4dXDp5*v#t
zNq707>SThd>8Vmfeb*H!zup~5HAeU`G$3DkzfNLWU$^+6pOT$g&ja(<1cukag@#bo
zJqV9gQrDY<2scr$70GJ+i_5%>(O@8R(S&(d@v&nIoy}Ir-V<k4cFTU0Q&Jkr1bP9P
zM2m&?+tqtDnR}HOy+X5}Zh!`Gg?y;qj`cfzSxRL&3N!{K44|iVS?f!9wU`bji^==m
z%WP>JXh0HVd#o8RGfL!42sG<4AaD@pl)@x0G|D+-(%UT-;fJ4Ev5fU(%igSl(*tV{
z3E9MH8x-4UacY_j-zCjH{-oFu*iCaVS4Q<ypT%qOz6mfmWoeqjkU+yoDZ)ymUaZ!n
zBqaPqG4mcC_b#uZ1u~odN-g3f93D5W2IY|-Op&ao3iEDC0F8Swqs}xPV60vCTm)u-
z93)?3zf`KXIA%uh<Eq$a56x&q>t461@%sR)sltAsWhpDv%~7N;gsoQRa(`m|Dt@<8
z>m$J2w|+Og5S*V(seN}Tr&e~qy4m!8h61LIqbZTxLf5UOyoynhGJ<OL*l0a!w-l$L
zUrBh#7GbF8_c4|qD#WtdALVebHD0Bt%})~x@(ek<fso`n&Kg3@b>Bc6vM}Z|u1%zX
zkA@JaDciG7U6uWTb|Xd~RDWK9&8`rWHN1<;X2sJ+glL(EH#}$dAqh!A&v14n6T^v&
ztNKuOi02k$cAt#+!=2FW*a!p!>z3M*mzn9)pO){Tlg=3;dQ~|VW11?2cllhEbiKRQ
zrsLnKP{kRSFdfSpxj@;Zj>%G71CFJ((FmP%V<6dZwk&<@rKZvP`egeeZ;12$9594E
z1`crRwq2pPDp5vXP){?3Ju5HBezmVnqlryth0`~XlX7AQKrK)tAyNc#$ZE(h@v*pw
zWW`T=Yrnr%RKBn+V_o|)Dr)^jku_i*D0(KZT!=@5OXEO(mWeUa;{*A=p8**o-<5%*
zv{Gx#rS92m&y=nPh`w^@qM^rS%}~T=ZBjbquS4mdsH}=!Xuk;YfigedeW&OYY(FQ|
z#n|!saaVEHI=0!)xZ$YYJqb*;&noF!x?ZxZyiYI<xp$-9cJAl{$;PL`Fs2*-@}T`y
zIo5RAF~pMv=h1#}XvX|J-`CA<Gy&;Vi5rhJvBYC3=NJjKN?6Gu5Ze_FGKiWsy*awx
z_Z{=W>y{EdUwfF!YYA4EcKbu&2MxM<Z<D$~)LPivdz7+SCYc=%<j{QpX)$Z_L)i8K
z#cG32KPTv~3ICq?qq6fPGl%k4-I7%=jb(|2wsaHQRY8cBlFT62QM^vlAe6IeI<{|z
zH(Q%~hWtuooVQC^ht84KTycPJetkau({93jP#M<V$)k$3i{>&T8Rn@^fT3Q694~CT
z$1R&xecw5PI)Ms*wIylsc0VQMv>4<dELvG85U4lWgPN@BLQo!WHDT$Z*yM4L!&2w}
zup_K`-%_l6uGT)&S6=H`H&7+*Bo}uIa1Mz-1MMSDywP|q&<$$s(uX~F_L@(h^O5lH
zS+}`k#k<WHu7w`F!6xNZ`--jw!ayI26yJN@FZaDu&=x%GL0Xp#ddd|gJ#n5z@(zaE
zwM3E$PHfK#gO&>Rl3?A-h@T8NZ;ljfwtG9J%+8+)Wv|lrNUYkww61hg1cutrO1^kd
zT<soQuo=<bZ63KGx`-)vh>~)@788npZbO&V>x0K_>34=KsP>CmP#TZ0c!I(-a$Dl3
ziYRV2-V=9_s6SIRS|y9*@&byYT~sRbtV0^N4crMbYzIF`a9g0@@WOE7y-l_%m7EVW
zdcWDI$$ogfVm%W?%A6TW<$<wgnTd6rlKhw>gI<AdC)M7-7?lj;#b0=FO@-Y(J5cy^
z3h$P5{aEoyq>*B!p$?7mm2%ka6=T>>!I52%+><UsZ-%Y6lbeVbA}N6Mk>zRyqYB^T
zDwQu96<y0;>db*~FT^fG<n{4b^2@-}z|!QDa-l}GFg!z=lBf_#KfOAK?Drfa<4HXF
z`|roo6Qq^l$NyGqOi8nDl@rRlU3$EP?=Wu18Fc_S8n(XF?{F`ocROQ~ckI+cNIg4O
z*e@>y4$9Z`OSl4@6?{ELf{#QYV|q0<1$uZTL3}PmF31fEpg^*(020y*x8sJH$yTv_
z)A;gAIaSo>buc31h{Q}USTyp$i_=Dwh~ZhUun!2Kr;-I8l;o$}$8jF<v|9K(KO%G1
z(PL+i3mE)ItHdYNlH(s-u^ucHS1w{Cr_iB4wTs!>?2#Xk-gziGABeSDbj|UuCZ2+f
zNpyvJHJ-!38$&wWz5;R8-JPWWNQwvjytwRaX884cBzyk@<6N024FqJ<Ju-AB3s=eP
z2TG#0MK4=(OI};Zcb-)iXnrnqwE3gAxBrdP<S{5ESm!K?37yXsVrF@jvxT8IL1sxJ
zrPew7*kxFXl_ETG+4bAom{9pv^=3Siq37ki%O}~h7=^K`p8MYUkh^{8*r;!sSU_@O
ziIMK&=PAAA<<sp-Rh<~**4s2>7kj8^Hi^4i{HRgv*+B%|`I2*7z+RGtzoLS;p@(bp
zw$jhvjH15ZRJ_G9CMB4}5ms|i_|eBD^mWS5WP&5sMp;`>s`o!#@5Dbf5TY#DNOw*K
znO;!`@g6Bs$O~Gg>v!60_&r;oeefqPhdP`eOK{n25jARU@5fc(*qzQ&>3?9IKOMSC
zRwoi54WQG>6hW=Nt~!oD(#W|E0ba!v)7^Rd)+vs>5t5L?=p;_cM;(v8cLE!1Ghhb1
zea_YI3MvF%+XX7Q1WDDu!GBYro*V3I-<y31&*^~Q8a>OH$(~5$HBSR84OULq8<1d4
zdfCjBrnAJX$}RJBF5$n6a)@wrJR?(0f0D;yu=)g9C{QgDx9bNj8HlfV2mLItd;f#5
zWfS)gC+gF0XV+FW?@v+%TP1jYn@L{A73r##aI97uxBHN@o3EHd_<T$u1oAc}Fdjju
zP9QrE9cq^neov?s%~j`>?R_20fd1sN2>Wrwi-2G4X+|@pf^!lU_w}42St8L`(4Hri
zo4(WL(gF?}Zy51!+IA%SeXg1zPsTI?3E9#jwP~8(0eP`b$;&Xo$+1dvG2+gIdr1)C
z{S+A@1#j8?i-Ri<JKDCDb9LLViCLz>S>XpZ0@PY7g8SD}kPh^ch_kfFnI9~|4=5gu
zzCDdTLhC&HwSVTJ>cK9nw{2tcKVY${-&gt}BW5lR)wqPgPmru2j@wUDU9GyV2aR76
z&Imd38$@#+eSFuN`O#vgh+GeuPb~VY(K`$ef$XbBLM%<YF1djq>p1V5O1jKO7G`q}
z`H3*hIPbM$!eu-<k6)O0`xaj&d#Trfj<|5`sb0zuq>|I_7u<I^Ifn5sg9#u_zBX!`
zZU7#?2vJE+h~URawGba>q?4m#lk*NhtfpJ}?4CNQKRqV#IY3{-%KCYSGT$Z{!(~$7
z(2<9~eJTy!Rr?X%*!eV;{n<Pyc%&@k!#>Ip<VMADc&e0j{b%0(GwO?nkr9$b2F<Ta
zmA{BZ|NHdzen?R7MT2I;9Lh&f=4U0U0lO;1_CPz=xVn^YWpz5B59vvVD}JoL^WxtZ
zPnMIFAnPf}K0^)2q>qGk>O6^LrXBtF+up1m93=`vt>tI89pgUR+Q#0S>3uMl+cWEG
zL*;qu!wmS1rOFjsxTqAvAIJy#L5h$E@DXm1R?fBfG)@_#qOIhooB^b6wn;gjLD@$8
zGu<6>v(SGHbztZ>=NKH$<X5;dkD#|)I*1HXFj)UV>kE2pwfUygEoY+^BCeNMf$j%|
zRX0FGeM}Fh2y!KeS(I~DY4s-5v|fU={tyJh10YIR2LR4R36F$HGyvt~^Ar+~>DQS5
z2UtI}@Z-rkle^kfq}y8*jB^s1uBQ6DdwfAS#OmMKD8Kw=SK~8LT~Ky+2{;Ym{^dSl
zURgWq06>Jt5EpodbbpH)oIaDGI|L`}j5{4A567ML;>cfI8UgfQUW486D`E!Wwpv}6
z-#~(=WY=f0z_TPKCSRCB*zN69g0Ss&erbFyQbNre5Ou8sO%d$PLw7O1t+DL4L+L`#
zP%sXK|GfmZgJo5DlD9ghoq&mzU%rlfJsCaox1&qsvk29fDeudLpj`xgMG}j<sLqwy
zlTQofUR>$B@mY+CwOk!47U)#uv;+!A{X;4JY(;liOTd1_F9(pebx<Ubbav+9Q}81o
zAmF26qZ_yLh7#XRloQTBkmT3hnd0>zQhpmIID&idqqm5P5X8JjrbZqfbu3QYj;m6s
zeAa}WcNIOc$)=cTMD0skAbxfF^GjE^8MM1LY}8ifaaip_pIV$)S~3bJVEt66NON3m
zJFg99i4h>6#zmuV0-<*mBx@?*AgAd1TPL#D#h!2%qDlR~R#x(VstQ=GL3aR|6cNay
z8$ZK~PF|Rrezl8ulfJTjExZCSZ6`Qc2D-2w)2FUHSwO9&|NHj*2A>0{R}z4hN4E=r
z(si33%}7*KbXyo6q~WUDInN{4fsr2o#2Qcf>*FnII5q|{iUPtqz>io?=Es7%p@KJe
z33jMc#QbY40~eR7ND2`>z=TnQRnnqWYM=|PohCR0GX28pQiFOYjeIp~&z+Lmmmo=T
z0#VqjZSFAkusVv`B8|Lor4&wakz`$O?<TmhG+00R8JM?#UVyo-@H!v6?FGyTnhoGL
zj)t5@oJO76H>rBFdt`EPaannS*L&)k(&t$H<ox~mu^0RSX&PV)H3;lnT<XBcoe2;Z
z+SH|%Uq4;&rBY8~QHmOX-Y2xz7bo#>J_(fHFu-db;Y?^N>`B#H`V&_LaLzIUN~j5g
z-@fTUMo+B0wJws&0+huh0pPRn4WQmMJ+Dt@zIJ5f);bdje+SCym%nB!wmre&%K##)
zL{L;=_FA;*_p|4o(sm1V*`l3*9w{8P0501slPACCG~pb_vr+?fXuA1Gn)>PB&(ph=
z>%v+OMKVxO2=~6+tu36g1+Vwvtm(Ts^2-{RpBlQLYmikcDJTaJ-fu(V2FEnqwp)*r
zOR{!|qvcHEuEfKuihbq0#&-TwsVpWz045@JDPUadFHS+JNGyQI2YR9&r8lIDfQ}n#
zB?i+fbt726oq?hmE{Hx6hU(X7t=LW%>sC6fbYOwez6N5bfjY+@T76z8Q_u3iCJm>b
z;n|ZgD9$N7!S;s?0*<?MZ!`LzCunSdA}xS+MKE`{D$<kP`Da(x*C*E8igI#l*+@{A
zEq)6$Y-|`*HYuDC$^tOL(ft%4b58(Uz#t%pv^?N>3QJ)?g?rJnG8G}}wZOo@{7tX~
zbj1_po!beI&}T>@Pv;6@tRP~HZqd>zFEMIuA$?G1HO0cmKjt^u<&k+tREB;EHEgKT
zLk09y0=L;f{$>4+$lcmrEVXOA=f1ZfICZF1pg})1^+0@x!lhd3aeJlNpn{H0N((P6
zmPY!cOcd!wo)#J-_z?JS9cUb}J~dkhhb?COwDUtKUdvTGkuc_>U@krcQT6uN?&|95
zv3W%)sAWpUs3m4JFc)j<II9L_Ek#sj!5!=#Mq4;xS>|G_8T4#CwXas2%Ivpiu*#;$
zf8V`CTW+rUC<2jX9UhOXZkjIl8d?0zP})(YA1_dIa&khCjkg2x^O;W0l0JhnA*CP&
zbGuDtyY9b3p77A_!##6$y?|9(neP5#Di;Da9kpl*kwS@(IdG!FI|qSBun@0oB4?)*
z_0|zlad0Sy*GS=z31k6lBXl%0hb{1m8HXi3#y1jIOe4|j+HZk)e0f;vbn(xqS=mM0
zGuN#`?&c#48uRBqC2oC%PmO@wVf5x2=32%p>G*)H_r!1Wd5d>F*%KGJC|XH1D`1Xo
zv)BNOV(vT%26R!;!7ETt<Z$M;_o8W(fK|H>&OYQ7lu-srbGnKF&TyB|t6xE-^$rxv
z(I2+OT^_qQ0mMZgx%)zPgL;nBD(DH#ovsF*s64?^n??F8>T}Tt)CMrB&f|M2Ds6b-
zDRA1YFzp6jwKcGQ`g_U9^$~A)7F9kmik4a)u4#wb%3-Q`Ml2vh3$~+27Df!=g=^rA
zkK4@fQfXEdrhNVe^vl)SlJ4&NHSk~WBqCXlcD6KDi81kIuxDR!y6@G2Y|)R!!;N89
zO7DGt;ln;!tH^0hRgt`KA2?mnD*XiivQ5|uV48_wH+&w{iGYOq5oRN`l5khh#GtTg
zI$<r$j)?z>k+Ww*@U@V8`_W4MbFW>#bMO{V9#IGS9*@gzyS{|&cjF3PXWRkCcJz0m
zs;z<GhXFW+SUHIU1{fK|lC=tvpF7E3xi7jVw<Iu;f07lmkS2n;P)id{{&JCoZ}~a%
z^R}>{3D`phr5Jc8G0S^c7ABH@l8R*E@pnKOt|uu{8j_kf^5QjTk}XIdtgy&>>XhH}
z%ELKJIO}K+D)hk5njn?~PCl$CiOrx+E&ca+ZU~&3RLKx#1{Hbnj2RvQhO2k?$|UQ6
z1+Ngrx-0&yf2jYzT@E@|?s@cz8B~`EUu@+%tq&0R6j-wx)MYUsjqd&M2w&fc__xyX
z@9;R?M-^b@%<BL|=|`}ARRTb9uEzF*Z<IBBTLx|L6W7*W`pECa%^OkP?FD%+{JyM$
zNStL%7=*N-TRH9d`DP})sOa<7Khs2@ybypw+Kg#`7vzFJ!U#rH!!BS-^r|eVK}3`Q
zQoca|#^xG*7}$CxU^U6)BhNCpepahrYsUh~ksj5F)@}iZ#(?vv2qR#vH5k*Dn!Rcp
zC?b1b1Gf0(R3U7l$<vvSgtx(UZ?-}b92C@mUQ7VbTJW$z{C%S^0|UbVAV)YJ2=Dtz
z$;cF5u8R18{C@jM`Jao(O+NQ*m-UkxyG1<_Mp4Nl=R3hbtTpj6TUOpKp!3zC_5>_i
zF<`@26ch8q5J2M^k(hlA4wk&<&94tTA3V(;1U)-Qe@+&}OM&W1c-f=oC#Bf0pH&q@
zaLCu*J-tz@<y!Uyv|AFK=><ntR!?E`#dZ-~ETjnXPo)q1$_;M2ES(__lHn~IFPE!*
z&)1VDik@+tEJP4O<3Nm|(Bggm5ybSBFZ=Oah4jlt<5<gT!CV{yMNgqa*k)DJ9T$o#
z7=kd;QDNGvH72GA1I3{5x>HcJ$?=HaCI*}Y+qF&x>jQ}}h!iHVUXB9^!Y=?wYC^TZ
zj-m#AiKc>P1fupt&;vpq#346m1q{mX=>Qr1to9N2%TRdZYz?4e@PvoXEth+m{-xj`
zD>P$gXICjTYGnt7xf+9T7fZ{z$~+9?hm0y(Cbj@e+;%K!7FC?{Y_8h{CJZ@<T%wY9
z;e``9QW^?Ahdemap!R-T#&B&4*w9`!ri8)E;VK<}$at>B%F^-jY8kPxNLAY}J)bT!
z;sB-p)5+nJ1)8!7$?PRYcz04o8eyQ&C^`bN0F4?OU3mosjS_pbOfUz@0L3e^i$oQc
zg>yQ6ownq$i!%|I%ZI5v=G-m^cX<%aCTCk}R^HDUqc<?9mf1kUIQafzUClzRg4LQA
z3}~uo2B1OGc=E^2N<i@!9N3~Fb5c@LUTZvY=IlIaX-U0JBX)NK1#s}l+e#Xau)zjN
zzOx1t(k`65zB`zhyoi^TAW~x!fn3kQ9K*zs8C|%f+@-9$m7G|sI-t*+bbu(MRIgRd
z0GS$o`b`w;@?2~G-7J)emtD40LOUq72nF2t40Hj68~jjVJC}a2GNm5`BivOlv#T*`
zzc4Z}$q^t4y6>g6(2*y>inNt6@R_nW9I<;2qsWD{DSXdNrhqZnk|?YXSsW)wDJ}M4
z*<aGHvgh7sNl5$IV|--siU$e59%sUl7Cu581e~L;Y%?6waY(`tiZJ+W#=4H6mYI|Z
zJ~GRZk2)O+?9sU47f@k&p)r#sL8pfe_{)+o)GF>9P~Lk%-@Dee^EX&7=4$&n=IR8(
z%tH_hDHt1PweXVbm3*tV{v4iq1@;RoD}S)s@&`d!tGUPuKu-=(9B@ZspDGYgP?Q#m
zzF@C{pMSkzJ!irjV8dH!=-sHT-U7*j#`=Z&#S&SAfqIjNBkcSDT>Y8mgDe7E=Oz;@
zGQF=uY(|-RX&%jY1De2xDuAt*2vHd8a03zawjxjGBqEbTrQ5C^YzUeD5TNFTdJ9k7
z_lotJ0QIY9Hk~DoM1Z9}LTSh~K8!~|$0dW^Ee?<8eJ+mGH3_pq!-abCv(W|ty<d1E
z-PV!jXptz%ss}G%n36NqL$(I*>u00Csy4G>SxgRQ;;%sud;Cp=i4ddev4nB!H^2dG
zMT$~2OFAW}n;j3|`whAY|2GpFptfOMgG-v3;qR9d(O+#3F6aeNgb)p+rT;2NnI-*x
zj4;HSJN%Eb^#8A0zT4O9B|i}gq<Q=Jtb+pixR{tMBT1=|wuZN^Xghe4f6t?cJAM4f
z$B!T7oSZ5_G9zzbkY>>6rd;~GnT1g`LkZKV4jWh3Jg~~o?;iXR0^2V@6$6n`P{J}Z
zjr3CPROe2x;CtqQ0?0N0(IX2sarNQm@q}=v$*N)E;3UPwKu$Oi$@T9}#XfRwWPu+>
zRUIZ*I6#0*G7B_x6S~3N1~L&JT3cJ6tTZ`3`kTcML|5IrzX36?YDi7SjrG~c3XZRz
zr~8{Wn>`B#zsu?wlUaM>Y;5XrcPs9F_{&&W_EK}b%QEn_viFkbpN0zRdvTUD_IDc>
z%|QD9S|<hZwOfSz5jRSq7J=n^;749sMXFT7IKZ0QWlPO#7aqfio-HFWgA!%~pOdLW
zp!b~nL9>{doVM`W4*_WnZ0<GTv@^#P&(ze{mDKU2;s0JN1KH*m1!Rg(8hHxxK+^RQ
z?AeE)I{`crl#uOK0d~VBaUq)+5Abef>q$ku>rU93fgADkmmrP67p~tdKlg*()ePuO
zvw4s`K}nt&$bSCRHSRSi!hs9RZ&d}tSNok4&0g+B#p!^RpU$oMCk6m50KV?s&M)0C
z0L-F#U<#bb<^&bsc=~p90q%NXfuk30zO6l%heZGz`Fp$g^bB&yajB`6BCp!gionj+
z|4}~PDV?~>1Z>EMfJN;GFFyzHOu*bFD55&qThwm>6;8Tf7q=+u17Ry0NNcdJRp~@T
z7JYMw!AabAV+h`Hc-I9o0f)BN!)k1@k;K&Q-#tHLLtU0hLDg+F23c>9gx&AtrTts*
z>A)aELM<&p&A<1PVACdf3@)iZssRoh2`_2D05HQtG@Ta^eA<KppbH54Llyw%t0K}>
zH~gFu6B6(*mZ%pLh?W=`DcS$<i~@2Mb_goeb$`#$CmXyFc`nSVV8KSW?VCtdn8qBQ
z!vy{*@MpbTrhcIU15sc}T)WIHC3S#rnD`;9$7oL^2`y-~T>G3A&owKn8U!9d9ZLK}
z;ha_N-$~993#F9jMw@~+z5tQ1pbc$_Fa?wSi<lDq+F;MDeQrksUJXr6+ZR!hk&$B=
zlI3OK(AEI>z*t6F`$9d4ASFhGehcr=>o<8+C}RY~$!NrQ2;iOS$Nbkd{NmcZdE%^N
z1m0Gi?Ck=0u*!K<ZA9lj_B&GNR<(eIe$I{2%*@>d-b$jlm&j)Q*ZO$TF^e&f0Tx(c
z;GYdFZsyfp+OYf2#qKw~b>0|4zvkeN?z$$V`_EW2K}O&w)=y9*j}O)3tvZExd>9cv
zQu;u=25|Z6pC>{tTgJC<0G2-&)BEQ3atWTM_oFkWK`vnMYKGZgp~WZFAm;HZePmZB
z4s=>nAR?<jnT_3Od2@TiZzl5d_aX*NC10<)a5%=Jm)w2!_|oH6r!!K}+@pQL<M-%b
zY~4~oAdC6mSqaBOXcA{4^XLI;1A=c}0v~4>b^8EfY5M{)(bal9p*XGS){a7?qe?J9
z@|@u@z#ys>D4BQB-_ae%Dn$&T(~kE5ENA__b!mm=rMz5RsGtcQuP5+1++`eiXq~g7
z+<EExRr&GUukewHd%mZe^ku(msbZ_Zs=t>A^xYcj326sFM>3yKt}~QYEC(7ZRvm17
z8^NdNvJlctXp)^z%~=!KbK7`Xp*5O%w~pXTNmMM6=kC=9D>%X1@85^XVLHF}?`tNl
zeu(hz>I=_VLyGIi0H0mmx7LJTpyIzS@SQ&|<G;TUHZlqRG>lqLf5Pn(Q<(VOUBEx|
z`@mXpTTnAWxNULP1p;pHzkcOd3l-Xov;}8ewjsD2^0%0;8BOqubY<--sUK3<nuo`o
zN?Dm}3o5FpBt=GI^$!hU?P}xGQ&<R9QGbi8%@(|!0hg<n{LkgWzKsBd8rkd@^LOx+
zx5j3U2I$ZRtZe+n+I(5Uodi@QSu{R|f}SACAoha-bUAKvQcGT5z5*KuheDX?J`NcK
zTo>=@-OZ1KZ}(JX+l@4hQ~H51mJcikp^a|4m6^Pv?nqyIJ~4$jkP_`bj$(#Fl_$P_
z-DQj#u{0LD%vQDpPceJ<>320e(*0hmmga8rEk0iNSkWBc*<9_~YJKa&Ux)0;pk7f%
x$Yx6f{pwlzZfYS3zp7hRI<vy}T=)=Vczn~<(1^QwI#YdJO;}y^{lAZZ{|`|KIT8Q>

-- 
GitLab


From 117c12d135039797e5c00e9f1c87ece7f4be13e0 Mon Sep 17 00:00:00 2001
From: Jc2k <john.carr@unrouted.co.uk>
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)
-- 
GitLab


From 257ae4d8d342b81c4146a654d19ba3dd9f422221 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
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     | 107 +++++++++++++-
 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, 404 insertions(+), 14 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_matrix_effect(
+                    effect=EFFECT_OFF, power_on=False
+                )
                 await coordinator.async_set_multizone_effect(
-                    effect=EFFECT_OFF,
-                    speed=EFFECT_MOVE_DEFAULT_SPEED,
-                    direction=EFFECT_MOVE_DEFAULT_DIRECTION,
-                    power_on=False,
+                    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()
 
-- 
GitLab


From 3ab294e8efc00c9f3cda2993318bb582ba675f8c Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
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."""
-
-    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}
-        )
+TYPE_TO_CLASSES = {
+    1: (PowerViewShadeWithTiltOnClosed,),
+    2: (PowerViewShadeWithTiltAnywhere,),
+    4: (PowerViewShadeWithTiltAnywhere,),
+    7: (PowerViewShadeTDBUTop, PowerViewShadeTDBUBottom),
+}
 
-    @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
-- 
GitLab


From 82d3397a9b6847d537b1158dcd335e547a11707c Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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        | 353 ++++++++----------
 1 file changed, 155 insertions(+), 198 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
-
-    def __init__(self, device: _SensorDeviceT, gateway: DeconzGateway) -> None:
+    entity_description: DeconzBinarySensorDescription
+
+    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),
-)
-- 
GitLab


From 7e19e56c6bbe797fcab6b3c670c1f89373101baa Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 20d71a869ef0da6ca21e624355b2bb109f5c6ff2 Mon Sep 17 00:00:00 2001
From: "David F. Mulcahey" <david.mulcahey@me.com>
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"],
-- 
GitLab


From e8af0071243cb4eb239ba0744cafae54a0d3618b Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
-- 
GitLab


From aa58d7fbd66efc05b7394703e501c7dd3c449202 Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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
-- 
GitLab


From b3ad0eebcd87437ea67fd4b7aa9c8b2c0888e0f1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From ac44c8e34d659305e7b67064eed8e1813835f379 Mon Sep 17 00:00:00 2001
From: Steven Looman <steven.looman@gmail.com>
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"
-- 
GitLab


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
-- 
GitLab


From 395636dfc2d52a17f7b31762c3bf19fbafed1273 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
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
-- 
GitLab


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
-- 
GitLab


From 262d1ad2a0d43b788d0b9e9900db6ebd13acb9eb Mon Sep 17 00:00:00 2001
From: Nathan Spencer <natekspencer@gmail.com>
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
-- 
GitLab


From ce4d93b0c1b11c7cde8d9543c1582bf2cc385dd2 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


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
-- 
GitLab


From 7d097d18b0c6041475080b3c400e37b25185faba Mon Sep 17 00:00:00 2001
From: Austin Brunkhorst <AustinBrunkhorst@users.noreply.github.com>
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 <nick@koston.org>
---
 .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
+    )
-- 
GitLab


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
-- 
GitLab


From eac1a1e51334055523d8da2935f0c9cda3e9da4d Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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"
-- 
GitLab


From 8aa5a785b5990ccf97c75d5cce843f0a6dbc0da2 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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"
-- 
GitLab


From fe5534666dec21a7b31cab0a82b1313a83e316b1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
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 }}
 
-- 
GitLab


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)
 
 
-- 
GitLab


From edad6d0f268e8c7cb98dbb303341eb822bd81527 Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
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
-- 
GitLab


From 69d935b7bd9f107ba474e6a4c5fa580617bccfb6 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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",
     [
-- 
GitLab


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
-- 
GitLab


From 8aa30cce26715255d43c9a02102d9c9668f6546f Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
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
-- 
GitLab


From 65187ab227f9136c2f0e89b49ab4772ccf4fb678 Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
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
-- 
GitLab


From 6826f2c291da16a76936f1087b7032210d1824f2 Mon Sep 17 00:00:00 2001
From: Guido Schmitz <Shutgun@users.noreply.github.com>
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)
-- 
GitLab


From d01f85b6aa21239a918552735f45614ce147d199 Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
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
-- 
GitLab


From c52b900bfe5cdf61360a3751cdee7f057eaae82b Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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:
-- 
GitLab


From a391b8dd9d212caeaf597126162d2c42928471c2 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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(
-- 
GitLab


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
-- 
GitLab


From 9aa60432558c98496978183683c070b2b9616b69 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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"}]
-- 
GitLab


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,
     ),
-- 
GitLab


From e7c614a8253ca1cf9e75f34e73d5f78aa3fc4cd7 Mon Sep 17 00:00:00 2001
From: Sean Vig <sean.v.775@gmail.com>
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,
+                    )
                 )
-            )
-- 
GitLab


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),
-- 
GitLab


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,
-- 
GitLab


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,
     ),
-- 
GitLab


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"
-- 
GitLab


From 918243b7c8a4a41c2b8cef65bf0444cba6996928 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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, {})
 
-- 
GitLab


From 62c4cd3c26d201ffff15876adf4d66a5fb00da34 Mon Sep 17 00:00:00 2001
From: Martin Hjelmare <marhje52@gmail.com>
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):
-- 
GitLab


From f2207af1c96b918d0b4bd365b2730c731da3557e Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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       | 61 +++++++++++--------
 2 files changed, 41 insertions(+), 31 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"},
+                    "macAddress": REDACTED,
                     "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,
+                        "tempinf": 70.9,
                         "humidityin": 29,
-                        "lastRain": "2022-01-07T19:45:00.000Z",
+                        "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,
-                        "solarradiation": 11.62,
-                        "tempf": 21,
-                        "tempinf": 70.9,
                         "totalrainin": 35.398,
-                        "tz": REDACTED,
+                        "solarradiation": 11.62,
                         "uv": 0,
-                        "weeklyrainin": 0,
-                        "winddir": 25,
-                        "windgustmph": 1.1,
-                        "windspeedmph": 0.2,
+                        "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",
                     },
-                    "macAddress": REDACTED,
+                    "info": {"name": "Side Yard", "location": REDACTED},
+                    "apiKey": REDACTED,
                 }
             ],
             "method": "subscribe",
-- 
GitLab


From b41cd57c337d7bad420b5baee1b5dafe596759d2 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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,
         },
     }
-- 
GitLab


From 030205df8fae4beae3db90a15f8fff1f7deea279 Mon Sep 17 00:00:00 2001
From: Martin Hjelmare <marhje52@gmail.com>
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,
-- 
GitLab


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",
-- 
GitLab


From 0f002e704466881b68fe76ddf233746f5313ac2f Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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": {
-- 
GitLab


From c05390e09b5e14310c89039e0a2412f6f72b94b2 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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",
-- 
GitLab


From 020b7e97629e150d175446862b94ff78c2b0d54c Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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},
+                }
+            },
         },
     }
-- 
GitLab


From 4ea46bf8418f329074bedac54635be2bc0076bbe Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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,
-            },
         },
     }
-- 
GitLab


From 2f6e55b3bb42c647367b7d8f1c83b5be5cff1664 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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": {
-- 
GitLab


From 8bc9aa9ea4e2be57c00a86de74ed21b8e2f0967d Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From 687987f05b9a456f2ca0799c1b4c256b462a7252 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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": [],
-- 
GitLab


From d4465e4e69eab00536f4776b5367dfcea9f9c87a Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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",
-- 
GitLab


From 1262c0e221f9fe9a8b4dd07367b9b6f00ea69b8f Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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": {
-- 
GitLab


From cc13641f29385d58c425ca4afa10f5623c484dab Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
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,
+    ),
 }
 
 
-- 
GitLab


From f92da26c042bc85290e3bcd9eb1737c338676d08 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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": "<EventState.INITIALIZED: 'initialized'>",
                 },
             }
-        ]
+        ],
     }
-- 
GitLab


From b446cab03b28d80bd62e1ad68ff87e5b3189829a Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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,
             }
         ],
     }
-- 
GitLab


From 54fb0d7cc2061ac6928c3af035c82fdb36f803fc Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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)
 
-- 
GitLab


From 9dd894a36e7dafac0bec15602c40dec6569a8277 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi>
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"]
 }
-- 
GitLab


From e3a3f934415beb195e26317347cfee9af4e95633 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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,
+        )
-- 
GitLab


From f23b1750e85f07091eb896a0b12b8f95e5646338 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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          | 100 +++++++++++++++---
 .../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, 780 insertions(+), 233 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,26 +320,86 @@ 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}",
+                    )
+                },
+            )
+
+    @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,
+        )
 
-            new_identifiers = {
+        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}",
                 )
             }
+            legacy_serial_identifier = (
+                IDENTIFIER_SERIAL_NUMBER,
+                accessory.serial_number,
+            )
 
-            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 = 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=new_identifiers
-            )
+            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"
+    )
-- 
GitLab


From a18f8d2ff3581f555403853a229e1937bd70ecf1 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From 712f40b6b031f60c52b1c040e8494127fd7a1596 Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
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."""
-- 
GitLab


From 230fe4453fbbeebbba9484a3fe5d5f1839fcfd9c Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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",
-- 
GitLab


From 46794f7a5d7697632eb106f14d3aa6c5615dc736 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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
-- 
GitLab


From a9f8bb32775968b69a1aa19944277a705ce7a200 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From d71a9d6ab32d6a535890bbb44936b687143378d4 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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",
-- 
GitLab


From d03e0380bbe8fd56049b93b2df9cba55f65bcf64 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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,
-- 
GitLab


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 <marhje52@gmail.com>
---
 .../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
+        )
-- 
GitLab


From 77571c8a84e093fa6cae50b850e96a5fa8e9f02c Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From 93961690609032589340fa69529b53700da21348 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From 1191f4b61db3562cae274a6eb67762c348f45464 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From 237b03150ecf1de86a9488ebb391838007420cb7 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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,
-- 
GitLab


From b28ad1282a8b31b604d5bd2496a7fa75ad2c7e77 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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)
-- 
GitLab


From f43c802a03640b893472de5a376a1bcc3723044c Mon Sep 17 00:00:00 2001
From: Jeef <jeeftor@users.noreply.github.com>
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"
-- 
GitLab


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
-- 
GitLab


From ec55a7b603a09b562dde1674f8191e467c974c76 Mon Sep 17 00:00:00 2001
From: Mike Degatano <michael.degatano@gmail.com>
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_DEFAULT in logger_config:
+        set_default_log_level(logger_config[LOGGER_DEFAULT])
 
-    if LOGGER_LOGS in config[DOMAIN]:
+    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)
 
-- 
GitLab


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",
-- 
GitLab


From 83557ef762c8ba041642b050f3ecb86b8e420806 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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"
 }
-- 
GitLab


From 577f7904b55619fb868cbfec7fe9db4dbded5150 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
-- 
GitLab


From 69e10e59821f7e5ca1d4d305079f059774b67864 Mon Sep 17 00:00:00 2001
From: Erik <erik@montnemery.com>
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_valid = migration.schema_is_valid(schema_status)
 
-        schema_is_current = migration.schema_is_current(current_version)
-        if schema_is_current:
+        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)
+
+            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 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", {}, []),
-- 
GitLab


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:
-- 
GitLab


From 4a1c40f09ba18876f31f6aef4b2ed3806fff5bf3 Mon Sep 17 00:00:00 2001
From: Erik <erik@montnemery.com>
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
-
-        schema_is_valid = migration.schema_is_valid(schema_status)
+        self.schema_version = current_version
 
-        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()
-            )
-            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
-
+    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)
 
-@dataclass
-class SchemaValidationStatus:
-    """Store schema validation status."""
+        if current_version is None:
+            current_version = _inspect_schema_version(session)
+            _LOGGER.debug(
+                "No schema version found. Inspected version: %s", current_version
+            )
 
-    current_version: int
+        return cast(int, current_version)
 
 
-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", {}, []),
-- 
GitLab


From 30920c3da7a714c5fef85fba4bd84191a9e5f44d Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-    )
-- 
GitLab


From 1c86a12233861a5e7b8f1cd7a71c34254e44bfef Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
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
-- 
GitLab


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)
-- 
GitLab


From 690556a5f14653facf73b70ea6a8bad6d9a71db9 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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/**
-- 
GitLab


From ad6c3d1cde5406cfdb4d333a50c5bd76a904251f Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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)
-- 
GitLab


From 37a5a09910af223fd93e1f8d88bb653fb261b6fe Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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)
-- 
GitLab


From c6340856e9b8da129aa03149d31544976eadda90 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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,
-- 
GitLab


From fc32071562de406c32e75410cd87920f82153856 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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:
-- 
GitLab


From 0daa5b55b5a97d2c9be72e2f17b67d33f12ecb7d Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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
-- 
GitLab


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
 
-- 
GitLab


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
-- 
GitLab


From 82322e3804af9cac55c6bea106f4bb0faff4c298 Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
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 <nick@koston.org>
---
 .../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": {},
-- 
GitLab


From f5868f00a0a9857bf8a26ae96e0f7b357570c909 Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
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)
-- 
GitLab


From 4cf0f9b19711cca20b476ea999ce223c5c6c09b0 Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
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,
     )
-- 
GitLab


From 01c66aa7c1f9624095fe472ff02d9613423d7853 Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
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",
-- 
GitLab


From ca4c4774ca9cf4efc9578cd9b2a9d9bd39ed9b31 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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",
-- 
GitLab


From 466c4656cab32e3939bb61f90c52aff44180a5fb Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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_valid = migration.schema_is_valid(schema_status)
 
-        schema_is_current = migration.schema_is_current(current_version)
-        if schema_is_current:
+        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)
+
+            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 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", {}, []),
-- 
GitLab


From 1e75c3829e72531eabd96afa1bad7f09e9530b04 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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 | 53 ++++------------------
 1 file changed, 9 insertions(+), 44 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)
-
-        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()
+    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")
+
+    await component.async_add_entities(entities)
 
     return True
 
-- 
GitLab


From ea6368775b6612cf0c7b3f92f74658fabf64fa97 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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)
-- 
GitLab


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 <frenck@frenck.nl>
---
 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]:
-- 
GitLab


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 }}
-- 
GitLab


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
-- 
GitLab


From b0ef1e3315906b4930806800aa0229995d5b36bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= <oyvind@wergeland.org>
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."""
-- 
GitLab


From 4462f2fc46957cf7b47c595c8125cfdf85291aee Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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"
-- 
GitLab


From acb147767366e73402a6ad4f9e8a053d26959110 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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:
-- 
GitLab


From 04cc2ae264c6ed43482e24ccf3cae24d191122f9 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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"
     ):
-- 
GitLab


From d80c0ddb5f1ed0265d10314b587bf7caa9823468 Mon Sep 17 00:00:00 2001
From: rappenze <rappenze@yahoo.com>
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 <elupus@ecce.se>

Co-authored-by: Joakim Plate <elupus@ecce.se>
---
 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
 
-- 
GitLab


From eae96eb4c22936b94e67b95eaa3a0e5083793b54 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
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",
+        },
+    }
-- 
GitLab


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
-- 
GitLab


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']"
-- 
GitLab


From e852c9b012f2f949cc08e9498b8a051f362669e9 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
-- 
GitLab


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)
 
-- 
GitLab


From 3379e144177cd3cca02fda5360177d66f6aa84e3 Mon Sep 17 00:00:00 2001
From: Quentame <polletquentin74@me.com>
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)
-- 
GitLab


From d87f433be7c114cee29068ce408a1f5498dacb59 Mon Sep 17 00:00:00 2001
From: Quentame <polletquentin74@me.com>
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
-- 
GitLab


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 }}-${{
-- 
GitLab


From 180b296426e9ea77374bc7a585b3d86fa11edff1 Mon Sep 17 00:00:00 2001
From: Diogo Gomes <diogogomes@gmail.com>
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 <levyshay1@gmail.com>

Co-authored-by: Shay Levy <levyshay1@gmail.com>
---
 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."""
 
-- 
GitLab


From 5a51738b2f44b669f05cd366811caea00fe3978a Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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
-- 
GitLab


From 46c60438565bbb2e778bb1fc22b13fc6dfeb53f6 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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
-- 
GitLab


From 000e0920968d7713e0fef9251a476e66bdbe1894 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
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.",
-- 
GitLab


From 5b6e46e7b78d9a4a030dfe4c0ff8bd18054cb328 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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
-- 
GitLab


From 2e261d5dc28479b1d1d0ea8b8ef6cfa2cf6c4917 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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
-- 
GitLab


From d327355afcd0643797f56686added805dbc78aac Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 2c206ad05081d6b6379277f7b6480c4a3e4c8df3 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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)
-
-        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)
+    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)
 
-        wait_recording_done(hass)
-        wait_recording_done(hass)
-        hass.stop()
+    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 patch(
-        "homeassistant.components.recorder.core.dt_util.utcnow",
-        return_value=now + timedelta(hours=1),
-    ):
+    wait_recording_done(hass)
+    wait_recording_done(hass)
+    hass.stop()
 
-        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)
+    # 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 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
+    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):
-- 
GitLab


From 6470a6b2033636faa59129f29c6ba51b8ceddd62 Mon Sep 17 00:00:00 2001
From: Kevin Cathcart <kevincathcart@gmail.com>
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
-- 
GitLab


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 <davet2001@users.noreply.github.com>
---
 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
-- 
GitLab


From 0f9703cd99459cc78f1036b1899d67a69ea4ccd9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9n?= <corbeau.lenaic@gmail.com>
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",
-- 
GitLab


From d3840a04b58765b2eb59c0bd12338c0b0b27bf82 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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]:
-<!--
-  The Integration Quality Scale scores an integration on the code quality
-  and user experience. Each level of the quality scale consists of a list
-  of requirements. We highly recommend getting your integration scored!
--->
-
-- [ ] No score or internal
-- [ ] 🥈 Silver
-- [ ] 🥇 Gold
-- [ ] 🏆 Platinum
-
 <!--
   This project is very active and we have a high turnover of pull requests.
 
-- 
GitLab


From 4dd0c079d533b16796fd4a86d1a5ceef8eefc41d Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:06:14 +0200
Subject: [PATCH 451/985] Deprecate name property of unit system (#80257)

---
 homeassistant/core.py             |  4 +++-
 homeassistant/util/unit_system.py | 16 ++++++++++++++--
 tests/util/test_unit_system.py    | 20 ++++++++++++++++++++
 3 files changed, 37 insertions(+), 3 deletions(-)

diff --git a/homeassistant/core.py b/homeassistant/core.py
index f4cefcb7fff..5a8ec07c1b9 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -2015,7 +2015,9 @@ class Config:
             "latitude": self.latitude,
             "longitude": self.longitude,
             "elevation": self.elevation,
-            "unit_system": self.units.name,
+            # We don't want any components to use the name of the unit system
+            # so we are using the private attribute here
+            "unit_system": self.units._name,  # pylint: disable=protected-access
             "location_name": self.location_name,
             "time_zone": self.time_zone,
             "external_url": self.external_url,
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 56c588ed1c9..347c8ae859b 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -31,6 +31,7 @@ from homeassistant.const import (
     VOLUME_LITERS,
     WIND_SPEED,
 )
+from homeassistant.helpers.frame import report
 
 from .unit_conversion import (
     DistanceConverter,
@@ -107,7 +108,7 @@ class UnitSystem:
         if errors:
             raise ValueError(errors)
 
-        self.name = name
+        self._name = name
         self.accumulated_precipitation_unit = accumulated_precipitation
         self.temperature_unit = temperature
         self.length_unit = length
@@ -116,10 +117,21 @@ class UnitSystem:
         self.volume_unit = volume
         self.wind_speed_unit = wind_speed
 
+    @property
+    def name(self) -> str:
+        """Return the name of the unit system."""
+        report(
+            "accesses the `name` property of the unit system. "
+            "This is deprecated and will stop working in Home Assistant 2023.1. "
+            "Please adjust to use instance check instead.",
+            error_if_core=False,
+        )
+        return self._name
+
     @property
     def is_metric(self) -> bool:
         """Determine if this is the metric unit system."""
-        return self.name == CONF_UNIT_SYSTEM_METRIC
+        return self._name == CONF_UNIT_SYSTEM_METRIC
 
     def temperature(self, temperature: float, from_unit: str) -> float:
         """Convert the given temperature to this unit system."""
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index a284fd10017..72a4456a219 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -3,6 +3,8 @@ import pytest
 
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
+    CONF_UNIT_SYSTEM_IMPERIAL,
+    CONF_UNIT_SYSTEM_METRIC,
     LENGTH,
     LENGTH_KILOMETERS,
     LENGTH_METERS,
@@ -298,3 +300,21 @@ def test_is_metric():
     """Test the is metric flag."""
     assert METRIC_SYSTEM.is_metric
     assert not IMPERIAL_SYSTEM.is_metric
+
+
+@pytest.mark.parametrize(
+    "unit_system, expected_name",
+    [
+        (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC),
+        (IMPERIAL_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
+    ],
+)
+def test_deprecated_name(
+    caplog: pytest.LogCaptureFixture, unit_system: UnitSystem, expected_name: str
+) -> None:
+    """Test the name is deprecated."""
+    assert unit_system.name == expected_name
+    assert (
+        "Detected code that accesses the `name` property of the unit system."
+        in caplog.text
+    )
-- 
GitLab


From faef09d3d7bc33720a740828822b154f46a11915 Mon Sep 17 00:00:00 2001
From: Tom Brien <TomBrien@users.noreply.github.com>
Date: Fri, 14 Oct 2022 11:10:24 +0100
Subject: [PATCH 452/985] Add repair issue for Coinbase YAML (#80156)

---
 homeassistant/components/coinbase/__init__.py         | 11 ++++++++++-
 homeassistant/components/coinbase/strings.json        |  6 ++++++
 .../components/coinbase/translations/en.json          |  6 ++++++
 3 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py
index 36ce65517db..49f62a751ab 100644
--- a/homeassistant/components/coinbase/__init__.py
+++ b/homeassistant/components/coinbase/__init__.py
@@ -11,7 +11,7 @@ import voluptuous as vol
 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
 from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry
+from homeassistant.helpers import entity_registry, issue_registry as ir
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.typing import ConfigType
 from homeassistant.util import Throttle
@@ -54,6 +54,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     """Set up the Coinbase component."""
     if DOMAIN not in config:
         return True
+    ir.async_create_issue(
+        hass,
+        DOMAIN,
+        "remove_yaml",
+        breaks_in_ha_version="2022.12.0",
+        is_fixable=False,
+        severity=ir.IssueSeverity.WARNING,
+        translation_key="removed_yaml",
+    )
     hass.async_create_task(
         hass.config_entries.flow.async_init(
             DOMAIN,
diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json
index 96bf021e394..6edd0f25338 100644
--- a/homeassistant/components/coinbase/strings.json
+++ b/homeassistant/components/coinbase/strings.json
@@ -38,5 +38,11 @@
       "currency_unavailable": "One or more of the requested currency balances is not provided by your Coinbase API.",
       "exchange_rate_unavailable": "One or more of the requested exchange rates is not provided by Coinbase."
     }
+  },
+  "issues": {
+    "removed_yaml": {
+      "title": "The Coinbase YAML configuration has been removed",
+      "description": "Configuring Coinbase using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
+    }
   }
 }
diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json
index 019159c8057..d755427d446 100644
--- a/homeassistant/components/coinbase/translations/en.json
+++ b/homeassistant/components/coinbase/translations/en.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Configuring Coinbase using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
+            "title": "The Coinbase YAML configuration has been removed"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "One or more of the requested currency balances is not provided by your Coinbase API.",
-- 
GitLab


From 849688f71fcaaeab2e9e80d143359bf36192d3bb Mon Sep 17 00:00:00 2001
From: DanielV <daniel@vikstrom.name>
Date: Fri, 14 Oct 2022 12:11:53 +0200
Subject: [PATCH 453/985] Add volvo on call device_info to group entities in
 one device per vehicle (#79329)

---
 homeassistant/components/volvooncall/__init__.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py
index 5a24712b7a3..65bc6c1cfbe 100644
--- a/homeassistant/components/volvooncall/__init__.py
+++ b/homeassistant/components/volvooncall/__init__.py
@@ -23,6 +23,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.helpers.typing import ConfigType
 from homeassistant.helpers.update_coordinator import (
@@ -314,6 +315,16 @@ class VolvoEntity(CoordinatorEntity):
         """Return true if unable to access real state of entity."""
         return True
 
+    @property
+    def device_info(self) -> DeviceInfo:
+        """Return a inique set of attributes for each vehicle."""
+        return DeviceInfo(
+            identifiers={(DOMAIN, self.vehicle.vin)},
+            name=self._vehicle_name,
+            model=self.vehicle.vehicle_type,
+            manufacturer="Volvo",
+        )
+
     @property
     def extra_state_attributes(self):
         """Return device specific state attributes."""
-- 
GitLab


From 0b7c84edbd932780e094d35578ff986f34d8cbd1 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:13:47 +0200
Subject: [PATCH 454/985] Adjust distance unit check in google travel time
 (#80232)

* Cleanup unused option in google travel time

* Adjust tests

* Adjust to use local constant

* Tweak logic

* Reduce size of PR

* Add tests

* Use system compare

* Use is not ==

* Adjust to use local constant again
---
 .../components/google_travel_time/const.py    |  8 +++--
 .../components/google_travel_time/sensor.py   |  7 +++-
 .../google_travel_time/test_config_flow.py    | 22 +++++-------
 .../google_travel_time/test_sensor.py         | 35 +++++++++++++++++++
 4 files changed, 55 insertions(+), 17 deletions(-)

diff --git a/homeassistant/components/google_travel_time/const.py b/homeassistant/components/google_travel_time/const.py
index 893c0e48bd0..efc17b22ec1 100644
--- a/homeassistant/components/google_travel_time/const.py
+++ b/homeassistant/components/google_travel_time/const.py
@@ -1,6 +1,4 @@
 """Constants for Google Travel Time."""
-from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
-
 DOMAIN = "google_travel_time"
 
 ATTRIBUTION = "Powered by Google"
@@ -84,4 +82,8 @@ TRANSIT_PREFS = ["less_walking", "fewer_transfers"]
 TRANSPORT_TYPE = ["bus", "subway", "train", "tram", "rail"]
 TRAVEL_MODE = ["driving", "walking", "bicycling", "transit"]
 TRAVEL_MODEL = ["best_guess", "pessimistic", "optimistic"]
-UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]
+
+# googlemaps library uses "metric" or "imperial" terminology in distance_matrix
+UNITS_METRIC = "metric"
+UNITS_IMPERIAL = "imperial"
+UNITS = [UNITS_METRIC, UNITS_IMPERIAL]
diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py
index df5221dea2f..524fae4a128 100644
--- a/homeassistant/components/google_travel_time/sensor.py
+++ b/homeassistant/components/google_travel_time/sensor.py
@@ -23,6 +23,7 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.location import find_coordinates
 import homeassistant.util.dt as dt_util
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import (
     ATTRIBUTION,
@@ -35,6 +36,8 @@ from .const import (
     CONF_UNITS,
     DEFAULT_NAME,
     DOMAIN,
+    UNITS_IMPERIAL,
+    UNITS_METRIC,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -63,7 +66,9 @@ async def async_setup_entry(
         options = new_data.pop(CONF_OPTIONS, {})
 
         if CONF_UNITS not in options:
-            options[CONF_UNITS] = hass.config.units.name
+            options[CONF_UNITS] = UNITS_METRIC
+            if hass.config.units is IMPERIAL_SYSTEM:
+                options[CONF_UNITS] = UNITS_IMPERIAL
 
         if CONF_TRAVEL_MODE in new_data:
             wstr = (
diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py
index 4fe5f797d45..06cc1851421 100644
--- a/tests/components/google_travel_time/test_config_flow.py
+++ b/tests/components/google_travel_time/test_config_flow.py
@@ -19,13 +19,9 @@ from homeassistant.components.google_travel_time.const import (
     DEFAULT_NAME,
     DEPARTURE_TIME,
     DOMAIN,
+    UNITS_IMPERIAL,
 )
-from homeassistant.const import (
-    CONF_API_KEY,
-    CONF_MODE,
-    CONF_NAME,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-)
+from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
 
 from .const import MOCK_CONFIG
 
@@ -79,7 +75,7 @@ async def test_invalid_config_entry(hass):
             {
                 CONF_MODE: "driving",
                 CONF_ARRIVAL_TIME: "test",
-                CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+                CONF_UNITS: UNITS_IMPERIAL,
             },
         )
     ],
@@ -100,7 +96,7 @@ async def test_options_flow(hass, mock_config):
             CONF_MODE: "driving",
             CONF_LANGUAGE: "en",
             CONF_AVOID: "tolls",
-            CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+            CONF_UNITS: UNITS_IMPERIAL,
             CONF_TIME_TYPE: ARRIVAL_TIME,
             CONF_TIME: "test",
             CONF_TRAFFIC_MODEL: "best_guess",
@@ -114,7 +110,7 @@ async def test_options_flow(hass, mock_config):
         CONF_MODE: "driving",
         CONF_LANGUAGE: "en",
         CONF_AVOID: "tolls",
-        CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+        CONF_UNITS: UNITS_IMPERIAL,
         CONF_ARRIVAL_TIME: "test",
         CONF_TRAFFIC_MODEL: "best_guess",
         CONF_TRANSIT_MODE: "train",
@@ -125,7 +121,7 @@ async def test_options_flow(hass, mock_config):
         CONF_MODE: "driving",
         CONF_LANGUAGE: "en",
         CONF_AVOID: "tolls",
-        CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+        CONF_UNITS: UNITS_IMPERIAL,
         CONF_ARRIVAL_TIME: "test",
         CONF_TRAFFIC_MODEL: "best_guess",
         CONF_TRANSIT_MODE: "train",
@@ -153,7 +149,7 @@ async def test_options_flow_departure_time(hass, mock_config):
             CONF_MODE: "driving",
             CONF_LANGUAGE: "en",
             CONF_AVOID: "tolls",
-            CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+            CONF_UNITS: UNITS_IMPERIAL,
             CONF_TIME_TYPE: DEPARTURE_TIME,
             CONF_TIME: "test",
             CONF_TRAFFIC_MODEL: "best_guess",
@@ -167,7 +163,7 @@ async def test_options_flow_departure_time(hass, mock_config):
         CONF_MODE: "driving",
         CONF_LANGUAGE: "en",
         CONF_AVOID: "tolls",
-        CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+        CONF_UNITS: UNITS_IMPERIAL,
         CONF_DEPARTURE_TIME: "test",
         CONF_TRAFFIC_MODEL: "best_guess",
         CONF_TRANSIT_MODE: "train",
@@ -178,7 +174,7 @@ async def test_options_flow_departure_time(hass, mock_config):
         CONF_MODE: "driving",
         CONF_LANGUAGE: "en",
         CONF_AVOID: "tolls",
-        CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+        CONF_UNITS: UNITS_IMPERIAL,
         CONF_DEPARTURE_TIME: "test",
         CONF_TRAFFIC_MODEL: "best_guess",
         CONF_TRANSIT_MODE: "train",
diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py
index daedcfef4c1..73bea673880 100644
--- a/tests/components/google_travel_time/test_sensor.py
+++ b/tests/components/google_travel_time/test_sensor.py
@@ -10,6 +10,9 @@ from homeassistant.components.google_travel_time.const import (
     CONF_TRAVEL_MODE,
     DOMAIN,
 )
+from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
+from homeassistant.core import HomeAssistant
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
 
 from .const import MOCK_CONFIG
 
@@ -218,3 +221,35 @@ async def test_sensor_deprecation_warning(hass, caplog):
         "add mode to the options dictionary instead!"
     )
     assert wstr in caplog.text
+
+
+@pytest.mark.parametrize(
+    "unit_system, expected_unit_option",
+    [
+        (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC),
+        (IMPERIAL_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
+    ],
+)
+async def test_sensor_unit_system(
+    hass: HomeAssistant,
+    unit_system: UnitSystem,
+    expected_unit_option: str,
+) -> None:
+    """Test that sensor works."""
+    hass.config.units = unit_system
+
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        data=MOCK_CONFIG,
+        options={},
+        entry_id="test",
+    )
+    config_entry.add_to_hass(hass)
+    with patch("homeassistant.components.google_travel_time.sensor.Client"), patch(
+        "homeassistant.components.google_travel_time.sensor.distance_matrix"
+    ) as distance_matrix_mock:
+        await hass.config_entries.async_setup(config_entry.entry_id)
+        await hass.async_block_till_done()
+
+    distance_matrix_mock.assert_called_once()
+    assert distance_matrix_mock.call_args.kwargs["units"] == expected_unit_option
-- 
GitLab


From 3e0e84521762967d26f45134c28d9befe0f2eb12 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:14:12 +0200
Subject: [PATCH 455/985] Adjust distance unit check in geonetnz_volcano
 (#80244)

* Adjust distance unit check in geonetnz_volcano

* Use system compare

* Use is not ==
---
 homeassistant/components/geonetnz_volcano/config_flow.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py
index 21c9265c1dc..6de169c8602 100644
--- a/homeassistant/components/geonetnz_volcano/config_flow.py
+++ b/homeassistant/components/geonetnz_volcano/config_flow.py
@@ -13,6 +13,7 @@ from homeassistant.const import (
 )
 from homeassistant.core import callback
 from homeassistant.helpers import config_validation as cv
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN
 
@@ -57,7 +58,7 @@ class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         if identifier in configured_instances(self.hass):
             return await self._show_form({"base": "already_configured"})
 
-        if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+        if self.hass.config.units is IMPERIAL_SYSTEM:
             user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL
         else:
             user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC
-- 
GitLab


From a06bd04def248653f1eca5384329e549e2d076e6 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:21:23 +0200
Subject: [PATCH 456/985] Adjust get_distance_unit in mazda integration
 (#80233)

* Adjust get_distance_unit in mazda integration

* Use system compare with DistanceConverter

* Adjust rounding

* Use is not ==

* Reduce size of PR
---
 homeassistant/components/mazda/sensor.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py
index 715b274b6f5..b322c80b6e6 100644
--- a/homeassistant/components/mazda/sensor.py
+++ b/homeassistant/components/mazda/sensor.py
@@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
     PERCENTAGE,
@@ -22,7 +21,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import StateType
-from homeassistant.util.unit_system import UnitSystem
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM, UnitSystem
 
 from . import MazdaEntity
 from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@@ -50,9 +49,9 @@ class MazdaSensorEntityDescription(
     unit: Callable[[UnitSystem], str | None] | None = None
 
 
-def _get_distance_unit(unit_system):
+def _get_distance_unit(unit_system: UnitSystem) -> str:
     """Return the distance unit for the given unit system."""
-    if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL:
+    if unit_system is IMPERIAL_SYSTEM:
         return LENGTH_MILES
     return LENGTH_KILOMETERS
 
-- 
GitLab


From 689dcb02dd46dd849593b9bafb4ed1844977fbe4 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:21:38 +0200
Subject: [PATCH 457/985] Adjust distance unit check in gdacs (#80235)

* Adjust length unit check in gdacs

* Use system compare

* Use is not ==

* Apply suggestion

Co-authored-by: Erik Montnemery <erik@montnemery.com>

Co-authored-by: Erik Montnemery <erik@montnemery.com>
---
 homeassistant/components/gdacs/__init__.py     |  4 ++--
 homeassistant/components/gdacs/geo_location.py | 11 ++++-------
 2 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py
index d4fe4177aed..71c06033469 100644
--- a/homeassistant/components/gdacs/__init__.py
+++ b/homeassistant/components/gdacs/__init__.py
@@ -11,7 +11,6 @@ from homeassistant.const import (
     CONF_LONGITUDE,
     CONF_RADIUS,
     CONF_SCAN_INTERVAL,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
 )
@@ -21,6 +20,7 @@ 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_conversion import DistanceConverter
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import (
     CONF_CATEGORIES,
@@ -88,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
     feeds = hass.data[DOMAIN].setdefault(FEED, {})
 
     radius = config_entry.data[CONF_RADIUS]
-    if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+    if hass.config.units is IMPERIAL_SYSTEM:
         radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS)
     # Create feed entity manager for all platforms.
     manager = GdacsFeedEntityManager(hass, config_entry, radius)
diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py
index 7e48cf0aa3a..b6083f2aa71 100644
--- a/homeassistant/components/gdacs/geo_location.py
+++ b/homeassistant/components/gdacs/geo_location.py
@@ -10,16 +10,13 @@ from aio_georss_gdacs.feed_entry import GdacsFeedEntry
 
 from homeassistant.components.geo_location import GeolocationEvent
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    LENGTH_KILOMETERS,
-    LENGTH_MILES,
-)
+from homeassistant.const import LENGTH_KILOMETERS, LENGTH_MILES
 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_conversion import DistanceConverter
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from . import GdacsFeedEntityManager
 from .const import DEFAULT_ICON, DOMAIN, FEED
@@ -110,7 +107,7 @@ class GdacsEvent(GeolocationEvent):
 
     async def async_added_to_hass(self) -> None:
         """Call when entity is added to hass."""
-        if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+        if self.hass.config.units is IMPERIAL_SYSTEM:
             self._attr_unit_of_measurement = LENGTH_MILES
         self._remove_signal_delete = async_dispatcher_connect(
             self.hass, f"gdacs_delete_{self._external_id}", self._delete_callback
@@ -152,7 +149,7 @@ class GdacsEvent(GeolocationEvent):
             event_name = f"{feed_entry.country} ({feed_entry.event_id})"
         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:
+        if self.hass.config.units is IMPERIAL_SYSTEM:
             self._attr_distance = DistanceConverter.convert(
                 feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES
             )
-- 
GitLab


From faeb663dfe27d0e06c52441f6db660b6af3e4c1c Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:21:58 +0200
Subject: [PATCH 458/985] Adjust distance unit check in geonetnz_quakes
 (#80236)

* Adjust length unit check in geonetnz_quakes

* Use system compare

* Use is not ==

* Apply suggestion

Co-authored-by: Erik Montnemery <erik@montnemery.com>

Co-authored-by: Erik Montnemery <erik@montnemery.com>
---
 homeassistant/components/geonetnz_quakes/__init__.py |  4 ++--
 .../components/geonetnz_quakes/geo_location.py       | 12 ++++--------
 2 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py
index e4d766fa979..bef0ee4c1fd 100644
--- a/homeassistant/components/geonetnz_quakes/__init__.py
+++ b/homeassistant/components/geonetnz_quakes/__init__.py
@@ -11,7 +11,6 @@ from homeassistant.const import (
     CONF_LONGITUDE,
     CONF_RADIUS,
     CONF_SCAN_INTERVAL,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
 )
@@ -21,6 +20,7 @@ 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_conversion import DistanceConverter
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import (
     CONF_MINIMUM_MAGNITUDE,
@@ -95,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
     feeds = hass.data[DOMAIN].setdefault(FEED, {})
 
     radius = config_entry.data[CONF_RADIUS]
-    if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+    if hass.config.units is IMPERIAL_SYSTEM:
         radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS)
     # Create feed entity manager for all platforms.
     manager = GeonetnzQuakesFeedEntityManager(hass, config_entry, radius)
diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py
index c6c872ba828..bd382f1767d 100644
--- a/homeassistant/components/geonetnz_quakes/geo_location.py
+++ b/homeassistant/components/geonetnz_quakes/geo_location.py
@@ -9,17 +9,13 @@ from aio_geojson_geonetnz_quakes.feed_entry import GeonetnzQuakesFeedEntry
 
 from homeassistant.components.geo_location import GeolocationEvent
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    ATTR_TIME,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    LENGTH_KILOMETERS,
-    LENGTH_MILES,
-)
+from homeassistant.const import ATTR_TIME, LENGTH_KILOMETERS, LENGTH_MILES
 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_conversion import DistanceConverter
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from . import GeonetnzQuakesFeedEntityManager
 from .const import DOMAIN, FEED
@@ -97,7 +93,7 @@ class GeonetnzQuakesEvent(GeolocationEvent):
 
     async def async_added_to_hass(self) -> None:
         """Call when entity is added to hass."""
-        if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+        if self.hass.config.units is IMPERIAL_SYSTEM:
             self._attr_unit_of_measurement = LENGTH_MILES
         self._remove_signal_delete = async_dispatcher_connect(
             self.hass,
@@ -140,7 +136,7 @@ class GeonetnzQuakesEvent(GeolocationEvent):
         """Update the internal state from the provided feed entry."""
         self._attr_name = feed_entry.title
         # Convert distance if not metric system.
-        if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL:
+        if self.hass.config.units is IMPERIAL_SYSTEM:
             self._attr_distance = DistanceConverter.convert(
                 feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES
             )
-- 
GitLab


From 1445e08090c5a0ae92be1099308a4bc07c31b7b0 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:22:11 +0200
Subject: [PATCH 459/985] Replace `is_metric` with `is METRIC_SYSTEM` (#80262)

---
 homeassistant/components/accuweather/__init__.py   | 3 ++-
 homeassistant/components/accuweather/sensor.py     | 5 +++--
 homeassistant/components/accuweather/weather.py    | 3 ++-
 homeassistant/components/airthings_ble/__init__.py | 3 ++-
 homeassistant/components/airthings_ble/sensor.py   | 3 ++-
 homeassistant/components/bloomsky/__init__.py      | 3 ++-
 homeassistant/components/darksky/sensor.py         | 3 ++-
 homeassistant/components/fitbit/sensor.py          | 7 ++++---
 homeassistant/components/life360/coordinator.py    | 3 ++-
 homeassistant/components/magicseaweed/sensor.py    | 3 ++-
 homeassistant/components/met/__init__.py           | 3 ++-
 homeassistant/components/met/weather.py            | 8 ++++++--
 homeassistant/components/mold_indicator/sensor.py  | 3 ++-
 homeassistant/components/mysensors/climate.py      | 5 ++++-
 homeassistant/components/mysensors/gateway.py      | 3 ++-
 homeassistant/components/mysensors/sensor.py       | 3 ++-
 homeassistant/components/noaa_tides/sensor.py      | 3 ++-
 homeassistant/components/roomba/irobot_base.py     | 3 ++-
 homeassistant/components/shelly/__init__.py        | 3 ++-
 19 files changed, 47 insertions(+), 23 deletions(-)

diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py
index 0484dd0c8e7..89af284f873 100644
--- a/homeassistant/components/accuweather/__init__.py
+++ b/homeassistant/components/accuweather/__init__.py
@@ -17,6 +17,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.device_registry import DeviceEntryType
 from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER
 
@@ -116,7 +117,7 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
                 current = await self.accuweather.async_get_current_conditions()
                 forecast = (
                     await self.accuweather.async_get_forecast(
-                        metric=self.hass.config.units.is_metric
+                        metric=self.hass.config.units is METRIC_SYSTEM
                     )
                     if self.forecast
                     else {}
diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py
index 4347bca5863..0238026c1a0 100644
--- a/homeassistant/components/accuweather/sensor.py
+++ b/homeassistant/components/accuweather/sensor.py
@@ -30,6 +30,7 @@ from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import StateType
 from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from . import AccuWeatherDataUpdateCoordinator
 from .const import (
@@ -412,12 +413,12 @@ class AccuWeatherSensor(
             self._attr_unique_id = (
                 f"{coordinator.location_key}-{description.key}".lower()
             )
-        if self.coordinator.hass.config.units.is_metric:
+        if self.coordinator.hass.config.units is METRIC_SYSTEM:
             self._unit_system = API_METRIC
         else:
             self._unit_system = API_IMPERIAL
         self._attr_native_unit_of_measurement = self.entity_description.unit_fn(
-            self.coordinator.hass.config.units.is_metric
+            self.coordinator.hass.config.units is METRIC_SYSTEM
         )
         self._attr_device_info = coordinator.device_info
         if forecast_day is not None:
diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py
index 2bbbe7b9160..82db25288b8 100644
--- a/homeassistant/components/accuweather/weather.py
+++ b/homeassistant/components/accuweather/weather.py
@@ -33,6 +33,7 @@ from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.update_coordinator import CoordinatorEntity
 from homeassistant.util.dt import utc_from_timestamp
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from . import AccuWeatherDataUpdateCoordinator
 from .const import (
@@ -70,7 +71,7 @@ class AccuWeatherEntity(
         # Coordinator data is used also for sensors which don't have units automatically
         # converted, hence the weather entity's native units follow the configured unit
         # system
-        if coordinator.hass.config.units.is_metric:
+        if coordinator.hass.config.units is METRIC_SYSTEM:
             self._attr_native_precipitation_unit = LENGTH_MILLIMETERS
             self._attr_native_pressure_unit = PRESSURE_HPA
             self._attr_native_temperature_unit = TEMP_CELSIUS
diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py
index 4e066ea8447..d7e6bddbcd4 100644
--- a/homeassistant/components/airthings_ble/__init__.py
+++ b/homeassistant/components/airthings_ble/__init__.py
@@ -12,6 +12,7 @@ from homeassistant.const import Platform
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
 
@@ -26,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     address = entry.unique_id
 
     elevation = hass.config.elevation
-    is_metric = hass.config.units.is_metric
+    is_metric = hass.config.units is METRIC_SYSTEM
     assert address is not None
 
     ble_device = bluetooth.async_ble_device_from_address(hass, address)
diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py
index 0f0ca2e4af5..37b5ce6160e 100644
--- a/homeassistant/components/airthings_ble/sensor.py
+++ b/homeassistant/components/airthings_ble/sensor.py
@@ -29,6 +29,7 @@ from homeassistant.helpers.update_coordinator import (
     CoordinatorEntity,
     DataUpdateCoordinator,
 )
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
 
@@ -112,7 +113,7 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up the Airthings BLE sensors."""
-    is_metric = hass.config.units.is_metric
+    is_metric = hass.config.units is METRIC_SYSTEM
 
     coordinator: DataUpdateCoordinator[AirthingsDevice] = hass.data[DOMAIN][
         entry.entry_id
diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py
index ed2ce1ebc70..5b069cacdb3 100644
--- a/homeassistant/components/bloomsky/__init__.py
+++ b/homeassistant/components/bloomsky/__init__.py
@@ -12,6 +12,7 @@ from homeassistant.helpers import discovery
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.typing import ConfigType
 from homeassistant.util import Throttle
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -33,7 +34,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
     api_key = config[DOMAIN][CONF_API_KEY]
 
     try:
-        bloomsky = BloomSky(api_key, hass.config.units.is_metric)
+        bloomsky = BloomSky(api_key, hass.config.units is METRIC_SYSTEM)
     except RuntimeError:
         return False
 
diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py
index ccd4516e39c..663b92a7ccf 100644
--- a/homeassistant/components/darksky/sensor.py
+++ b/homeassistant/components/darksky/sensor.py
@@ -45,6 +45,7 @@ import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 from homeassistant.util import Throttle
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -584,7 +585,7 @@ def setup_platform(
 
     if CONF_UNITS in config:
         units = config[CONF_UNITS]
-    elif hass.config.units.is_metric:
+    elif hass.config.units is METRIC_SYSTEM:
         units = "si"
     else:
         units = "us"
diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py
index f9dc74fc328..c4e9970691d 100644
--- a/homeassistant/components/fitbit/sensor.py
+++ b/homeassistant/components/fitbit/sensor.py
@@ -32,6 +32,7 @@ from homeassistant.helpers.icon import icon_for_battery_level
 from homeassistant.helpers.network import NoURLAvailableError, get_url
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 from homeassistant.util.json import load_json, save_json
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import (
     ATTR_ACCESS_TOKEN,
@@ -199,7 +200,7 @@ def setup_platform(
         if (unit_system := config[CONF_UNIT_SYSTEM]) == "default":
             authd_client.system = user_profile["locale"]
             if authd_client.system != "en_GB":
-                if hass.config.units.is_metric:
+                if hass.config.units is METRIC_SYSTEM:
                     authd_client.system = "metric"
                 else:
                     authd_client.system = "en_US"
@@ -215,7 +216,7 @@ def setup_platform(
                 user_profile,
                 config_path,
                 description,
-                hass.config.units.is_metric,
+                hass.config.units is METRIC_SYSTEM,
                 clock_format,
             )
             for description in FITBIT_RESOURCES_LIST
@@ -229,7 +230,7 @@ def setup_platform(
                         user_profile,
                         config_path,
                         FITBIT_RESOURCE_BATTERY,
-                        hass.config.units.is_metric,
+                        hass.config.units is METRIC_SYSTEM,
                         clock_format,
                         dev_extra,
                     )
diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py
index edb86e9727a..ba3e7672cdb 100644
--- a/homeassistant/components/life360/coordinator.py
+++ b/homeassistant/components/life360/coordinator.py
@@ -23,6 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 from homeassistant.util import dt as dt_util
 from homeassistant.util.unit_conversion import DistanceConverter
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import (
     COMM_TIMEOUT,
@@ -206,7 +207,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]):
                     address = address1 or address2
 
                 speed = max(0, float(loc["speed"]) * SPEED_FACTOR_MPH)
-                if self._hass.config.units.is_metric:
+                if self._hass.config.units is METRIC_SYSTEM:
                     speed = DistanceConverter.convert(
                         speed, LENGTH_MILES, LENGTH_KILOMETERS
                     )
diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py
index b0b2e92ada5..13bc8b9be2e 100644
--- a/homeassistant/components/magicseaweed/sensor.py
+++ b/homeassistant/components/magicseaweed/sensor.py
@@ -24,6 +24,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 from homeassistant.util import Throttle
 import homeassistant.util.dt as dt_util
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -92,7 +93,7 @@ def setup_platform(
 
     if CONF_UNITS in config:
         units = config.get(CONF_UNITS)
-    elif hass.config.units.is_metric:
+    elif hass.config.units is METRIC_SYSTEM:
         units = UNITS[0]
     else:
         units = UNITS[2]
diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py
index 2857c057482..fc79ac4c60f 100644
--- a/homeassistant/components/met/__init__.py
+++ b/homeassistant/components/met/__init__.py
@@ -26,6 +26,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 from homeassistant.util import dt as dt_util
 from homeassistant.util.unit_conversion import DistanceConverter
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import (
     CONF_TRACK_HOME,
@@ -95,7 +96,7 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator["MetWeatherData"]):
         """Initialize global Met data updater."""
         self._unsub_track_home: Callable[[], None] | None = None
         self.weather = MetWeatherData(
-            hass, config_entry.data, hass.config.units.is_metric
+            hass, config_entry.data, hass.config.units is METRIC_SYSTEM
         )
         self.weather.set_coordinates()
 
diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py
index c843be73fe7..2aa70929795 100644
--- a/homeassistant/components/met/weather.py
+++ b/homeassistant/components/met/weather.py
@@ -30,6 +30,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType
 from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.update_coordinator import CoordinatorEntity
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from . import MetDataUpdateCoordinator
 from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP
@@ -51,10 +52,13 @@ async def async_setup_entry(
     async_add_entities(
         [
             MetWeather(
-                coordinator, config_entry.data, hass.config.units.is_metric, False
+                coordinator,
+                config_entry.data,
+                hass.config.units is METRIC_SYSTEM,
+                False,
             ),
             MetWeather(
-                coordinator, config_entry.data, hass.config.units.is_metric, True
+                coordinator, config_entry.data, hass.config.units is METRIC_SYSTEM, True
             ),
         ]
     )
diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py
index 5685e76fac0..f10ddbe1ab0 100644
--- a/homeassistant/components/mold_indicator/sensor.py
+++ b/homeassistant/components/mold_indicator/sensor.py
@@ -23,6 +23,7 @@ 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
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -67,7 +68,7 @@ async def async_setup_platform(
         [
             MoldIndicator(
                 name,
-                hass.config.units.is_metric,
+                hass.config.units is METRIC_SYSTEM,
                 indoor_temp_sensor,
                 outdoor_temp_sensor,
                 indoor_humidity_sensor,
diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py
index 435bf2ffddb..44468d8db4c 100644
--- a/homeassistant/components/mysensors/climate.py
+++ b/homeassistant/components/mysensors/climate.py
@@ -20,6 +20,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .. import mysensors
 from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
@@ -94,7 +95,9 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
     @property
     def temperature_unit(self) -> str:
         """Return the unit of measurement."""
-        return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
+        return (
+            TEMP_CELSIUS if self.hass.config.units is METRIC_SYSTEM else TEMP_FAHRENHEIT
+        )
 
     @property
     def current_temperature(self) -> float | None:
diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py
index c93e0380757..eace7b355d7 100644
--- a/homeassistant/components/mysensors/gateway.py
+++ b/homeassistant/components/mysensors/gateway.py
@@ -22,6 +22,7 @@ from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import EVENT_HOMEASSISTANT_STOP
 from homeassistant.core import Event, HomeAssistant, callback
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import (
     CONF_BAUD_RATE,
@@ -220,7 +221,7 @@ async def _get_gateway(
             protocol_version=version,
         )
     gateway.event_callback = event_callback
-    gateway.metric = hass.config.units.is_metric
+    gateway.metric = hass.config.units is METRIC_SYSTEM
 
     if persistence:
         await gateway.start_persistence()
diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py
index f21d343f9c3..5bafed04353 100644
--- a/homeassistant/components/mysensors/sensor.py
+++ b/homeassistant/components/mysensors/sensor.py
@@ -35,6 +35,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .. import mysensors
 from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
@@ -242,7 +243,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity):
             return custom_unit
 
         if set_req(self.value_type) == set_req.V_TEMP:
-            if self.hass.config.units.is_metric:
+            if self.hass.config.units is METRIC_SYSTEM:
                 return TEMP_CELSIUS
             return TEMP_FAHRENHEIT
 
diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py
index 49635973cf8..8de16055714 100644
--- a/homeassistant/components/noaa_tides/sensor.py
+++ b/homeassistant/components/noaa_tides/sensor.py
@@ -20,6 +20,7 @@ from homeassistant.exceptions import PlatformNotReady
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -57,7 +58,7 @@ def setup_platform(
 
     if CONF_UNIT_SYSTEM in config:
         unit_system = config[CONF_UNIT_SYSTEM]
-    elif hass.config.units.is_metric:
+    elif hass.config.units is METRIC_SYSTEM:
         unit_system = UNIT_SYSTEMS[1]
     else:
         unit_system = UNIT_SYSTEMS[0]
diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py
index f443f72279f..8ec91acf965 100644
--- a/homeassistant/components/roomba/irobot_base.py
+++ b/homeassistant/components/roomba/irobot_base.py
@@ -17,6 +17,7 @@ from homeassistant.const import STATE_IDLE, STATE_PAUSED
 import homeassistant.helpers.device_registry as dr
 from homeassistant.helpers.entity import DeviceInfo, Entity
 import homeassistant.util.dt as dt_util
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from . import roomba_reported_state
 from .const import DOMAIN
@@ -221,7 +222,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity):
 
         if cleaned_area := mission_state.get("sqft", 0):  # Imperial
             # Convert to m2 if the unit_system is set to metric
-            if self.hass.config.units.is_metric:
+            if self.hass.config.units is METRIC_SYSTEM:
                 cleaned_area = round(cleaned_area * 0.0929)
 
         return (cleaning_time, cleaned_area)
diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index b2b927e48b5..bc7a3f771f7 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -20,6 +20,7 @@ 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 (
     AIOSHELLY_DEVICE_TIMEOUT_SEC,
@@ -108,7 +109,7 @@ 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 else "F"
+    temperature_unit = "C" if hass.config.units is METRIC_SYSTEM else "F"
 
     options = aioshelly.common.ConnectionOptions(
         entry.data[CONF_HOST],
-- 
GitLab


From 02d0b8eef653ef925f79877e70463dc2bfae17d8 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:22:27 +0200
Subject: [PATCH 460/985] Adjust distance unit check in waze travel time
 (#80241)

* Adjust distance unit check in waze travel time

* Reduce size of PR

* Use system compare

* Use is not ==
---
 homeassistant/components/waze_travel_time/sensor.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py
index 85b6acdc19a..66ee5376266 100644
--- a/homeassistant/components/waze_travel_time/sensor.py
+++ b/homeassistant/components/waze_travel_time/sensor.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
     CONF_NAME,
     CONF_REGION,
     CONF_UNIT_SYSTEM_IMPERIAL,
+    CONF_UNIT_SYSTEM_METRIC,
     EVENT_HOMEASSISTANT_STARTED,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
@@ -28,6 +29,7 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.location import find_coordinates
 from homeassistant.util.unit_conversion import DistanceConverter
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import (
     CONF_AVOID_FERRIES,
@@ -63,11 +65,13 @@ async def async_setup_entry(
     defaults = {
         CONF_REALTIME: DEFAULT_REALTIME,
         CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE,
-        CONF_UNITS: hass.config.units.name,
+        CONF_UNITS: CONF_UNIT_SYSTEM_METRIC,
         CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES,
         CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS,
         CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS,
     }
+    if hass.config.units is IMPERIAL_SYSTEM:
+        defaults[CONF_UNITS] = CONF_UNIT_SYSTEM_IMPERIAL
 
     if not config_entry.options:
         new_data = config_entry.data.copy()
-- 
GitLab


From 4c8f8c2e61135b0a7db2708b0c4776ea656b09e9 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:22:48 +0200
Subject: [PATCH 461/985] Adjust distance unit check in here travel time
 (#80243)

* Adjust distance unit check in here travel time

* Add tests

* Use system compare

* Use is not ==
---
 .../here_travel_time/config_flow.py           | 17 ++++++++++-----
 .../here_travel_time/test_config_flow.py      | 21 ++++++++++++++++++-
 2 files changed, 32 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py
index 09faf95177d..54f164c43a7 100644
--- a/homeassistant/components/here_travel_time/config_flow.py
+++ b/homeassistant/components/here_travel_time/config_flow.py
@@ -15,6 +15,8 @@ from homeassistant.const import (
     CONF_MODE,
     CONF_NAME,
     CONF_UNIT_SYSTEM,
+    CONF_UNIT_SYSTEM_IMPERIAL,
+    CONF_UNIT_SYSTEM_METRIC,
 )
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.data_entry_flow import FlowResult
@@ -24,6 +26,7 @@ from homeassistant.helpers.selector import (
     LocationSelector,
     TimeSelector,
 )
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import (
     CONF_ARRIVAL_TIME,
@@ -88,13 +91,16 @@ def get_user_step_schema(data: dict[str, Any]) -> vol.Schema:
 
 def default_options(hass: HomeAssistant) -> dict[str, str | None]:
     """Get the default options."""
-    return {
+    default = {
         CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
         CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
         CONF_ARRIVAL_TIME: None,
         CONF_DEPARTURE_TIME: None,
-        CONF_UNIT_SYSTEM: hass.config.units.name,
+        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
     }
+    if hass.config.units is IMPERIAL_SYSTEM:
+        default[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL
+    return default
 
 
 class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -255,24 +261,25 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow):
                 menu_options=["departure_time", "no_time"],
             )
 
+        defaults = default_options(self.hass)
         schema = vol.Schema(
             {
                 vol.Optional(
                     CONF_TRAFFIC_MODE,
                     default=self.config_entry.options.get(
-                        CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED
+                        CONF_TRAFFIC_MODE, defaults[CONF_TRAFFIC_MODE]
                     ),
                 ): vol.In(TRAFFIC_MODES),
                 vol.Optional(
                     CONF_ROUTE_MODE,
                     default=self.config_entry.options.get(
-                        CONF_ROUTE_MODE, ROUTE_MODE_FASTEST
+                        CONF_ROUTE_MODE, defaults[CONF_ROUTE_MODE]
                     ),
                 ): vol.In(ROUTE_MODES),
                 vol.Optional(
                     CONF_UNIT_SYSTEM,
                     default=self.config_entry.options.get(
-                        CONF_UNIT_SYSTEM, self.hass.config.units.name
+                        CONF_UNIT_SYSTEM, defaults[CONF_UNIT_SYSTEM]
                     ),
                 ): vol.In(UNITS),
             }
diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py
index b56f97a8053..1777258c6e9 100644
--- a/tests/components/here_travel_time/test_config_flow.py
+++ b/tests/components/here_travel_time/test_config_flow.py
@@ -31,6 +31,7 @@ from homeassistant.const import (
     CONF_UNIT_SYSTEM_METRIC,
 )
 from homeassistant.core import HomeAssistant
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
 
 from .const import (
     API_KEY,
@@ -227,10 +228,21 @@ async def test_step_destination_coordinates(
 
 
 @pytest.mark.usefixtures("valid_response")
+@pytest.mark.parametrize(
+    "unit_system, expected_unit_option",
+    [
+        (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC),
+        (IMPERIAL_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
+    ],
+)
 async def test_step_destination_entity(
-    hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult
+    hass: HomeAssistant,
+    origin_step_result: data_entry_flow.FlowResult,
+    unit_system: UnitSystem,
+    expected_unit_option: str,
 ) -> None:
     """Test the origin coordinates step."""
+    hass.config.units = unit_system
     menu_result = await hass.config_entries.flow.async_configure(
         origin_step_result["flow_id"], {"next_step_id": "destination_entity"}
     )
@@ -250,6 +262,13 @@ async def test_step_destination_entity(
         CONF_DESTINATION_ENTITY_ID: "zone.home",
         CONF_MODE: TRAVEL_MODE_CAR,
     }
+    assert entry.options == {
+        CONF_UNIT_SYSTEM: expected_unit_option,
+        CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
+        CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED,
+        CONF_ARRIVAL_TIME: None,
+        CONF_DEPARTURE_TIME: None,
+    }
 
 
 async def test_form_invalid_auth(hass: HomeAssistant) -> None:
-- 
GitLab


From f502f8c93166148bff8c9c9d46522504b5ad7397 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:35:06 +0200
Subject: [PATCH 462/985] Use attributes in coinbase sensor (#80086)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
---
 homeassistant/components/coinbase/sensor.py | 164 ++++++++------------
 1 file changed, 64 insertions(+), 100 deletions(-)

diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py
index d1e25dcf2a0..e264fed0215 100644
--- a/homeassistant/components/coinbase/sensor.py
+++ b/homeassistant/components/coinbase/sensor.py
@@ -5,12 +5,12 @@ import logging
 
 from homeassistant.components.sensor import SensorEntity, SensorStateClass
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ATTRIBUTION
 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 CoinbaseData
 from .const import (
     API_ACCOUNT_AMOUNT,
     API_ACCOUNT_BALANCE,
@@ -51,24 +51,24 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up Coinbase sensor platform."""
-    instance = hass.data[DOMAIN][config_entry.entry_id]
+    instance: CoinbaseData = hass.data[DOMAIN][config_entry.entry_id]
 
     entities: list[SensorEntity] = []
 
-    provided_currencies = [
+    provided_currencies: list[str] = [
         account[API_ACCOUNT_CURRENCY]
         for account in instance.accounts
         if account[API_RESOURCE_TYPE] != API_TYPE_VAULT
     ]
 
-    desired_currencies = []
+    desired_currencies: list[str] = []
 
     if CONF_CURRENCIES in config_entry.options:
         desired_currencies = config_entry.options[CONF_CURRENCIES]
 
-    exchange_base_currency = instance.exchange_rates[API_ACCOUNT_CURRENCY]
+    exchange_base_currency: str = instance.exchange_rates[API_ACCOUNT_CURRENCY]
 
-    exchange_precision = config_entry.options.get(
+    exchange_precision: int = config_entry.options.get(
         CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_PRECISION_DEFAULT
     )
 
@@ -83,6 +83,7 @@ async def async_setup_entry(
         entities.append(AccountSensor(instance, currency))
 
     if CONF_EXCHANGE_RATES in config_entry.options:
+        rate: str
         for rate in config_entry.options[CONF_EXCHANGE_RATES]:
             entities.append(
                 ExchangeRateSensor(
@@ -96,29 +97,36 @@ async def async_setup_entry(
 class AccountSensor(SensorEntity):
     """Representation of a Coinbase.com sensor."""
 
-    def __init__(self, coinbase_data, currency):
+    _attr_attribution = ATTRIBUTION
+
+    def __init__(self, coinbase_data: CoinbaseData, currency: str) -> None:
         """Initialize the sensor."""
         self._coinbase_data = coinbase_data
         self._currency = currency
         for account in coinbase_data.accounts:
             if (
-                account[API_ACCOUNT_CURRENCY] == currency
-                and account[API_RESOURCE_TYPE] != API_TYPE_VAULT
+                account[API_ACCOUNT_CURRENCY] != currency
+                or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
             ):
-                self._name = f"Coinbase {account[API_ACCOUNT_NAME]}"
-                self._id = (
-                    f"coinbase-{account[API_ACCOUNT_ID]}-wallet-"
-                    f"{account[API_ACCOUNT_CURRENCY]}"
-                )
-                self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
-                self._unit_of_measurement = account[API_ACCOUNT_CURRENCY]
-                self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
-                    API_ACCOUNT_AMOUNT
-                ]
-                self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
-                    API_ACCOUNT_CURRENCY
-                ]
-                break
+                continue
+            self._attr_name = f"Coinbase {account[API_ACCOUNT_NAME]}"
+            self._attr_unique_id = (
+                f"coinbase-{account[API_ACCOUNT_ID]}-wallet-"
+                f"{account[API_ACCOUNT_CURRENCY]}"
+            )
+            self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
+            self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY]
+            self._attr_icon = CURRENCY_ICONS.get(
+                account[API_ACCOUNT_CURRENCY], DEFAULT_COIN_ICON
+            )
+            self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
+                API_ACCOUNT_AMOUNT
+            ]
+            self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
+                API_ACCOUNT_CURRENCY
+            ]
+            break
+
         self._attr_state_class = SensorStateClass.TOTAL
         self._attr_device_info = DeviceInfo(
             configuration_url="https://www.coinbase.com/settings/api",
@@ -129,35 +137,9 @@ class AccountSensor(SensorEntity):
         )
 
     @property
-    def name(self):
-        """Return the name of the sensor."""
-        return self._name
-
-    @property
-    def unique_id(self):
-        """Return the Unique ID of the sensor."""
-        return self._id
-
-    @property
-    def native_value(self):
-        """Return the state of the sensor."""
-        return self._state
-
-    @property
-    def native_unit_of_measurement(self):
-        """Return the unit of measurement this sensor expresses itself in."""
-        return self._unit_of_measurement
-
-    @property
-    def icon(self):
-        """Return the icon to use in the frontend, if any."""
-        return CURRENCY_ICONS.get(self._unit_of_measurement, DEFAULT_COIN_ICON)
-
-    @property
-    def extra_state_attributes(self):
+    def extra_state_attributes(self) -> dict[str, str]:
         """Return the state attributes of the sensor."""
         return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._native_currency}",
         }
 
@@ -166,34 +148,46 @@ class AccountSensor(SensorEntity):
         self._coinbase_data.update()
         for account in self._coinbase_data.accounts:
             if (
-                account[API_ACCOUNT_CURRENCY] == self._currency
-                and account[API_RESOURCE_TYPE] != API_TYPE_VAULT
+                account[API_ACCOUNT_CURRENCY] != self._currency
+                or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
             ):
-                self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
-                self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
-                    API_ACCOUNT_AMOUNT
-                ]
-                self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
-                    API_ACCOUNT_CURRENCY
-                ]
-                break
+                continue
+            self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
+            self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
+                API_ACCOUNT_AMOUNT
+            ]
+            self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
+                API_ACCOUNT_CURRENCY
+            ]
+            break
 
 
 class ExchangeRateSensor(SensorEntity):
     """Representation of a Coinbase.com sensor."""
 
-    def __init__(self, coinbase_data, exchange_currency, exchange_base, precision):
+    _attr_attribution = ATTRIBUTION
+
+    def __init__(
+        self,
+        coinbase_data: CoinbaseData,
+        exchange_currency: str,
+        exchange_base: str,
+        precision: int,
+    ) -> None:
         """Initialize the sensor."""
         self._coinbase_data = coinbase_data
-        self.currency = exchange_currency
-        self._name = f"{exchange_currency} Exchange Rate"
-        self._id = f"coinbase-{coinbase_data.user_id}-xe-{exchange_currency}"
+        self._currency = exchange_currency
+        self._attr_name = f"{exchange_currency} Exchange Rate"
+        self._attr_unique_id = (
+            f"coinbase-{coinbase_data.user_id}-xe-{exchange_currency}"
+        )
         self._precision = precision
-        self._state = round(
-            1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]),
-            self._precision,
+        self._attr_icon = CURRENCY_ICONS.get(exchange_currency, DEFAULT_COIN_ICON)
+        self._attr_native_value = round(
+            1 / float(coinbase_data.exchange_rates[API_RATES][exchange_currency]),
+            precision,
         )
-        self._unit_of_measurement = exchange_base
+        self._attr_native_unit_of_measurement = exchange_base
         self._attr_state_class = SensorStateClass.MEASUREMENT
         self._attr_device_info = DeviceInfo(
             configuration_url="https://www.coinbase.com/settings/api",
@@ -203,40 +197,10 @@ class ExchangeRateSensor(SensorEntity):
             name=f"Coinbase {self._coinbase_data.user_id[-4:]}",
         )
 
-    @property
-    def name(self):
-        """Return the name of the sensor."""
-        return self._name
-
-    @property
-    def unique_id(self):
-        """Return the unique ID of the sensor."""
-        return self._id
-
-    @property
-    def native_value(self):
-        """Return the state of the sensor."""
-        return self._state
-
-    @property
-    def native_unit_of_measurement(self):
-        """Return the unit of measurement this sensor expresses itself in."""
-        return self._unit_of_measurement
-
-    @property
-    def icon(self):
-        """Return the icon to use in the frontend, if any."""
-        return CURRENCY_ICONS.get(self.currency, DEFAULT_COIN_ICON)
-
-    @property
-    def extra_state_attributes(self):
-        """Return the state attributes of the sensor."""
-        return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
     def update(self) -> None:
         """Get the latest state of the sensor."""
         self._coinbase_data.update()
-        self._state = round(
-            1 / float(self._coinbase_data.exchange_rates.rates[self.currency]),
+        self._attr_native_value = round(
+            1 / float(self._coinbase_data.exchange_rates.rates[self._currency]),
             self._precision,
         )
-- 
GitLab


From 4769ec8c76b91fad5178518b4433dea63bed50f5 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:51:47 +0200
Subject: [PATCH 463/985] Replace `not is_metric` with `is IMPERIAL_SYSTEM`
 (#80266)

---
 homeassistant/components/citybikes/sensor.py   | 3 ++-
 homeassistant/components/nissan_leaf/sensor.py | 5 +++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py
index 5074378adca..25716b67464 100644
--- a/homeassistant/components/citybikes/sensor.py
+++ b/homeassistant/components/citybikes/sensor.py
@@ -37,6 +37,7 @@ from homeassistant.helpers.event import async_track_time_interval
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 from homeassistant.util import location
 from homeassistant.util.unit_conversion import DistanceConverter
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -170,7 +171,7 @@ async def async_setup_platform(
     stations_list = set(config.get(CONF_STATIONS_LIST, []))
     radius = config.get(CONF_RADIUS, 0)
     name = config[CONF_NAME]
-    if not hass.config.units.is_metric:
+    if hass.config.units is IMPERIAL_SYSTEM:
         radius = DistanceConverter.convert(radius, LENGTH_FEET, LENGTH_METERS)
 
     # Create a single instance of CityBikesNetworks.
diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py
index 0e7cc0c00cd..4543467f932 100644
--- a/homeassistant/components/nissan_leaf/sensor.py
+++ b/homeassistant/components/nissan_leaf/sensor.py
@@ -13,6 +13,7 @@ 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_conversion import DistanceConverter
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from . import LeafEntity
 from .const import (
@@ -122,7 +123,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity):
         if ret is None:
             return None
 
-        if not self.car.hass.config.units.is_metric or self.car.force_miles:
+        if self.car.hass.config.units is IMPERIAL_SYSTEM or self.car.force_miles:
             ret = DistanceConverter.convert(ret, LENGTH_KILOMETERS, LENGTH_MILES)
 
         return round(ret)
@@ -130,7 +131,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity):
     @property
     def native_unit_of_measurement(self) -> str:
         """Battery range unit."""
-        if not self.car.hass.config.units.is_metric or self.car.force_miles:
+        if self.car.hass.config.units is IMPERIAL_SYSTEM or self.car.force_miles:
             return LENGTH_MILES
         return LENGTH_KILOMETERS
 
-- 
GitLab


From 7b47f12f48b2a431a2390734ef5f528a68b00d8d Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:53:24 +0200
Subject: [PATCH 464/985] Drop use of `is_metric` in ecowitt (#80267)

---
 homeassistant/components/ecowitt/sensor.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py
index a644cd3ca7a..3139a033289 100644
--- a/homeassistant/components/ecowitt/sensor.py
+++ b/homeassistant/components/ecowitt/sensor.py
@@ -40,6 +40,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import StateType
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
 
 from .const import DOMAIN
 from .entity import EcowittEntity
@@ -216,9 +217,9 @@ async def async_setup_entry(
             return
 
         # Ignore metrics that are not supported by the user's locale
-        if sensor.stype in _METRIC and not hass.config.units.is_metric:
+        if sensor.stype in _METRIC and hass.config.units is not METRIC_SYSTEM:
             return
-        if sensor.stype in _IMPERIAL and hass.config.units.is_metric:
+        if sensor.stype in _IMPERIAL and hass.config.units is not IMPERIAL_SYSTEM:
             return
         mapping = ECOWITT_SENSORS_MAPPING[sensor.stype]
 
-- 
GitLab


From 336ea2e7d1b37275ea8e44341b2fc4f76fa3b708 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:53:56 +0200
Subject: [PATCH 465/985] Drop use of `is_metric` in weather (#80272)

---
 homeassistant/components/weather/__init__.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py
index 2014c5b4eed..8ffced6d5d2 100644
--- a/homeassistant/components/weather/__init__.py
+++ b/homeassistant/components/weather/__init__.py
@@ -44,6 +44,7 @@ from homeassistant.util.unit_conversion import (
     SpeedConverter,
     TemperatureConverter,
 )
+from homeassistant.util.unit_system import METRIC_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -419,7 +420,9 @@ class WeatherEntity(Entity):
 
         Should not be set by integrations.
         """
-        return PRESSURE_HPA if self.hass.config.units.is_metric else PRESSURE_INHG
+        return (
+            PRESSURE_HPA if self.hass.config.units is METRIC_SYSTEM else PRESSURE_INHG
+        )
 
     @final
     @property
@@ -483,7 +486,7 @@ class WeatherEntity(Entity):
         """
         return (
             SPEED_KILOMETERS_PER_HOUR
-            if self.hass.config.units.is_metric
+            if self.hass.config.units is METRIC_SYSTEM
             else SPEED_MILES_PER_HOUR
         )
 
-- 
GitLab


From 0715b87934661f11812292512ce793b41f7a6105 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 12:54:43 +0200
Subject: [PATCH 466/985] Drop use of `is_metric` in nws (#80270)

---
 homeassistant/components/nws/sensor.py  | 3 ++-
 homeassistant/components/nws/weather.py | 1 -
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py
index 1e062bf3d36..ab34781f209 100644
--- a/homeassistant/components/nws/sensor.py
+++ b/homeassistant/components/nws/sensor.py
@@ -23,6 +23,7 @@ from homeassistant.util.unit_conversion import (
     PressureConverter,
     SpeedConverter,
 )
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from . import base_unique_id, device_info
 from .const import (
@@ -80,7 +81,7 @@ class NWSSensor(CoordinatorEntity, SensorEntity):
         self.entity_description = description
 
         self._attr_name = f"{station} {description.name}"
-        if not hass.config.units.is_metric:
+        if hass.config.units is IMPERIAL_SYSTEM:
             self._attr_native_unit_of_measurement = description.unit_convert
 
     @property
diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py
index 4684714d58c..7963c1161a9 100644
--- a/homeassistant/components/nws/weather.py
+++ b/homeassistant/components/nws/weather.py
@@ -107,7 +107,6 @@ class NWSWeather(WeatherEntity):
             self.coordinator_forecast = hass_data[COORDINATOR_FORECAST_HOURLY]
         self.station = self.nws.station
 
-        self.is_metric = units.is_metric
         self.mode = mode
 
         self.observation = None
-- 
GitLab


From 8dc3ff72c6ff6f9591151759465ac5d8a20e2b2f Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 13:03:17 +0200
Subject: [PATCH 467/985] Cleanup config deprecation warning (#80251)

---
 homeassistant/config.py | 15 +--------------
 tests/test_config.py    | 33 ++-------------------------------
 2 files changed, 3 insertions(+), 45 deletions(-)

diff --git a/homeassistant/config.py b/homeassistant/config.py
index 0f68e0bd235..e71b0dcd726 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -46,7 +46,6 @@ from .const import (
     CONF_UNIT_SYSTEM,
     CONF_UNIT_SYSTEM_IMPERIAL,
     LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
-    TEMP_CELSIUS,
     __version__,
 )
 from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback
@@ -204,7 +203,7 @@ CORE_CONFIG_SCHEMA = vol.All(
             CONF_LATITUDE: cv.latitude,
             CONF_LONGITUDE: cv.longitude,
             CONF_ELEVATION: vol.Coerce(int),
-            vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
+            vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
             CONF_UNIT_SYSTEM: cv.unit_system,
             CONF_TIME_ZONE: cv.time_zone,
             vol.Optional(CONF_INTERNAL_URL): cv.url,
@@ -607,18 +606,6 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non
             hac.units = IMPERIAL_SYSTEM
         else:
             hac.units = METRIC_SYSTEM
-    elif CONF_TEMPERATURE_UNIT in config:
-        unit = config[CONF_TEMPERATURE_UNIT]
-        hac.units = METRIC_SYSTEM if unit == TEMP_CELSIUS else IMPERIAL_SYSTEM
-        _LOGGER.warning(
-            "Found deprecated temperature unit in core "
-            "configuration expected unit system. Replace '%s: %s' "
-            "with '%s: %s'",
-            CONF_TEMPERATURE_UNIT,
-            unit,
-            CONF_UNIT_SYSTEM,
-            hac.units.name,
-        )
 
 
 def _log_pkg_error(package: str, component: str, config: dict, message: str) -> None:
diff --git a/tests/test_config.py b/tests/test_config.py
index 9b3f9d8755f..15c5f84bc42 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -22,7 +22,6 @@ from homeassistant.const import (
     CONF_LATITUDE,
     CONF_LONGITUDE,
     CONF_NAME,
-    CONF_TEMPERATURE_UNIT,
     CONF_UNIT_SYSTEM,
     CONF_UNIT_SYSTEM_IMPERIAL,
     CONF_UNIT_SYSTEM_METRIC,
@@ -538,34 +537,6 @@ async def test_loading_configuration(hass):
     assert hass.config.currency == "EUR"
 
 
-async def test_loading_configuration_temperature_unit(hass):
-    """Test backward compatibility when loading core config."""
-    await config_util.async_process_ha_core_config(
-        hass,
-        {
-            "latitude": 60,
-            "longitude": 50,
-            "elevation": 25,
-            "name": "Huis",
-            CONF_TEMPERATURE_UNIT: "C",
-            "time_zone": "America/New_York",
-            "external_url": "https://www.example.com",
-            "internal_url": "http://example.local",
-        },
-    )
-
-    assert hass.config.latitude == 60
-    assert hass.config.longitude == 50
-    assert hass.config.elevation == 25
-    assert hass.config.location_name == "Huis"
-    assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC
-    assert hass.config.time_zone == "America/New_York"
-    assert hass.config.external_url == "https://www.example.com"
-    assert hass.config.internal_url == "http://example.local"
-    assert hass.config.config_source is ConfigSource.YAML
-    assert hass.config.currency == "EUR"
-
-
 async def test_loading_configuration_default_media_dirs_docker(hass):
     """Test loading core config onto hass object."""
     with patch("homeassistant.config.is_docker_env", return_value=True):
@@ -591,7 +562,7 @@ async def test_loading_configuration_from_packages(hass):
             "longitude": -1,
             "elevation": 500,
             "name": "Huis",
-            CONF_TEMPERATURE_UNIT: "C",
+            CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
             "time_zone": "Europe/Madrid",
             "external_url": "https://www.example.com",
             "internal_url": "http://example.local",
@@ -615,7 +586,7 @@ async def test_loading_configuration_from_packages(hass):
                 "longitude": -1,
                 "elevation": 500,
                 "name": "Huis",
-                CONF_TEMPERATURE_UNIT: "C",
+                CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
                 "time_zone": "Europe/Madrid",
                 "packages": {"empty_package": None},
             },
-- 
GitLab


From a534c136a1f7dec97fbe27b67e5c40ec5596df88 Mon Sep 17 00:00:00 2001
From: hesselonline <hesselonline@users.noreply.github.com>
Date: Fri, 14 Oct 2022 13:09:00 +0200
Subject: [PATCH 468/985] Fix wallbox jwt issue (#79948)

* Bump Wallbox package

* remove debug message

* Force update of auth token by emptying it first

* Force token refresh by emptying token
Improve exception handling

* include tests

* Update __init__.py

* Removed the clearing ot jwt token, issue is fixed by upstream fix in wallbox package.

* Catch connectionerror

* Update homeassistant/components/wallbox/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Run black

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 homeassistant/components/wallbox/__init__.py  | 11 +++++---
 homeassistant/components/wallbox/lock.py      |  3 +++
 .../components/wallbox/manifest.json          |  2 +-
 homeassistant/components/wallbox/number.py    |  3 +++
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 tests/components/wallbox/__init__.py          | 26 +++++++++++++++++++
 tests/components/wallbox/test_lock.py         | 13 ++++++++++
 tests/components/wallbox/test_number.py       | 19 +++++++++++++-
 9 files changed, 73 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py
index 9175f42827d..6382cf05940 100644
--- a/homeassistant/components/wallbox/__init__.py
+++ b/homeassistant/components/wallbox/__init__.py
@@ -17,6 +17,7 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.update_coordinator import (
     CoordinatorEntity,
     DataUpdateCoordinator,
+    UpdateFailed,
 )
 
 from .const import (
@@ -93,6 +94,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
         """Authenticate using Wallbox API."""
         try:
             self._wallbox.authenticate()
+
         except requests.exceptions.HTTPError as wallbox_connection_error:
             if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN:
                 raise ConfigEntryAuthFailed from wallbox_connection_error
@@ -125,11 +127,12 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
             data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
                 data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
             )
-
             return data
-
-        except requests.exceptions.HTTPError as wallbox_connection_error:
-            raise ConnectionError from wallbox_connection_error
+        except (
+            ConnectionError,
+            requests.exceptions.HTTPError,
+        ) as wallbox_connection_error:
+            raise UpdateFailed from wallbox_connection_error
 
     async def _async_update_data(self) -> dict[str, Any]:
         """Get new sensor data for Wallbox component."""
diff --git a/homeassistant/components/wallbox/lock.py b/homeassistant/components/wallbox/lock.py
index d1b3c787735..7b5dca58010 100644
--- a/homeassistant/components/wallbox/lock.py
+++ b/homeassistant/components/wallbox/lock.py
@@ -6,6 +6,7 @@ from typing import Any
 from homeassistant.components.lock import LockEntity, LockEntityDescription
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import PlatformNotReady
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from . import InvalidAuth, WallboxCoordinator, WallboxEntity
@@ -36,6 +37,8 @@ async def async_setup_entry(
         )
     except InvalidAuth:
         return
+    except ConnectionError as exc:
+        raise PlatformNotReady from exc
 
     async_add_entities(
         [
diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json
index 5c195b8bfce..433a759bea5 100644
--- a/homeassistant/components/wallbox/manifest.json
+++ b/homeassistant/components/wallbox/manifest.json
@@ -3,7 +3,7 @@
   "name": "Wallbox",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/wallbox",
-  "requirements": ["wallbox==0.4.9"],
+  "requirements": ["wallbox==0.4.10"],
   "codeowners": ["@hesselonline"],
   "iot_class": "cloud_polling",
   "loggers": ["wallbox"]
diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py
index 5470ec11532..04db0ad9f7c 100644
--- a/homeassistant/components/wallbox/number.py
+++ b/homeassistant/components/wallbox/number.py
@@ -7,6 +7,7 @@ from typing import Optional, cast
 from homeassistant.components.number import NumberEntity, NumberEntityDescription
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import PlatformNotReady
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from . import InvalidAuth, WallboxCoordinator, WallboxEntity
@@ -46,6 +47,8 @@ async def async_setup_entry(
         )
     except InvalidAuth:
         return
+    except ConnectionError as exc:
+        raise PlatformNotReady from exc
 
     async_add_entities(
         [
diff --git a/requirements_all.txt b/requirements_all.txt
index c8e5427e2c7..b6bc47c6141 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2512,7 +2512,7 @@ vultr==0.1.2
 wakeonlan==2.1.0
 
 # homeassistant.components.wallbox
-wallbox==0.4.9
+wallbox==0.4.10
 
 # homeassistant.components.waqi
 waqiasync==1.0.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 884c57df5dc..588a41eaa62 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1740,7 +1740,7 @@ vultr==0.1.2
 wakeonlan==2.1.0
 
 # homeassistant.components.wallbox
-wallbox==0.4.9
+wallbox==0.4.10
 
 # homeassistant.components.folder_watcher
 watchdog==2.1.9
diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py
index 8c979c42ebe..53af3b6383d 100644
--- a/tests/components/wallbox/__init__.py
+++ b/tests/components/wallbox/__init__.py
@@ -173,3 +173,29 @@ async def setup_integration_read_only(hass: HomeAssistant) -> None:
 
         await hass.config_entries.async_setup(entry.entry_id)
         await hass.async_block_till_done()
+
+
+async def setup_integration_platform_not_ready(hass: HomeAssistant) -> None:
+    """Test wallbox sensor class setup for read only."""
+
+    with requests_mock.Mocker() as mock_request:
+        mock_request.get(
+            "https://user-api.wall-box.com/users/signin",
+            json=authorisation_response,
+            status_code=HTTPStatus.OK,
+        )
+        mock_request.get(
+            "https://api.wall-box.com/chargers/status/12345",
+            json=test_response,
+            status_code=HTTPStatus.OK,
+        )
+        mock_request.put(
+            "https://api.wall-box.com/v2/charger/12345",
+            json=test_response,
+            status_code=HTTPStatus.NOT_FOUND,
+        )
+
+        entry.add_to_hass(hass)
+
+        await hass.config_entries.async_setup(entry.entry_id)
+        await hass.async_block_till_done()
diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py
index 567d92757cd..bf0daa5c828 100644
--- a/tests/components/wallbox/test_lock.py
+++ b/tests/components/wallbox/test_lock.py
@@ -13,6 +13,7 @@ from . import (
     authorisation_response,
     entry,
     setup_integration,
+    setup_integration_platform_not_ready,
     setup_integration_read_only,
 )
 from .const import MOCK_LOCK_ENTITY_ID
@@ -109,3 +110,15 @@ async def test_wallbox_lock_class_authentication_error(hass: HomeAssistant) -> N
     assert state is None
 
     await hass.config_entries.async_unload(entry.entry_id)
+
+
+async def test_wallbox_lock_class_platform_not_ready(hass: HomeAssistant) -> None:
+    """Test wallbox lock not loaded on authentication error."""
+
+    await setup_integration_platform_not_ready(hass)
+
+    state = hass.states.get(MOCK_LOCK_ENTITY_ID)
+
+    assert state is None
+
+    await hass.config_entries.async_unload(entry.entry_id)
diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py
index 58e3450e6aa..c16c85d3696 100644
--- a/tests/components/wallbox/test_number.py
+++ b/tests/components/wallbox/test_number.py
@@ -9,7 +9,12 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY
 from homeassistant.const import ATTR_ENTITY_ID
 from homeassistant.core import HomeAssistant
 
-from . import authorisation_response, entry, setup_integration
+from . import (
+    authorisation_response,
+    entry,
+    setup_integration,
+    setup_integration_platform_not_ready,
+)
 from .const import MOCK_NUMBER_ENTITY_ID
 
 
@@ -71,3 +76,15 @@ async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> Non
                 blocking=True,
             )
     await hass.config_entries.async_unload(entry.entry_id)
+
+
+async def test_wallbox_number_class_platform_not_ready(hass: HomeAssistant) -> None:
+    """Test wallbox lock not loaded on authentication error."""
+
+    await setup_integration_platform_not_ready(hass)
+
+    state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
+
+    assert state is None
+
+    await hass.config_entries.async_unload(entry.entry_id)
-- 
GitLab


From f21a004aa9c092f06f2881cb11df944365d05a7b Mon Sep 17 00:00:00 2001
From: Janick Bergeron <janick@bergeron.com>
Date: Fri, 14 Oct 2022 04:11:10 -0700
Subject: [PATCH 469/985] Fix before sunrise OR after sunset condition (#76143)

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Ben Randall <veleek@gmail.com>
---
 homeassistant/helpers/condition.py |  57 ++++++++-----
 tests/helpers/test_condition.py    | 126 ++++++++++++++++++++++++++++-
 2 files changed, 160 insertions(+), 23 deletions(-)

diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index a628cdefff4..387d2ad09b0 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -586,31 +586,46 @@ def sun(
     before_offset = before_offset or timedelta(0)
     after_offset = after_offset or timedelta(0)
 
-    sunrise_today = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today)
-    sunset_today = get_astral_event_date(hass, SUN_EVENT_SUNSET, today)
-
-    sunrise = sunrise_today
-    sunset = sunset_today
-    if today > dt_util.as_local(
-        cast(datetime, sunrise_today)
-    ).date() and SUN_EVENT_SUNRISE in (before, after):
-        tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date()
-        sunrise_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
-        sunrise = sunrise_tomorrow
-
-    if today > dt_util.as_local(
-        cast(datetime, sunset_today)
-    ).date() and SUN_EVENT_SUNSET in (before, after):
-        tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date()
-        sunset_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
-        sunset = sunset_tomorrow
-
-    if sunrise is None and SUN_EVENT_SUNRISE in (before, after):
+    sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today)
+    sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today)
+
+    has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after)
+    has_sunset_condition = SUN_EVENT_SUNSET in (before, after)
+
+    after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date()
+    if after_sunrise and has_sunrise_condition:
+        tomorrow = today + timedelta(days=1)
+        sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow)
+
+    after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date()
+    if after_sunset and has_sunset_condition:
+        tomorrow = today + timedelta(days=1)
+        sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow)
+
+    # Special case: before sunrise OR after sunset
+    # This will handle the very rare case in the polar region when the sun rises/sets
+    # but does not set/rise.
+    # However this entire condition does not handle those full days of darkness or light,
+    # the following should be used instead:
+    #
+    #    condition:
+    #      condition: state
+    #      entity_id: sun.sun
+    #      state: 'above_horizon' (or 'below_horizon')
+    #
+    if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET:
+        wanted_time_before = cast(datetime, sunrise) + before_offset
+        condition_trace_update_result(wanted_time_before=wanted_time_before)
+        wanted_time_after = cast(datetime, sunset) + after_offset
+        condition_trace_update_result(wanted_time_after=wanted_time_after)
+        return utcnow < wanted_time_before or utcnow > wanted_time_after
+
+    if sunrise is None and has_sunrise_condition:
         # There is no sunrise today
         condition_trace_set_result(False, message="no sunrise today")
         return False
 
-    if sunset is None and SUN_EVENT_SUNSET in (before, after):
+    if sunset is None and has_sunset_condition:
         # There is no sunset today
         condition_trace_set_result(False, message="no sunset today")
         return False
diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py
index b7e4caf68c7..e3189955912 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -2735,9 +2735,9 @@ async def test_if_action_after_sunset_with_offset(hass, hass_ws_client, calls):
     )
 
 
-async def test_if_action_before_and_after_during(hass, hass_ws_client, calls):
+async def test_if_action_after_and_before_during(hass, hass_ws_client, calls):
     """
-    Test if action was after sunset and before sunrise.
+    Test if action was after sunrise and before sunset.
 
     This is true from sunrise until sunset.
     """
@@ -2837,6 +2837,128 @@ async def test_if_action_before_and_after_during(hass, hass_ws_client, calls):
     )
 
 
+async def test_if_action_before_or_after_during(hass, hass_ws_client, calls):
+    """
+    Test if action was before sunrise or after sunset.
+
+    This is true from midnight until sunrise and from sunset until midnight
+    """
+    await async_setup_component(
+        hass,
+        automation.DOMAIN,
+        {
+            automation.DOMAIN: {
+                "id": "sun",
+                "trigger": {"platform": "event", "event_type": "test_event"},
+                "condition": {
+                    "condition": "sun",
+                    "before": SUN_EVENT_SUNRISE,
+                    "after": SUN_EVENT_SUNSET,
+                },
+                "action": {"service": "test.automation"},
+            }
+        },
+    )
+
+    # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local
+    # sunrise: 2015-09-16 13:33:18 UTC,   sunset: 2015-09-17 01:53:45 UTC
+    # now = sunrise - 1s -> 'before sunrise' | 'after sunset' true
+    now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC)
+    with patch("homeassistant.util.dt.utcnow", return_value=now):
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 1
+    await assert_automation_condition_trace(
+        hass_ws_client,
+        "sun",
+        {
+            "result": True,
+            "wanted_time_after": "2015-09-17T01:53:44.723614+00:00",
+            "wanted_time_before": "2015-09-16T13:33:18.342542+00:00",
+        },
+    )
+
+    # now = sunset + 1s -> 'before sunrise' | 'after sunset' true
+    now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC)
+    with patch("homeassistant.util.dt.utcnow", return_value=now):
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 2
+    await assert_automation_condition_trace(
+        hass_ws_client,
+        "sun",
+        {
+            "result": True,
+            "wanted_time_after": "2015-09-17T01:53:44.723614+00:00",
+            "wanted_time_before": "2015-09-16T13:33:18.342542+00:00",
+        },
+    )
+
+    # now = sunrise + 1s -> 'before sunrise' | 'after sunset' false
+    now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC)
+    with patch("homeassistant.util.dt.utcnow", return_value=now):
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 2
+    await assert_automation_condition_trace(
+        hass_ws_client,
+        "sun",
+        {
+            "result": False,
+            "wanted_time_after": "2015-09-17T01:53:44.723614+00:00",
+            "wanted_time_before": "2015-09-16T13:33:18.342542+00:00",
+        },
+    )
+
+    # now = sunset - 1s -> 'before sunrise' | 'after sunset' false
+    now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC)
+    with patch("homeassistant.util.dt.utcnow", return_value=now):
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 2
+    await assert_automation_condition_trace(
+        hass_ws_client,
+        "sun",
+        {
+            "result": False,
+            "wanted_time_after": "2015-09-17T01:53:44.723614+00:00",
+            "wanted_time_before": "2015-09-16T13:33:18.342542+00:00",
+        },
+    )
+
+    # now = midnight + 1s local  -> 'before sunrise' | 'after sunset' true
+    now = datetime(2015, 9, 16, 7, 0, 1, tzinfo=dt_util.UTC)
+    with patch("homeassistant.util.dt.utcnow", return_value=now):
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 3
+    await assert_automation_condition_trace(
+        hass_ws_client,
+        "sun",
+        {
+            "result": True,
+            "wanted_time_after": "2015-09-17T01:53:44.723614+00:00",
+            "wanted_time_before": "2015-09-16T13:33:18.342542+00:00",
+        },
+    )
+
+    # now = midnight - 1s local  -> 'before sunrise' | 'after sunset' true
+    now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC)
+    with patch("homeassistant.util.dt.utcnow", return_value=now):
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 4
+    await assert_automation_condition_trace(
+        hass_ws_client,
+        "sun",
+        {
+            "result": True,
+            "wanted_time_after": "2015-09-17T01:53:44.723614+00:00",
+            "wanted_time_before": "2015-09-16T13:33:18.342542+00:00",
+        },
+    )
+
+
 async def test_if_action_before_sunrise_no_offset_kotzebue(hass, hass_ws_client, calls):
     """
     Test if action was before sunrise.
-- 
GitLab


From a63c9e8fb956ba365b68c3aa97e7402292f447be Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Fri, 14 Oct 2022 13:21:37 +0200
Subject: [PATCH 470/985] Update python-typing-update to 0.5.0 (#80315)

---
 .pre-commit-config.yaml                           | 3 ++-
 homeassistant/components/hue/v1/device_trigger.py | 6 +++---
 tests/components/airthings_ble/__init__.py        | 7 +++----
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3ba82c4aa58..ab4d048deb8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -69,11 +69,12 @@ repos:
     hooks:
       - id: prettier
   - repo: https://github.com/cdce8p/python-typing-update
-    rev: v0.3.5
+    rev: v0.5.0
     hooks:
       # Run `python-typing-update` hook manually from time to time
       # to update python typing syntax.
       # Will require manual work, before submitting changes!
+      # pre-commit run --hook-stage manual python-typing-update --all-files
       - id: python-typing-update
         stages: [manual]
         args:
diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py
index 4316ea65406..e3639e802da 100644
--- a/homeassistant/components/hue/v1/device_trigger.py
+++ b/homeassistant/components/hue/v1/device_trigger.py
@@ -1,4 +1,6 @@
 """Provides device automations for Philips Hue events in V1 bridge/api."""
+from __future__ import annotations
+
 from typing import TYPE_CHECKING
 
 import voluptuous as vol
@@ -173,9 +175,7 @@ async def async_attach_trigger(
 
 
 @callback
-def async_get_triggers(
-    bridge: "HueBridge", device: DeviceEntry
-) -> list[dict[str, str]]:
+def async_get_triggers(bridge: HueBridge, device: DeviceEntry) -> list[dict[str, str]]:
     """Return device triggers for device on `v1` bridge.
 
     Make sure device is a supported remote model.
diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py
index c6b59e02c15..d480f44b27e 100644
--- a/tests/components/airthings_ble/__init__.py
+++ b/tests/components/airthings_ble/__init__.py
@@ -1,5 +1,6 @@
 """Tests for the Airthings BLE integration."""
-from typing import Union
+from __future__ import annotations
+
 from unittest.mock import patch
 
 from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice
@@ -17,9 +18,7 @@ def patch_async_setup_entry(return_value=True):
     )
 
 
-def patch_async_ble_device_from_address(
-    return_value: Union[BluetoothServiceInfoBleak, None]
-):
+def patch_async_ble_device_from_address(return_value: BluetoothServiceInfoBleak | None):
     """Patch async ble device from address to return a given value."""
     return patch(
         "homeassistant.components.bluetooth.async_ble_device_from_address",
-- 
GitLab


From 6a757662e47061dcd99faf4f16881784ed2e2d89 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 13:44:18 +0200
Subject: [PATCH 471/985] Deprecate is_metric property of unit system (#80313)

---
 homeassistant/util/unit_system.py |  8 +++++++-
 tests/util/test_unit_system.py    | 18 +++++++++++++++---
 2 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 347c8ae859b..5c1f6d647a1 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -131,7 +131,13 @@ class UnitSystem:
     @property
     def is_metric(self) -> bool:
         """Determine if this is the metric unit system."""
-        return self._name == CONF_UNIT_SYSTEM_METRIC
+        report(
+            "accesses the `is_metric` property of the unit system. "
+            "This is deprecated and will stop working in Home Assistant 2023.1. "
+            "Please adjust to use instance check instead.",
+            error_if_core=False,
+        )
+        return self is METRIC_SYSTEM
 
     def temperature(self, temperature: float, from_unit: str) -> float:
         """Convert the given temperature to this unit system."""
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 72a4456a219..8c7a9cf2fc5 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -296,10 +296,22 @@ def test_properties():
     assert METRIC_SYSTEM.accumulated_precipitation_unit == LENGTH_MILLIMETERS
 
 
-def test_is_metric():
+@pytest.mark.parametrize(
+    "unit_system, expected_flag",
+    [
+        (METRIC_SYSTEM, True),
+        (IMPERIAL_SYSTEM, False),
+    ],
+)
+def test_is_metric(
+    caplog: pytest.LogCaptureFixture, unit_system: UnitSystem, expected_flag: bool
+):
     """Test the is metric flag."""
-    assert METRIC_SYSTEM.is_metric
-    assert not IMPERIAL_SYSTEM.is_metric
+    assert unit_system.is_metric == expected_flag
+    assert (
+        "Detected code that accesses the `is_metric` property of the unit system."
+        in caplog.text
+    )
 
 
 @pytest.mark.parametrize(
-- 
GitLab


From d75e449c525e4d6096bef10ea4f53cf2729f0806 Mon Sep 17 00:00:00 2001
From: "David F. Mulcahey" <david.mulcahey@me.com>
Date: Fri, 14 Oct 2022 08:15:10 -0400
Subject: [PATCH 472/985] Add ability to convert ZCL schemas to vol schemas to
 ZHA (#79908)

* try serializing cluster command schemas

* use min and max value from zigpy type

* different type assignments

* initial command execution changes

* cleanup

* cleanup and typing

* typing

* typing

* add tests

* mypy

* handle raw values too

* check for None responses

* make backwards compatible

* update yaml for svc change
---
 homeassistant/components/zha/api.py          | 102 ++++++----
 homeassistant/components/zha/core/device.py  |  93 ++++++---
 homeassistant/components/zha/core/helpers.py |  82 +++++++-
 homeassistant/components/zha/services.yaml   |   5 +
 tests/components/zha/test_helpers.py         | 197 +++++++++++++++++++
 5 files changed, 413 insertions(+), 66 deletions(-)
 create mode 100644 tests/components/zha/test_helpers.py

diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py
index 6cbcdf50983..c68136c23da 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 
 import asyncio
 import logging
-from typing import TYPE_CHECKING, Any, NamedTuple
+from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar, cast
 
 import voluptuous as vol
 import zigpy.backups
@@ -31,6 +31,7 @@ from .core.const import (
     ATTR_LEVEL,
     ATTR_MANUFACTURER,
     ATTR_MEMBERS,
+    ATTR_PARAMS,
     ATTR_TYPE,
     ATTR_VALUE,
     ATTR_WARNING_DEVICE_DURATION,
@@ -69,6 +70,7 @@ from .core.group import GroupMember
 from .core.helpers import (
     async_cluster_exists,
     async_is_bindable_target,
+    cluster_command_schema_to_vol_schema,
     convert_install_code,
     get_matched_clusters,
     qr_to_install_code,
@@ -110,6 +112,17 @@ IEEE_SERVICE = "ieee_based_service"
 
 IEEE_SCHEMA = vol.All(cv.string, EUI64.convert)
 
+# typing typevar
+_T = TypeVar("_T")
+
+
+def _ensure_list_if_present(value: _T | None) -> list[_T] | list[Any] | None:
+    """Wrap value in list if it is provided and not one."""
+    if value is None:
+        return None
+    return cast("list[_T]", value) if isinstance(value, list) else [value]
+
+
 SERVICE_PERMIT_PARAMS = {
     vol.Optional(ATTR_IEEE): IEEE_SCHEMA,
     vol.Optional(ATTR_DURATION, default=60): vol.All(
@@ -181,17 +194,22 @@ SERVICE_SCHEMAS = {
             ): cv.positive_int,
         }
     ),
-    SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema(
-        {
-            vol.Required(ATTR_IEEE): IEEE_SCHEMA,
-            vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
-            vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
-            vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
-            vol.Required(ATTR_COMMAND): cv.positive_int,
-            vol.Required(ATTR_COMMAND_TYPE): cv.string,
-            vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list,
-            vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
-        }
+    SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.All(
+        vol.Schema(
+            {
+                vol.Required(ATTR_IEEE): IEEE_SCHEMA,
+                vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
+                vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
+                vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
+                vol.Required(ATTR_COMMAND): cv.positive_int,
+                vol.Required(ATTR_COMMAND_TYPE): cv.string,
+                vol.Exclusive(ATTR_ARGS, "attrs_params"): _ensure_list_if_present,
+                vol.Exclusive(ATTR_PARAMS, "attrs_params"): dict,
+                vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
+            }
+        ),
+        cv.deprecated(ATTR_ARGS),
+        cv.has_at_least_one_key(ATTR_ARGS, ATTR_PARAMS),
     ),
     SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND: vol.Schema(
         {
@@ -711,6 +729,8 @@ async def websocket_device_cluster_commands(
     hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Return a list of cluster commands."""
+    import voluptuous_serialize  # pylint: disable=import-outside-toplevel
+
     zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
     ieee: EUI64 = msg[ATTR_IEEE]
     endpoint_id: int = msg[ATTR_ENDPOINT_ID]
@@ -731,6 +751,10 @@ async def websocket_device_cluster_commands(
                         TYPE: CLIENT,
                         ID: cmd_id,
                         ATTR_NAME: cmd.name,
+                        "schema": voluptuous_serialize.convert(
+                            cluster_command_schema_to_vol_schema(cmd.schema),
+                            custom_serializer=cv.custom_serializer,
+                        ),
                     }
                 )
             for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items():
@@ -739,6 +763,10 @@ async def websocket_device_cluster_commands(
                         TYPE: CLUSTER_COMMAND_SERVER,
                         ID: cmd_id,
                         ATTR_NAME: cmd.name,
+                        "schema": voluptuous_serialize.convert(
+                            cluster_command_schema_to_vol_schema(cmd.schema),
+                            custom_serializer=cv.custom_serializer,
+                        ),
                     }
                 )
     _LOGGER.debug(
@@ -1285,41 +1313,45 @@ def async_load_api(hass: HomeAssistant) -> None:
         cluster_type: str = service.data[ATTR_CLUSTER_TYPE]
         command: int = service.data[ATTR_COMMAND]
         command_type: str = service.data[ATTR_COMMAND_TYPE]
-        args: list = service.data[ATTR_ARGS]
+        args: list | None = service.data.get(ATTR_ARGS)
+        params: dict | None = service.data.get(ATTR_PARAMS)
         manufacturer: int | None = service.data.get(ATTR_MANUFACTURER)
         zha_device = zha_gateway.get_device(ieee)
-        response = None
         if zha_device is not None:
             if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None:
                 manufacturer = zha_device.manufacturer_code
-            response = await zha_device.issue_cluster_command(
+
+            await zha_device.issue_cluster_command(
                 endpoint_id,
                 cluster_id,
                 command,
                 command_type,
-                *args,
+                args,
+                params,
                 cluster_type=cluster_type,
                 manufacturer=manufacturer,
             )
-        _LOGGER.debug(
-            "Issued command for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s",
-            ATTR_CLUSTER_ID,
-            cluster_id,
-            ATTR_CLUSTER_TYPE,
-            cluster_type,
-            ATTR_ENDPOINT_ID,
-            endpoint_id,
-            ATTR_COMMAND,
-            command,
-            ATTR_COMMAND_TYPE,
-            command_type,
-            ATTR_ARGS,
-            args,
-            ATTR_MANUFACTURER,
-            manufacturer,
-            RESPONSE,
-            response,
-        )
+            _LOGGER.debug(
+                "Issued command for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s]",
+                ATTR_CLUSTER_ID,
+                cluster_id,
+                ATTR_CLUSTER_TYPE,
+                cluster_type,
+                ATTR_ENDPOINT_ID,
+                endpoint_id,
+                ATTR_COMMAND,
+                command,
+                ATTR_COMMAND_TYPE,
+                command_type,
+                ATTR_ARGS,
+                args,
+                ATTR_PARAMS,
+                params,
+                ATTR_MANUFACTURER,
+                manufacturer,
+            )
+        else:
+            raise ValueError(f"Device with IEEE {str(ieee)} not found")
 
     async_register_admin_service(
         hass,
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index a0a4521e19d..5eb436cbe53 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -17,11 +17,14 @@ import zigpy.exceptions
 from zigpy.profiles import PROFILES
 import zigpy.quirks
 from zigpy.types.named import EUI64, NWK
+from zigpy.zcl.clusters import Cluster
 from zigpy.zcl.clusters.general import Groups, Identify
+from zigpy.zcl.foundation import Status as ZclStatus, ZCLCommandDef
 import zigpy.zdo.types as zdo_types
 
 from homeassistant.const import ATTR_COMMAND, ATTR_NAME
 from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.dispatcher import (
     async_dispatcher_connect,
     async_dispatcher_send,
@@ -35,6 +38,7 @@ from .const import (
     ATTR_ATTRIBUTE,
     ATTR_AVAILABLE,
     ATTR_CLUSTER_ID,
+    ATTR_CLUSTER_TYPE,
     ATTR_COMMAND_TYPE,
     ATTR_DEVICE_TYPE,
     ATTR_ENDPOINT_ID,
@@ -49,6 +53,7 @@ from .const import (
     ATTR_NEIGHBORS,
     ATTR_NODE_DESCRIPTOR,
     ATTR_NWK,
+    ATTR_PARAMS,
     ATTR_POWER_SOURCE,
     ATTR_QUIRK_APPLIED,
     ATTR_QUIRK_CLASS,
@@ -74,7 +79,7 @@ from .const import (
     UNKNOWN_MODEL,
     ZHA_OPTIONS,
 )
-from .helpers import LogMixin, async_get_zha_config_value
+from .helpers import LogMixin, async_get_zha_config_value, convert_to_zcl_values
 
 if TYPE_CHECKING:
     from ..api import ClusterBinding
@@ -558,7 +563,7 @@ class ZHADevice(LogMixin):
         return device_info
 
     @callback
-    def async_get_clusters(self):
+    def async_get_clusters(self) -> dict[int, dict[str, dict[int, Cluster]]]:
         """Get all clusters for this device."""
         return {
             ep_id: {
@@ -592,9 +597,11 @@ class ZHADevice(LogMixin):
         }
 
     @callback
-    def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN):
+    def async_get_cluster(
+        self, endpoint_id: int, cluster_id: int, cluster_type: str = CLUSTER_TYPE_IN
+    ) -> Cluster:
         """Get zigbee cluster from this entity."""
-        clusters = self.async_get_clusters()
+        clusters: dict[int, dict[str, dict[int, Cluster]]] = self.async_get_clusters()
         return clusters[endpoint_id][cluster_type][cluster_id]
 
     @callback
@@ -660,36 +667,62 @@ class ZHADevice(LogMixin):
 
     async def issue_cluster_command(
         self,
-        endpoint_id,
-        cluster_id,
-        command,
-        command_type,
-        *args,
-        cluster_type=CLUSTER_TYPE_IN,
-        manufacturer=None,
-    ):
-        """Issue a command against specified zigbee cluster on this entity."""
-        cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type)
-        if cluster is None:
-            return None
-        if command_type == CLUSTER_COMMAND_SERVER:
-            response = await cluster.command(
-                command, *args, manufacturer=manufacturer, expect_reply=True
+        endpoint_id: int,
+        cluster_id: int,
+        command: int,
+        command_type: str,
+        args: list | None,
+        params: dict[str, Any] | None,
+        cluster_type: str = CLUSTER_TYPE_IN,
+        manufacturer: int | None = None,
+    ) -> None:
+        """Issue a command against specified zigbee cluster on this device."""
+        try:
+            cluster: Cluster = self.async_get_cluster(
+                endpoint_id, cluster_id, cluster_type
+            )
+        except KeyError as exc:
+            raise ValueError(
+                f"Cluster {cluster_id} not found on endpoint {endpoint_id} while issuing command {command} with args {args}"
+            ) from exc
+        commands: dict[int, ZCLCommandDef] = (
+            cluster.server_commands
+            if command_type == CLUSTER_COMMAND_SERVER
+            else cluster.client_commands
+        )
+        if args is not None:
+            self.warning(
+                "args [%s] are deprecated and should be passed with the params key. The parameter names are: %s",
+                args,
+                [field.name for field in commands[command].schema.fields],
             )
+            response = await getattr(cluster, commands[command].name)(*args)
         else:
-            response = await cluster.client_command(command, *args)
-
+            assert params is not None
+            response = await (
+                getattr(cluster, commands[command].name)(
+                    **convert_to_zcl_values(params, commands[command].schema)
+                )
+            )
         self.debug(
-            "Issued cluster command: %s %s %s %s %s %s %s",
-            f"{ATTR_CLUSTER_ID}: {cluster_id}",
-            f"{ATTR_COMMAND}: {command}",
-            f"{ATTR_COMMAND_TYPE}: {command_type}",
-            f"{ATTR_ARGS}: {args}",
-            f"{ATTR_CLUSTER_ID}: {cluster_type}",
-            f"{ATTR_MANUFACTURER}: {manufacturer}",
-            f"{ATTR_ENDPOINT_ID}: {endpoint_id}",
+            "Issued cluster command: %s %s %s %s %s %s %s %s",
+            f"{ATTR_CLUSTER_ID}: [{cluster_id}]",
+            f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]",
+            f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]",
+            f"{ATTR_COMMAND}: [{command}]",
+            f"{ATTR_COMMAND_TYPE}: [{command_type}]",
+            f"{ATTR_ARGS}: [{args}]",
+            f"{ATTR_PARAMS}: [{params}]",
+            f"{ATTR_MANUFACTURER}: [{manufacturer}]",
         )
-        return response
+        if response is None:
+            return  # client commands don't return a response
+        if isinstance(response, Exception):
+            raise HomeAssistantError("Failed to issue cluster command") from response
+        if response[1] is not ZclStatus.SUCCESS:
+            raise HomeAssistantError(
+                f"Failed to issue cluster command with status: {response[1]}"
+            )
 
     async def async_add_to_group(self, group_id: int) -> None:
         """Add this device to the provided zigbee group."""
diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py
index 7fd789ac3f5..409d45789b5 100644
--- a/homeassistant/components/zha/core/helpers.py
+++ b/homeassistant/components/zha/core/helpers.py
@@ -10,9 +10,11 @@ import asyncio
 import binascii
 from collections.abc import Callable, Iterator
 from dataclasses import dataclass
+import enum
 import functools
 import itertools
 import logging
+import operator
 from random import uniform
 import re
 from typing import TYPE_CHECKING, Any, TypeVar
@@ -22,12 +24,13 @@ import zigpy.exceptions
 import zigpy.types
 import zigpy.util
 import zigpy.zcl
+from zigpy.zcl.foundation import CommandSchema
 import zigpy.zdo.types as zdo_types
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, State, callback
 from homeassistant.exceptions import IntegrationError
-from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers import config_validation as cv, device_registry as dr
 
 from .const import (
     CLUSTER_TYPE_IN,
@@ -120,6 +123,83 @@ async def get_matched_clusters(
     return clusters_to_bind
 
 
+def cluster_command_schema_to_vol_schema(schema: CommandSchema) -> vol.Schema:
+    """Convert a cluster command schema to a voluptuous schema."""
+    return vol.Schema(
+        {
+            vol.Optional(field.name)
+            if field.optional
+            else vol.Required(field.name): schema_type_to_vol(field.type)
+            for field in schema.fields
+        }
+    )
+
+
+def schema_type_to_vol(field_type: Any) -> Any:
+    """Convert a schema type to a voluptuous type."""
+    if issubclass(field_type, enum.Flag) and len(field_type.__members__.keys()):
+        return cv.multi_select(
+            [key.replace("_", " ") for key in field_type.__members__.keys()]
+        )
+    if issubclass(field_type, enum.Enum) and len(field_type.__members__.keys()):
+        return vol.In([key.replace("_", " ") for key in field_type.__members__.keys()])
+    if (
+        issubclass(field_type, zigpy.types.FixedIntType)
+        or issubclass(field_type, enum.Flag)
+        or issubclass(field_type, enum.Enum)
+    ):
+        return vol.All(
+            vol.Coerce(int), vol.Range(field_type.min_value, field_type.max_value)
+        )
+    return str
+
+
+def convert_to_zcl_values(
+    fields: dict[str, Any], schema: CommandSchema
+) -> dict[str, Any]:
+    """Convert user input to ZCL values."""
+    converted_fields: dict[str, Any] = {}
+    for field in schema.fields:
+        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)
+                )
+        elif issubclass(field.type, enum.Enum):
+            value = (
+                field.type[value.replace(" ", "_")]
+                if isinstance(value, str)
+                else field.type(value)
+            )
+        else:
+            value = field.type(value)
+        _LOGGER.debug(
+            "Converted ZCL schema field(%s) value from: %s to: %s",
+            field.name,
+            fields[field.name],
+            value,
+        )
+        converted_fields[field.name] = value
+    return converted_fields
+
+
 @callback
 def async_is_bindable_target(source_zha_device, target_zha_device):
     """Determine if target is bindable to source."""
diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml
index 0e645da365e..132dae6e745 100644
--- a/homeassistant/components/zha/services.yaml
+++ b/homeassistant/components/zha/services.yaml
@@ -187,6 +187,11 @@ issue_zigbee_cluster_command:
       example: "[arg1, arg2, argN]"
       selector:
         object:
+    params:
+      name: Params
+      description: parameters to pass to the command
+      selector:
+        object:
     manufacturer:
       name: Manufacturer
       description: manufacturer code
diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py
new file mode 100644
index 00000000000..f5fb5c4f5c0
--- /dev/null
+++ b/tests/components/zha/test_helpers.py
@@ -0,0 +1,197 @@
+"""Tests for ZHA helpers."""
+import logging
+from unittest.mock import patch
+
+import pytest
+import voluptuous_serialize
+import zigpy.profiles.zha as zha
+from zigpy.types.basic import uint16_t
+import zigpy.zcl.clusters.general as general
+import zigpy.zcl.clusters.lighting as lighting
+
+from homeassistant.components.zha.core.helpers import (
+    cluster_command_schema_to_vol_schema,
+    convert_to_zcl_values,
+)
+from homeassistant.const import Platform
+import homeassistant.helpers.config_validation as cv
+
+from .common import async_enable_traffic
+from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@pytest.fixture(autouse=True)
+def light_platform_only():
+    """Only setup the light and required base platforms to speed up tests."""
+    with patch(
+        "homeassistant.components.zha.PLATFORMS",
+        (
+            Platform.BUTTON,
+            Platform.LIGHT,
+            Platform.NUMBER,
+            Platform.SELECT,
+        ),
+    ):
+        yield
+
+
+@pytest.fixture
+async def device_light(hass, zigpy_device_mock, zha_device_joined):
+    """Test light."""
+
+    zigpy_device = zigpy_device_mock(
+        {
+            1: {
+                SIG_EP_INPUT: [
+                    general.OnOff.cluster_id,
+                    general.LevelControl.cluster_id,
+                    lighting.Color.cluster_id,
+                    general.Groups.cluster_id,
+                    general.Identify.cluster_id,
+                ],
+                SIG_EP_OUTPUT: [],
+                SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+                SIG_EP_PROFILE: zha.PROFILE_ID,
+            }
+        }
+    )
+    color_cluster = zigpy_device.endpoints[1].light_color
+    color_cluster.PLUGGED_ATTR_READS = {
+        "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature
+        | lighting.Color.ColorCapabilities.XY_attributes
+    }
+    zha_device = await zha_device_joined(zigpy_device)
+    zha_device.available = True
+    return color_cluster, zha_device
+
+
+async def test_zcl_schema_conversions(hass, device_light):
+    """Test ZHA ZCL schema conversion helpers."""
+    color_cluster, zha_device = device_light
+    await async_enable_traffic(hass, [zha_device])
+    command_schema = color_cluster.commands_by_name["color_loop_set"].schema
+    expected_schema = [
+        {
+            "type": "multi_select",
+            "options": ["Action", "Direction", "Time", "Start Hue"],
+            "name": "update_flags",
+            "required": True,
+        },
+        {
+            "type": "select",
+            "options": [
+                ("Deactivate", "Deactivate"),
+                ("Activate from color loop hue", "Activate from color loop hue"),
+                ("Activate from current hue", "Activate from current hue"),
+            ],
+            "name": "action",
+            "required": True,
+        },
+        {
+            "type": "select",
+            "options": [("Decrement", "Decrement"), ("Increment", "Increment")],
+            "name": "direction",
+            "required": True,
+        },
+        {
+            "type": "integer",
+            "valueMin": 0,
+            "valueMax": 65535,
+            "name": "time",
+            "required": True,
+        },
+        {
+            "type": "integer",
+            "valueMin": 0,
+            "valueMax": 65535,
+            "name": "start_hue",
+            "required": True,
+        },
+        {
+            "type": "integer",
+            "valueMin": 0,
+            "valueMax": 255,
+            "name": "options_mask",
+            "optional": True,
+        },
+        {
+            "type": "integer",
+            "valueMin": 0,
+            "valueMax": 255,
+            "name": "options_override",
+            "optional": True,
+        },
+    ]
+    vol_schema = voluptuous_serialize.convert(
+        cluster_command_schema_to_vol_schema(command_schema),
+        custom_serializer=cv.custom_serializer,
+    )
+    assert vol_schema == expected_schema
+
+    raw_data = {
+        "update_flags": ["Action", "Start Hue"],
+        "action": "Activate from current hue",
+        "direction": "Increment",
+        "time": 20,
+        "start_hue": 196,
+    }
+
+    converted_data = convert_to_zcl_values(raw_data, command_schema)
+
+    assert isinstance(
+        converted_data["update_flags"], lighting.Color.ColorLoopUpdateFlags
+    )
+    assert lighting.Color.ColorLoopUpdateFlags.Action in converted_data["update_flags"]
+    assert (
+        lighting.Color.ColorLoopUpdateFlags.Start_Hue in converted_data["update_flags"]
+    )
+
+    assert isinstance(converted_data["action"], lighting.Color.ColorLoopAction)
+    assert (
+        converted_data["action"]
+        == lighting.Color.ColorLoopAction.Activate_from_current_hue
+    )
+
+    assert isinstance(converted_data["direction"], lighting.Color.ColorLoopDirection)
+    assert converted_data["direction"] == lighting.Color.ColorLoopDirection.Increment
+
+    assert isinstance(converted_data["time"], uint16_t)
+    assert converted_data["time"] == 20
+
+    assert isinstance(converted_data["start_hue"], uint16_t)
+    assert converted_data["start_hue"] == 196
+
+    raw_data = {
+        "update_flags": [0b0000_0001, 0b0000_1000],
+        "action": 0x02,
+        "direction": 0x01,
+        "time": 20,
+        "start_hue": 196,
+    }
+
+    converted_data = convert_to_zcl_values(raw_data, command_schema)
+
+    assert isinstance(
+        converted_data["update_flags"], lighting.Color.ColorLoopUpdateFlags
+    )
+    assert lighting.Color.ColorLoopUpdateFlags.Action in converted_data["update_flags"]
+    assert (
+        lighting.Color.ColorLoopUpdateFlags.Start_Hue in converted_data["update_flags"]
+    )
+
+    assert isinstance(converted_data["action"], lighting.Color.ColorLoopAction)
+    assert (
+        converted_data["action"]
+        == lighting.Color.ColorLoopAction.Activate_from_current_hue
+    )
+
+    assert isinstance(converted_data["direction"], lighting.Color.ColorLoopDirection)
+    assert converted_data["direction"] == lighting.Color.ColorLoopDirection.Increment
+
+    assert isinstance(converted_data["time"], uint16_t)
+    assert converted_data["time"] == 20
+
+    assert isinstance(converted_data["start_hue"], uint16_t)
+    assert converted_data["start_hue"] == 196
-- 
GitLab


From b42e26fbdd9f25c11681121ba7ac1e34fe7ce148 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Fri, 14 Oct 2022 14:45:57 +0200
Subject: [PATCH 473/985] Use SupportedDialect enum in recorder (#80319)

---
 homeassistant/components/recorder/statistics.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py
index 773676b07b0..88638a6ccc7 100644
--- a/homeassistant/components/recorder/statistics.py
+++ b/homeassistant/components/recorder/statistics.py
@@ -1529,7 +1529,7 @@ def _filter_unique_constraint_integrity_error(
             and err.orig.pgcode == "23505"
         ):
             ignore = True
-        if dialect_name == "mysql" and hasattr(err.orig, "args"):
+        if dialect_name == SupportedDialect.MYSQL and hasattr(err.orig, "args"):
             with contextlib.suppress(TypeError):
                 if err.orig.args[0] == 1062:
                     ignore = True
-- 
GitLab


From 70702f33773217b473e05ce1cbbc780286c317b5 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 14:53:04 +0200
Subject: [PATCH 474/985] Use local UNIT constants in geonetnz_volcano (#80323)

---
 .../components/geonetnz_volcano/__init__.py        | 12 +++++++++---
 .../components/geonetnz_volcano/config_flow.py     | 14 +++++++++-----
 homeassistant/components/geonetnz_volcano/const.py |  3 +++
 .../components/geonetnz_volcano/sensor.py          |  4 ++--
 4 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py
index e4bf2d2cb8c..da081b42599 100644
--- a/homeassistant/components/geonetnz_volcano/__init__.py
+++ b/homeassistant/components/geonetnz_volcano/__init__.py
@@ -14,7 +14,6 @@ from homeassistant.const import (
     CONF_RADIUS,
     CONF_SCAN_INTERVAL,
     CONF_UNIT_SYSTEM,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
 )
@@ -26,7 +25,14 @@ from homeassistant.helpers.typing import ConfigType
 from homeassistant.util.unit_conversion import DistanceConverter
 
 from .config_flow import configured_instances
-from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, PLATFORMS
+from .const import (
+    DEFAULT_RADIUS,
+    DEFAULT_SCAN_INTERVAL,
+    DOMAIN,
+    FEED,
+    IMPERIAL_UNITS,
+    PLATFORMS,
+)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -85,7 +91,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:
+    if unit_system == IMPERIAL_UNITS:
         radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS)
     # Create feed entity manager for all platforms.
     manager = GeonetnzVolcanoFeedEntityManager(hass, config_entry, radius, unit_system)
diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py
index 6de169c8602..d095aefab01 100644
--- a/homeassistant/components/geonetnz_volcano/config_flow.py
+++ b/homeassistant/components/geonetnz_volcano/config_flow.py
@@ -8,14 +8,18 @@ from homeassistant.const import (
     CONF_RADIUS,
     CONF_SCAN_INTERVAL,
     CONF_UNIT_SYSTEM,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    CONF_UNIT_SYSTEM_METRIC,
 )
 from homeassistant.core import callback
 from homeassistant.helpers import config_validation as cv
 from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
-from .const import DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN
+from .const import (
+    DEFAULT_RADIUS,
+    DEFAULT_SCAN_INTERVAL,
+    DOMAIN,
+    IMPERIAL_UNITS,
+    METRIC_UNITS,
+)
 
 
 @callback
@@ -59,9 +63,9 @@ class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
             return await self._show_form({"base": "already_configured"})
 
         if self.hass.config.units is IMPERIAL_SYSTEM:
-            user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL
+            user_input[CONF_UNIT_SYSTEM] = IMPERIAL_UNITS
         else:
-            user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC
+            user_input[CONF_UNIT_SYSTEM] = METRIC_UNITS
 
         scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
         user_input[CONF_SCAN_INTERVAL] = scan_interval.total_seconds()
diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py
index 3a23084aa1f..8c17ff42544 100644
--- a/homeassistant/components/geonetnz_volcano/const.py
+++ b/homeassistant/components/geonetnz_volcano/const.py
@@ -18,3 +18,6 @@ DEFAULT_RADIUS = 50.0
 DEFAULT_SCAN_INTERVAL = timedelta(minutes=5)
 
 PLATFORMS = [Platform.SENSOR]
+
+IMPERIAL_UNITS = "imperial"
+METRIC_UNITS = "metric"
diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py
index 51bca3e467a..e48a4cec0cf 100644
--- a/homeassistant/components/geonetnz_volcano/sensor.py
+++ b/homeassistant/components/geonetnz_volcano/sensor.py
@@ -9,7 +9,6 @@ from homeassistant.const import (
     ATTR_ATTRIBUTION,
     ATTR_LATITUDE,
     ATTR_LONGITUDE,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
 )
@@ -27,6 +26,7 @@ from .const import (
     DEFAULT_ICON,
     DOMAIN,
     FEED,
+    IMPERIAL_UNITS,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -113,7 +113,7 @@ class GeonetnzVolcanoSensor(SensorEntity):
         """Update the internal state from the provided feed entry."""
         self._title = feed_entry.title
         # Convert distance if not metric system.
-        if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL:
+        if self._unit_system == IMPERIAL_UNITS:
             self._distance = round(
                 DistanceConverter.convert(
                     feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES
-- 
GitLab


From 3840cbd012485e7cdae16421930b906f7e673791 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Paris?= <jfparis@gmail.com>
Date: Fri, 14 Oct 2022 13:53:48 +0100
Subject: [PATCH 475/985] Tag eafm sensors are measurement to collect long term
 stats (#80312)

---
 homeassistant/components/eafm/sensor.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py
index 57a248d70e9..ed663920a80 100644
--- a/homeassistant/components/eafm/sensor.py
+++ b/homeassistant/components/eafm/sensor.py
@@ -5,7 +5,7 @@ import logging
 from aioeafm import get_station
 import async_timeout
 
-from homeassistant.components.sensor import SensorEntity
+from homeassistant.components.sensor import SensorEntity, SensorStateClass
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS
 from homeassistant.core import HomeAssistant
@@ -91,6 +91,7 @@ class Measurement(CoordinatorEntity, SensorEntity):
     """A gauge at a flood monitoring station."""
 
     attribution = "This uses Environment Agency flood and river level data from the real-time data API"
+    _attr_state_class = SensorStateClass.MEASUREMENT
 
     def __init__(self, coordinator, key):
         """Initialise the gauge with a data instance and station."""
-- 
GitLab


From 2a887e6ed758c8e73caa510bacf3ef0dd8748b51 Mon Sep 17 00:00:00 2001
From: Marvin Wichmann <me@marvin-wichmann.de>
Date: Fri, 14 Oct 2022 15:10:19 +0200
Subject: [PATCH 476/985] Update xknx to 1.2.0 (#80318)

---
 homeassistant/components/knx/manifest.json | 2 +-
 requirements_all.txt                       | 2 +-
 requirements_test_all.txt                  | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index 0f2f1201415..5107956050f 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -3,7 +3,7 @@
   "name": "KNX",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/knx",
-  "requirements": ["xknx==1.1.0"],
+  "requirements": ["xknx==1.2.0"],
   "codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
   "quality_scale": "platinum",
   "iot_class": "local_push",
diff --git a/requirements_all.txt b/requirements_all.txt
index b6bc47c6141..e302960e4e8 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2557,7 +2557,7 @@ xboxapi==2.0.1
 xiaomi-ble==0.10.0
 
 # homeassistant.components.knx
-xknx==1.1.0
+xknx==1.2.0
 
 # homeassistant.components.bluesound
 # homeassistant.components.fritz
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 588a41eaa62..e3b755a310f 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1770,7 +1770,7 @@ xbox-webapi==2.0.11
 xiaomi-ble==0.10.0
 
 # homeassistant.components.knx
-xknx==1.1.0
+xknx==1.2.0
 
 # homeassistant.components.bluesound
 # homeassistant.components.fritz
-- 
GitLab


From 7d56ae772e672fc2f8a32e650ef8cec4537c8ebb Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 15:11:22 +0200
Subject: [PATCH 477/985] Use local UNIT constants in waze_travel_time (#80325)

---
 homeassistant/components/waze_travel_time/const.py  |  6 +++---
 homeassistant/components/waze_travel_time/sensor.py | 10 +++++-----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py
index 37278543dfb..50384e79d92 100644
--- a/homeassistant/components/waze_travel_time/const.py
+++ b/homeassistant/components/waze_travel_time/const.py
@@ -1,6 +1,4 @@
 """Constants for waze_travel_time."""
-from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
-
 DOMAIN = "waze_travel_time"
 
 CONF_DESTINATION = "destination"
@@ -21,7 +19,9 @@ DEFAULT_AVOID_TOLL_ROADS = False
 DEFAULT_AVOID_SUBSCRIPTION_ROADS = False
 DEFAULT_AVOID_FERRIES = False
 
-UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]
+IMPERIAL_UNITS = "imperial"
+METRIC_UNITS = "metric"
+UNITS = [METRIC_UNITS, IMPERIAL_UNITS]
 
 REGIONS = ["US", "NA", "EU", "IL", "AU"]
 VEHICLE_TYPES = ["car", "taxi", "motorcycle"]
diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py
index 66ee5376266..c4ff489cadf 100644
--- a/homeassistant/components/waze_travel_time/sensor.py
+++ b/homeassistant/components/waze_travel_time/sensor.py
@@ -16,8 +16,6 @@ from homeassistant.const import (
     ATTR_ATTRIBUTION,
     CONF_NAME,
     CONF_REGION,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    CONF_UNIT_SYSTEM_METRIC,
     EVENT_HOMEASSISTANT_STARTED,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
@@ -49,6 +47,8 @@ from .const import (
     DEFAULT_REALTIME,
     DEFAULT_VEHICLE_TYPE,
     DOMAIN,
+    IMPERIAL_UNITS,
+    METRIC_UNITS,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -65,13 +65,13 @@ async def async_setup_entry(
     defaults = {
         CONF_REALTIME: DEFAULT_REALTIME,
         CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE,
-        CONF_UNITS: CONF_UNIT_SYSTEM_METRIC,
+        CONF_UNITS: METRIC_UNITS,
         CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES,
         CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS,
         CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS,
     }
     if hass.config.units is IMPERIAL_SYSTEM:
-        defaults[CONF_UNITS] = CONF_UNIT_SYSTEM_IMPERIAL
+        defaults[CONF_UNITS] = IMPERIAL_UNITS
 
     if not config_entry.options:
         new_data = config_entry.data.copy()
@@ -248,7 +248,7 @@ class WazeTravelTimeData:
 
                 self.duration, distance = routes[route]
 
-                if units == CONF_UNIT_SYSTEM_IMPERIAL:
+                if units == IMPERIAL_UNITS:
                     # Convert to miles.
                     self.distance = DistanceConverter.convert(
                         distance, LENGTH_KILOMETERS, LENGTH_MILES
-- 
GitLab


From 4c3097a1571ae8821ba6793b3385a11689a4f68e Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 15:16:03 +0200
Subject: [PATCH 478/985] Use local UNIT constants in here_travel_time (#80324)

* Use local UNIT constants in here_travel_time

* Fix test
---
 .../components/here_travel_time/__init__.py          |  4 ++--
 .../components/here_travel_time/config_flow.py       |  8 ++++----
 homeassistant/components/here_travel_time/const.py   | 12 ++++--------
 homeassistant/components/here_travel_time/sensor.py  |  4 ++--
 tests/components/here_travel_time/test_sensor.py     |  2 +-
 5 files changed, 13 insertions(+), 17 deletions(-)

diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py
index ddfdc34ff69..b9ffa3e4baa 100644
--- a/homeassistant/components/here_travel_time/__init__.py
+++ b/homeassistant/components/here_travel_time/__init__.py
@@ -14,7 +14,6 @@ from homeassistant.const import (
     CONF_API_KEY,
     CONF_MODE,
     CONF_UNIT_SYSTEM,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LENGTH_METERS,
     LENGTH_MILES,
     Platform,
@@ -45,6 +44,7 @@ from .const import (
     CONF_ROUTE_MODE,
     DEFAULT_SCAN_INTERVAL,
     DOMAIN,
+    IMPERIAL_UNITS,
     NO_ROUTE_ERROR_MESSAGE,
     TRAFFIC_MODE_ENABLED,
     TRAVEL_MODES_VEHICLE,
@@ -178,7 +178,7 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator):
             traffic_time: float = summary["baseTime"]
             if self.config.travel_mode in TRAVEL_MODES_VEHICLE:
                 traffic_time = summary["trafficTime"]
-            if self.config.units == CONF_UNIT_SYSTEM_IMPERIAL:
+            if self.config.units == IMPERIAL_UNITS:
                 # Convert to miles.
                 distance = DistanceConverter.convert(
                     distance, LENGTH_METERS, LENGTH_MILES
diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py
index 54f164c43a7..a0f1fbbeb8c 100644
--- a/homeassistant/components/here_travel_time/config_flow.py
+++ b/homeassistant/components/here_travel_time/config_flow.py
@@ -15,8 +15,6 @@ from homeassistant.const import (
     CONF_MODE,
     CONF_NAME,
     CONF_UNIT_SYSTEM,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    CONF_UNIT_SYSTEM_METRIC,
 )
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.data_entry_flow import FlowResult
@@ -43,6 +41,8 @@ from .const import (
     CONF_TRAFFIC_MODE,
     DEFAULT_NAME,
     DOMAIN,
+    IMPERIAL_UNITS,
+    METRIC_UNITS,
     ROUTE_MODE_FASTEST,
     ROUTE_MODES,
     TRAFFIC_MODE_ENABLED,
@@ -96,10 +96,10 @@ def default_options(hass: HomeAssistant) -> dict[str, str | None]:
         CONF_ROUTE_MODE: ROUTE_MODE_FASTEST,
         CONF_ARRIVAL_TIME: None,
         CONF_DEPARTURE_TIME: None,
-        CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
+        CONF_UNIT_SYSTEM: METRIC_UNITS,
     }
     if hass.config.units is IMPERIAL_SYSTEM:
-        default[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL
+        default[CONF_UNIT_SYSTEM] = IMPERIAL_UNITS
     return default
 
 
diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py
index b79e7b4e4cd..ea0dc5c136e 100644
--- a/homeassistant/components/here_travel_time/const.py
+++ b/homeassistant/components/here_travel_time/const.py
@@ -1,10 +1,4 @@
 """Constants for the HERE Travel Time integration."""
-from homeassistant.const import (
-    CONF_UNIT_SYSTEM,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    CONF_UNIT_SYSTEM_METRIC,
-)
-
 DOMAIN = "here_travel_time"
 DEFAULT_SCAN_INTERVAL = 300
 
@@ -65,14 +59,16 @@ ICONS = {
     TRAVEL_MODE_TRUCK: ICON_TRUCK,
 }
 
-UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]
+IMPERIAL_UNITS = "imperial"
+METRIC_UNITS = "metric"
+UNITS = [METRIC_UNITS, IMPERIAL_UNITS]
 
 ATTR_DURATION = "duration"
 ATTR_DISTANCE = "distance"
 ATTR_ORIGIN = "origin"
 ATTR_DESTINATION = "destination"
 
-ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM
+ATTR_UNIT_SYSTEM = "unit_system"
 ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE
 
 ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic"
diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py
index e1d8342df19..1ee0087eab7 100644
--- a/homeassistant/components/here_travel_time/sensor.py
+++ b/homeassistant/components/here_travel_time/sensor.py
@@ -17,7 +17,6 @@ from homeassistant.const import (
     ATTR_LONGITUDE,
     CONF_MODE,
     CONF_NAME,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
     TIME_MINUTES,
@@ -41,6 +40,7 @@ from .const import (
     DOMAIN,
     ICON_CAR,
     ICONS,
+    IMPERIAL_UNITS,
 )
 
 SCAN_INTERVAL = timedelta(minutes=5)
@@ -216,6 +216,6 @@ class DistanceSensor(HERETravelTimeSensor):
     @property
     def native_unit_of_measurement(self) -> str | None:
         """Return the unit of measurement of the sensor."""
-        if self.coordinator.config.units == CONF_UNIT_SYSTEM_IMPERIAL:
+        if self.coordinator.config.units == IMPERIAL_UNITS:
             return LENGTH_MILES
         return LENGTH_KILOMETERS
diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py
index 316563b20cf..5cc4802d253 100644
--- a/tests/components/here_travel_time/test_sensor.py
+++ b/tests/components/here_travel_time/test_sensor.py
@@ -16,7 +16,6 @@ from homeassistant.components.here_travel_time.const import (
     CONF_ORIGIN_LATITUDE,
     CONF_ORIGIN_LONGITUDE,
     CONF_ROUTE_MODE,
-    CONF_UNIT_SYSTEM,
     DOMAIN,
     ICON_BICYCLE,
     ICON_CAR,
@@ -41,6 +40,7 @@ from homeassistant.const import (
     CONF_API_KEY,
     CONF_MODE,
     CONF_NAME,
+    CONF_UNIT_SYSTEM,
     EVENT_HOMEASSISTANT_START,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
-- 
GitLab


From dd266b7119d8bc3dcf58fd5ab9b766ee73d89530 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 14 Oct 2022 15:23:44 +0200
Subject: [PATCH 479/985] Remove elevation warning from sun (#80239)

---
 homeassistant/components/sun/__init__.py |  6 ------
 tests/components/sun/test_init.py        | 16 ++++------------
 tests/components/sun/test_trigger.py     |  2 +-
 tests/helpers/test_condition.py          |  2 +-
 tests/helpers/test_event.py              | 12 +++---------
 5 files changed, 9 insertions(+), 29 deletions(-)

diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py
index 256df2f8971..65836e0c619 100644
--- a/homeassistant/components/sun/__init__.py
+++ b/homeassistant/components/sun/__init__.py
@@ -9,7 +9,6 @@ from astral.location import Elevation, Location
 
 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
 from homeassistant.const import (
-    CONF_ELEVATION,
     EVENT_CORE_CONFIG_UPDATE,
     SUN_EVENT_SUNRISE,
     SUN_EVENT_SUNSET,
@@ -82,11 +81,6 @@ _PHASE_UPDATES = {
 
 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     """Track the state of the sun."""
-    if config.get(CONF_ELEVATION) is not None:
-        _LOGGER.warning(
-            "Elevation is now configured in Home Assistant core. "
-            "See https://www.home-assistant.io/docs/configuration/basic/"
-        )
     hass.async_create_task(
         hass.config_entries.flow.async_init(
             DOMAIN,
diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py
index 13aa6d48791..6f0f26f5f7a 100644
--- a/tests/components/sun/test_init.py
+++ b/tests/components/sun/test_init.py
@@ -18,9 +18,7 @@ async def test_setting_rising(hass):
     """Test retrieving sun setting and rising."""
     utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC)
     with freeze_time(utc_now):
-        await async_setup_component(
-            hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
-        )
+        await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
 
     await hass.async_block_till_done()
     state = hass.states.get(sun.ENTITY_ID)
@@ -112,9 +110,7 @@ async def test_state_change(hass, caplog):
     """Test if the state changes at next setting/rising."""
     now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC)
     with freeze_time(now):
-        await async_setup_component(
-            hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
-        )
+        await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
 
     await hass.async_block_till_done()
 
@@ -167,9 +163,7 @@ async def test_norway_in_june(hass):
     june = datetime(2016, 6, 1, tzinfo=dt_util.UTC)
 
     with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=june):
-        assert await async_setup_component(
-            hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
-        )
+        assert await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
 
     state = hass.states.get(sun.ENTITY_ID)
     assert state is not None
@@ -195,9 +189,7 @@ async def test_state_change_count(hass):
     now = datetime(2016, 6, 1, tzinfo=dt_util.UTC)
 
     with freeze_time(now):
-        assert await async_setup_component(
-            hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
-        )
+        assert await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
 
     events = []
 
diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py
index 5f2275dca34..a2f0fca8b7e 100644
--- a/tests/components/sun/test_trigger.py
+++ b/tests/components/sun/test_trigger.py
@@ -32,7 +32,7 @@ def setup_comp(hass):
     """Initialize components."""
     mock_component(hass, "group")
     hass.loop.run_until_complete(
-        async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}})
+        async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
     )
 
 
diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py
index e3189955912..65db1291f9d 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -42,7 +42,7 @@ def setup_comp(hass):
     """Initialize components."""
     hass.config.set_time_zone(hass.config.time_zone)
     hass.loop.run_until_complete(
-        async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}})
+        async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
     )
 
 
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index 57b89e64cce..536ccaaac68 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -3333,9 +3333,7 @@ async def test_track_sunrise(hass):
     # Setup sun component
     hass.config.latitude = latitude
     hass.config.longitude = longitude
-    assert await async_setup_component(
-        hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
-    )
+    assert await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
 
     location = LocationInfo(
         latitude=hass.config.latitude, longitude=hass.config.longitude
@@ -3400,9 +3398,7 @@ async def test_track_sunrise_update_location(hass):
     # Setup sun component
     hass.config.latitude = 32.87336
     hass.config.longitude = 117.22743
-    assert await async_setup_component(
-        hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
-    )
+    assert await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
 
     location = LocationInfo(
         latitude=hass.config.latitude, longitude=hass.config.longitude
@@ -3476,9 +3472,7 @@ async def test_track_sunset(hass):
     # Setup sun component
     hass.config.latitude = latitude
     hass.config.longitude = longitude
-    assert await async_setup_component(
-        hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}
-    )
+    assert await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}})
 
     # Get next sunrise/sunset
     utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC)
-- 
GitLab


From 81f40afd80c43f2727a4a58b4384702dff06f79c Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Fri, 14 Oct 2022 15:28:03 +0200
Subject: [PATCH 480/985] Only reload modified automations (#80282)

* Only reload modified automations

* Update tests

* Adjust spelling

* Improve efficiency of matching automations and configurations

* Reload automations without an alias if they have been moved

* Add test

* Add test

* Add test
---
 .../components/automation/__init__.py         | 221 +++++++++++----
 tests/components/automation/test_init.py      | 267 ++++++++++++++++++
 2 files changed, 428 insertions(+), 60 deletions(-)

diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index 3037d7cc3a7..9b6f9de1945 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -1,7 +1,9 @@
 """Allow to set up simple automation rules via the config file."""
 from __future__ import annotations
 
+import asyncio
 from collections.abc import Callable, Mapping
+from dataclasses import dataclass
 import logging
 from typing import Any, Protocol, cast
 
@@ -274,7 +276,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
 
     async def reload_service_handler(service_call: ServiceCall) -> None:
         """Remove all automations and load new ones from config."""
-        if (conf := await component.async_prepare_reload()) is None:
+        if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
             return
         async_get_blueprints(hass).async_reset_cache()
         await _async_process_config(hass, conf, component)
@@ -660,20 +662,27 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
         )
 
 
-async def _async_process_config(
-    hass: HomeAssistant,
-    config: dict[str, Any],
-    component: EntityComponent[AutomationEntity],
-) -> bool:
-    """Process config and add automations.
+@dataclass
+class AutomationEntityConfig:
+    """Container for prepared automation entity configuration."""
 
-    Returns if blueprints were used.
-    """
-    entities: list[AutomationEntity] = []
+    config_block: ConfigType
+    config_key: str
+    list_no: int
+    raw_blueprint_inputs: ConfigType | None
+    raw_config: ConfigType | None
+
+
+async def _prepare_automation_config(
+    hass: HomeAssistant,
+    config: ConfigType,
+) -> tuple[bool, list[AutomationEntityConfig]]:
+    """Parse configuration and prepare automation entity configuration."""
+    automation_configs: list[AutomationEntityConfig] = []
     blueprints_used = False
 
     for config_key in extract_domain_configs(config, DOMAIN):
-        conf: list[dict[str, Any] | blueprint.BlueprintInputs] = config[config_key]
+        conf: list[ConfigType | blueprint.BlueprintInputs] = config[config_key]
 
         for list_no, config_block in enumerate(conf):
             raw_blueprint_inputs = None
@@ -700,62 +709,154 @@ async def _async_process_config(
             else:
                 raw_config = cast(AutomationConfig, config_block).raw_config
 
-            automation_id: str | None = config_block.get(CONF_ID)
-            name: str = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}"
+            automation_configs.append(
+                AutomationEntityConfig(
+                    config_block, config_key, list_no, raw_blueprint_inputs, raw_config
+                )
+            )
+
+    return (blueprints_used, automation_configs)
 
-            initial_state: bool | None = config_block.get(CONF_INITIAL_STATE)
 
-            action_script = Script(
-                hass,
-                config_block[CONF_ACTION],
-                name,
-                DOMAIN,
-                running_description="automation actions",
-                script_mode=config_block[CONF_MODE],
-                max_runs=config_block[CONF_MAX],
-                max_exceeded=config_block[CONF_MAX_EXCEEDED],
-                logger=LOGGER,
-                # We don't pass variables here
-                # Automation will already render them to use them in the condition
-                # and so will pass them on to the script.
+def _automation_name(automation_config: AutomationEntityConfig) -> str:
+    """Return the configured name of an automation."""
+    config_block = automation_config.config_block
+    config_key = automation_config.config_key
+    list_no = automation_config.list_no
+    return config_block.get(CONF_ALIAS) or f"{config_key} {list_no}"
+
+
+async def _create_automation_entities(
+    hass: HomeAssistant, automation_configs: list[AutomationEntityConfig]
+) -> list[AutomationEntity]:
+    """Create automation entities from prepared configuration."""
+    entities: list[AutomationEntity] = []
+
+    for automation_config in automation_configs:
+        config_block = automation_config.config_block
+
+        automation_id: str | None = config_block.get(CONF_ID)
+        name = _automation_name(automation_config)
+
+        initial_state: bool | None = config_block.get(CONF_INITIAL_STATE)
+
+        action_script = Script(
+            hass,
+            config_block[CONF_ACTION],
+            name,
+            DOMAIN,
+            running_description="automation actions",
+            script_mode=config_block[CONF_MODE],
+            max_runs=config_block[CONF_MAX],
+            max_exceeded=config_block[CONF_MAX_EXCEEDED],
+            logger=LOGGER,
+            # We don't pass variables here
+            # Automation will already render them to use them in the condition
+            # and so will pass them on to the script.
+        )
+
+        if CONF_CONDITION in config_block:
+            cond_func = await _async_process_if(hass, name, config_block)
+
+            if cond_func is None:
+                continue
+        else:
+            cond_func = None
+
+        # Add trigger variables to variables
+        variables = None
+        if CONF_TRIGGER_VARIABLES in config_block:
+            variables = ScriptVariables(
+                dict(config_block[CONF_TRIGGER_VARIABLES].as_dict())
             )
+        if CONF_VARIABLES in config_block:
+            if variables:
+                variables.variables.update(config_block[CONF_VARIABLES].as_dict())
+            else:
+                variables = config_block[CONF_VARIABLES]
+
+        entity = AutomationEntity(
+            automation_id,
+            name,
+            config_block[CONF_TRIGGER],
+            cond_func,
+            action_script,
+            initial_state,
+            variables,
+            config_block.get(CONF_TRIGGER_VARIABLES),
+            automation_config.raw_config,
+            automation_config.raw_blueprint_inputs,
+            config_block[CONF_TRACE],
+        )
+        entities.append(entity)
+
+    return entities
+
+
+async def _async_process_config(
+    hass: HomeAssistant,
+    config: dict[str, Any],
+    component: EntityComponent[AutomationEntity],
+) -> bool:
+    """Process config and add automations.
+
+    Returns if blueprints were used.
+    """
+
+    def automation_matches_config(
+        automation: AutomationEntity, config: AutomationEntityConfig
+    ) -> bool:
+        name = _automation_name(config)
+        return automation.name == name and automation.raw_config == config.raw_config
+
+    def find_matches(
+        automations: list[AutomationEntity],
+        automation_configs: list[AutomationEntityConfig],
+    ) -> tuple[set[int], set[int]]:
+        """Find matches between a list of automation entities and a list of configurations.
 
-            if CONF_CONDITION in config_block:
-                cond_func = await _async_process_if(hass, name, config, config_block)
+        An automation or configuration is only allowed to match at most once to handle
+        the case of multiple automations with identical configuration.
 
-                if cond_func is None:
+        Returns a tuple of sets of indices: ({automation_matches}, {config_matches})
+        """
+        automation_matches: set[int] = set()
+        config_matches: set[int] = set()
+
+        for automation_idx, automation in enumerate(automations):
+            for config_idx, config in enumerate(automation_configs):
+                if config_idx in config_matches:
+                    # Only allow an automation config to match at most once
                     continue
-            else:
-                cond_func = None
+                if automation_matches_config(automation, config):
+                    automation_matches.add(automation_idx)
+                    config_matches.add(config_idx)
+                    # Only allow an automation to match at most once
+                    break
 
-            # Add trigger variables to variables
-            variables = None
-            if CONF_TRIGGER_VARIABLES in config_block:
-                variables = ScriptVariables(
-                    dict(config_block[CONF_TRIGGER_VARIABLES].as_dict())
-                )
-            if CONF_VARIABLES in config_block:
-                if variables:
-                    variables.variables.update(config_block[CONF_VARIABLES].as_dict())
-                else:
-                    variables = config_block[CONF_VARIABLES]
-
-            entity = AutomationEntity(
-                automation_id,
-                name,
-                config_block[CONF_TRIGGER],
-                cond_func,
-                action_script,
-                initial_state,
-                variables,
-                config_block.get(CONF_TRIGGER_VARIABLES),
-                raw_config,
-                raw_blueprint_inputs,
-                config_block[CONF_TRACE],
-            )
+        return automation_matches, config_matches
+
+    blueprints_used, automation_configs = await _prepare_automation_config(hass, config)
+    automations: list[AutomationEntity] = list(component.entities)
 
-            entities.append(entity)
+    # Find automations and configurations which have matches
+    automation_matches, config_matches = find_matches(automations, automation_configs)
 
+    # Remove automations which have changed config or no longer exist
+    tasks = [
+        automation.async_remove()
+        for idx, automation in enumerate(automations)
+        if idx not in automation_matches
+    ]
+    await asyncio.gather(*tasks)
+
+    # Create automations which have changed config or have been added
+    updated_automation_configs = [
+        config
+        for idx, config in enumerate(automation_configs)
+        if idx not in config_matches
+    ]
+    entities = await _create_automation_entities(hass, updated_automation_configs)
     if entities:
         await component.async_add_entities(entities)
 
@@ -763,10 +864,10 @@ async def _async_process_config(
 
 
 async def _async_process_if(
-    hass: HomeAssistant, name: str, config: dict[str, Any], p_config: dict[str, Any]
+    hass: HomeAssistant, name: str, config: dict[str, Any]
 ) -> IfAction | None:
     """Process if checks."""
-    if_configs = p_config[CONF_CONDITION]
+    if_configs = config[CONF_CONDITION]
 
     checks: list[condition.ConditionCheckerType] = []
     for if_config in if_configs:
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index 3cdf0c3a477..f129d829d4c 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -15,6 +15,7 @@ from homeassistant.components.automation import (
     EVENT_AUTOMATION_RELOADED,
     EVENT_AUTOMATION_TRIGGERED,
     SERVICE_TRIGGER,
+    AutomationEntity,
 )
 from homeassistant.const import (
     ATTR_ENTITY_ID,
@@ -720,6 +721,7 @@ async def test_automation_stops(hass, calls, service):
             blocking=True,
         )
     else:
+        config[automation.DOMAIN]["alias"] = "goodbye"
         with patch(
             "homeassistant.config.load_yaml_config_file",
             autospec=True,
@@ -735,6 +737,271 @@ async def test_automation_stops(hass, calls, service):
     assert len(calls) == (1 if service == "turn_off_no_stop" else 0)
 
 
+async def test_reload_unchanged_does_not_stop(hass, calls):
+    """Test that turning off / reloading stops any running actions as appropriate."""
+    test_entity = "test.entity"
+
+    config = {
+        automation.DOMAIN: {
+            "alias": "hello",
+            "trigger": {"platform": "event", "event_type": "test_event"},
+            "action": [
+                {"event": "running"},
+                {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
+                {"service": "test.automation"},
+            ],
+        }
+    }
+    assert await async_setup_component(hass, automation.DOMAIN, config)
+
+    running = asyncio.Event()
+
+    @callback
+    def running_cb(event):
+        running.set()
+
+    hass.bus.async_listen_once("running", running_cb)
+    hass.states.async_set(test_entity, "hello")
+
+    hass.bus.async_fire("test_event")
+    await running.wait()
+
+    with patch(
+        "homeassistant.config.load_yaml_config_file",
+        autospec=True,
+        return_value=config,
+    ):
+        await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True)
+
+    hass.states.async_set(test_entity, "goodbye")
+    await hass.async_block_till_done()
+
+    assert len(calls) == 1
+
+
+async def test_reload_moved_automation_without_alias(hass, calls):
+    """Test that changing the order of automations without alias triggers reload."""
+    with patch(
+        "homeassistant.components.automation.AutomationEntity", wraps=AutomationEntity
+    ) as automation_entity_init:
+        config = {
+            automation.DOMAIN: [
+                {
+                    "trigger": {"platform": "event", "event_type": "test_event"},
+                    "action": [{"service": "test.automation"}],
+                },
+                {
+                    "alias": "automation_with_alias",
+                    "trigger": {"platform": "event", "event_type": "test_event2"},
+                    "action": [{"service": "test.automation"}],
+                },
+            ]
+        }
+        assert await async_setup_component(hass, automation.DOMAIN, config)
+        assert automation_entity_init.call_count == 2
+        automation_entity_init.reset_mock()
+
+        assert hass.states.get("automation.automation_0")
+        assert not hass.states.get("automation.automation_1")
+        assert hass.states.get("automation.automation_with_alias")
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 1
+
+        # Reverse the order of the automations
+        config[automation.DOMAIN].reverse()
+        with patch(
+            "homeassistant.config.load_yaml_config_file",
+            autospec=True,
+            return_value=config,
+        ):
+            await hass.services.async_call(
+                automation.DOMAIN, SERVICE_RELOAD, blocking=True
+            )
+
+        assert automation_entity_init.call_count == 1
+        automation_entity_init.reset_mock()
+
+        assert not hass.states.get("automation.automation_0")
+        assert hass.states.get("automation.automation_1")
+        assert hass.states.get("automation.automation_with_alias")
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 2
+
+
+async def test_reload_identical_automations_without_id(hass, calls):
+    """Test reloading of identical automations without id."""
+    with patch(
+        "homeassistant.components.automation.AutomationEntity", wraps=AutomationEntity
+    ) as automation_entity_init:
+        config = {
+            automation.DOMAIN: [
+                {
+                    "alias": "dolly",
+                    "trigger": {"platform": "event", "event_type": "test_event"},
+                    "action": [{"service": "test.automation"}],
+                },
+                {
+                    "alias": "dolly",
+                    "trigger": {"platform": "event", "event_type": "test_event"},
+                    "action": [{"service": "test.automation"}],
+                },
+                {
+                    "alias": "dolly",
+                    "trigger": {"platform": "event", "event_type": "test_event"},
+                    "action": [{"service": "test.automation"}],
+                },
+            ]
+        }
+        assert await async_setup_component(hass, automation.DOMAIN, config)
+        assert automation_entity_init.call_count == 3
+        automation_entity_init.reset_mock()
+
+        assert hass.states.get("automation.dolly")
+        assert hass.states.get("automation.dolly_2")
+        assert hass.states.get("automation.dolly_3")
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 3
+
+        # Reload the automations without any change
+        with patch(
+            "homeassistant.config.load_yaml_config_file",
+            autospec=True,
+            return_value=config,
+        ):
+            await hass.services.async_call(
+                automation.DOMAIN, SERVICE_RELOAD, blocking=True
+            )
+
+        assert automation_entity_init.call_count == 0
+        automation_entity_init.reset_mock()
+
+        assert hass.states.get("automation.dolly")
+        assert hass.states.get("automation.dolly_2")
+        assert hass.states.get("automation.dolly_3")
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 6
+
+        # Remove two clones
+        del config[automation.DOMAIN][-1]
+        del config[automation.DOMAIN][-1]
+        with patch(
+            "homeassistant.config.load_yaml_config_file",
+            autospec=True,
+            return_value=config,
+        ):
+            await hass.services.async_call(
+                automation.DOMAIN, SERVICE_RELOAD, blocking=True
+            )
+
+        assert automation_entity_init.call_count == 0
+        automation_entity_init.reset_mock()
+
+        assert hass.states.get("automation.dolly")
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 7
+
+        # Add two clones
+        config[automation.DOMAIN].append(config[automation.DOMAIN][-1])
+        config[automation.DOMAIN].append(config[automation.DOMAIN][-1])
+        with patch(
+            "homeassistant.config.load_yaml_config_file",
+            autospec=True,
+            return_value=config,
+        ):
+            await hass.services.async_call(
+                automation.DOMAIN, SERVICE_RELOAD, blocking=True
+            )
+
+        assert automation_entity_init.call_count == 2
+        automation_entity_init.reset_mock()
+
+        assert hass.states.get("automation.dolly")
+        assert hass.states.get("automation.dolly_2")
+        assert hass.states.get("automation.dolly_3")
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 10
+
+
+@pytest.mark.parametrize(
+    "automation_config",
+    (
+        {
+            "trigger": {"platform": "event", "event_type": "test_event"},
+            "action": [{"service": "test.automation"}],
+        },
+        # An automation using templates
+        {
+            "trigger": {"platform": "event", "event_type": "test_event"},
+            "action": [{"service": "{{ 'test.automation' }}"}],
+        },
+        # An automation using blueprint
+        {
+            "use_blueprint": {
+                "path": "test_event_service.yaml",
+                "input": {
+                    "trigger_event": "test_event",
+                    "service_to_call": "test.automation",
+                    "a_number": 5,
+                },
+            }
+        },
+        # An automation using blueprint with templated input
+        {
+            "use_blueprint": {
+                "path": "test_event_service.yaml",
+                "input": {
+                    "trigger_event": "{{ 'test_event' }}",
+                    "service_to_call": "{{ 'test.automation' }}",
+                    "a_number": 5,
+                },
+            }
+        },
+    ),
+)
+async def test_reload_unchanged_automation(hass, calls, automation_config):
+    """Test an unmodified automation is not reloaded."""
+    with patch(
+        "homeassistant.components.automation.AutomationEntity", wraps=AutomationEntity
+    ) as automation_entity_init:
+        config = {automation.DOMAIN: [automation_config]}
+        assert await async_setup_component(hass, automation.DOMAIN, config)
+        assert automation_entity_init.call_count == 1
+        automation_entity_init.reset_mock()
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 1
+
+        # Reload the automations without any change
+        with patch(
+            "homeassistant.config.load_yaml_config_file",
+            autospec=True,
+            return_value=config,
+        ):
+            await hass.services.async_call(
+                automation.DOMAIN, SERVICE_RELOAD, blocking=True
+            )
+
+        assert automation_entity_init.call_count == 0
+        automation_entity_init.reset_mock()
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 2
+
+
 async def test_automation_restore_state(hass):
     """Ensure states are restored on startup."""
     time = dt_util.utcnow()
-- 
GitLab


From 40bfc61ebd65439df19954038bdf06c2a0afcb77 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 14 Oct 2022 15:48:10 +0200
Subject: [PATCH 481/985] Revert "Add Mazda brand" (#80314)

---
 homeassistant/brands/mazda.json           |  5 -----
 homeassistant/generated/integrations.json | 11 +++--------
 2 files changed, 3 insertions(+), 13 deletions(-)
 delete mode 100644 homeassistant/brands/mazda.json

diff --git a/homeassistant/brands/mazda.json b/homeassistant/brands/mazda.json
deleted file mode 100644
index 89b554e4279..00000000000
--- a/homeassistant/brands/mazda.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "domain": "mazda",
-  "name": "Mazda",
-  "integrations": ["mazda"]
-}
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 00a3848f8c4..9e8441dd242 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2433,14 +2433,9 @@
       "name": "Matrix"
     },
     "mazda": {
-      "name": "Mazda",
-      "integrations": {
-        "mazda": {
-          "config_flow": true,
-          "iot_class": "cloud_polling",
-          "name": "Mazda Connected Services"
-        }
-      }
+      "config_flow": true,
+      "iot_class": "cloud_polling",
+      "name": "Mazda Connected Services"
     },
     "meater": {
       "config_flow": true,
-- 
GitLab


From d5e26326dc4c39f1db3963b306ed295c1f7fbbf9 Mon Sep 17 00:00:00 2001
From: mnorrsken <46407660+mnorrsken@users.noreply.github.com>
Date: Fri, 14 Oct 2022 16:03:48 +0200
Subject: [PATCH 482/985] Bump pyTibber to 0.25.4 (#80316)

---
 homeassistant/components/tibber/manifest.json | 2 +-
 requirements_all.txt                          | 2 +-
 requirements_test_all.txt                     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index d885bc8fbfd..dcb15d55002 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -3,7 +3,7 @@
   "domain": "tibber",
   "name": "Tibber",
   "documentation": "https://www.home-assistant.io/integrations/tibber",
-  "requirements": ["pyTibber==0.25.2"],
+  "requirements": ["pyTibber==0.25.4"],
   "codeowners": ["@danielhiversen"],
   "quality_scale": "silver",
   "config_flow": true,
diff --git a/requirements_all.txt b/requirements_all.txt
index e302960e4e8..92527521d97 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1409,7 +1409,7 @@ pyRFXtrx==0.30.0
 pySwitchmate==0.5.1
 
 # homeassistant.components.tibber
-pyTibber==0.25.2
+pyTibber==0.25.4
 
 # homeassistant.components.dlink
 pyW215==0.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e3b755a310f..4ee1a844851 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1009,7 +1009,7 @@ pyMetno==0.9.0
 pyRFXtrx==0.30.0
 
 # homeassistant.components.tibber
-pyTibber==0.25.2
+pyTibber==0.25.4
 
 # homeassistant.components.nextbus
 py_nextbusnext==0.1.5
-- 
GitLab


From 284893d9423d0f85030c06c54b9465374430c70d Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Fri, 14 Oct 2022 16:43:09 +0200
Subject: [PATCH 483/985] Fix reload of automation and scripts when blueprint
 changed (#80322)

---
 .../components/automation/__init__.py         |  2 +-
 homeassistant/components/blueprint/models.py  |  6 +-
 homeassistant/components/script/__init__.py   |  2 +-
 tests/components/automation/test_init.py      | 55 +++++++++++++++++++
 tests/components/blueprint/test_models.py     |  2 +-
 5 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index 9b6f9de1945..9ad1f79354e 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -276,9 +276,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
 
     async def reload_service_handler(service_call: ServiceCall) -> None:
         """Remove all automations and load new ones from config."""
+        await async_get_blueprints(hass).async_reset_cache()
         if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
             return
-        async_get_blueprints(hass).async_reset_cache()
         await _async_process_config(hass, conf, component)
         hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context)
 
diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py
index f77a2bed9a4..bc0938b1097 100644
--- a/homeassistant/components/blueprint/models.py
+++ b/homeassistant/components/blueprint/models.py
@@ -202,10 +202,10 @@ class DomainBlueprints:
         """Return the blueprint folder."""
         return pathlib.Path(self.hass.config.path(BLUEPRINT_FOLDER, self.domain))
 
-    @callback
-    def async_reset_cache(self) -> None:
+    async def async_reset_cache(self) -> None:
         """Reset the blueprint cache."""
-        self._blueprints = {}
+        async with self._load_lock:
+            self._blueprints = {}
 
     def _load_blueprint(self, blueprint_path) -> Blueprint:
         """Load a blueprint."""
diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py
index 0effdc2754b..27bc2556dc5 100644
--- a/homeassistant/components/script/__init__.py
+++ b/homeassistant/components/script/__init__.py
@@ -176,9 +176,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
 
     async def reload_service(service: ServiceCall) -> None:
         """Call a service to reload scripts."""
+        await async_get_blueprints(hass).async_reset_cache()
         if (conf := await component.async_prepare_reload()) is None:
             return
-        async_get_blueprints(hass).async_reset_cache()
         await _async_process_config(hass, conf, component)
 
     async def turn_on_service(service: ServiceCall) -> None:
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index f129d829d4c..858f3de6549 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -46,6 +46,7 @@ from homeassistant.helpers.script import (
     _async_stop_scripts_at_shutdown,
 )
 from homeassistant.setup import async_setup_component
+from homeassistant.util import yaml
 import homeassistant.util.dt as dt_util
 
 from tests.common import (
@@ -1002,6 +1003,60 @@ async def test_reload_unchanged_automation(hass, calls, automation_config):
         assert len(calls) == 2
 
 
+async def test_reload_automation_when_blueprint_changes(hass, calls):
+    """Test an automation is updated at reload if the blueprint has changed."""
+    with patch(
+        "homeassistant.components.automation.AutomationEntity", wraps=AutomationEntity
+    ) as automation_entity_init:
+        config = {
+            automation.DOMAIN: [
+                {
+                    "use_blueprint": {
+                        "path": "test_event_service.yaml",
+                        "input": {
+                            "trigger_event": "test_event",
+                            "service_to_call": "test.automation",
+                            "a_number": 5,
+                        },
+                    }
+                }
+            ]
+        }
+        assert await async_setup_component(hass, automation.DOMAIN, config)
+        assert automation_entity_init.call_count == 1
+        automation_entity_init.reset_mock()
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 1
+
+        # Reload the automations without any change, but with updated blueprint
+        blueprint_path = automation.async_get_blueprints(hass).blueprint_folder
+        blueprint_config = yaml.load_yaml(blueprint_path / "test_event_service.yaml")
+        blueprint_config["action"] = [blueprint_config["action"]]
+        blueprint_config["action"].append(blueprint_config["action"][-1])
+
+        with patch(
+            "homeassistant.config.load_yaml_config_file",
+            autospec=True,
+            return_value=config,
+        ), patch(
+            "homeassistant.components.blueprint.models.yaml.load_yaml",
+            autospec=True,
+            return_value=blueprint_config,
+        ):
+            await hass.services.async_call(
+                automation.DOMAIN, SERVICE_RELOAD, blocking=True
+            )
+
+        assert automation_entity_init.call_count == 1
+        automation_entity_init.reset_mock()
+
+        hass.bus.async_fire("test_event")
+        await hass.async_block_till_done()
+        assert len(calls) == 3
+
+
 async def test_automation_restore_state(hass):
     """Ensure states are restored on startup."""
     time = dt_util.utcnow()
diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py
index 02ed94709db..589025a08ba 100644
--- a/tests/components/blueprint/test_models.py
+++ b/tests/components/blueprint/test_models.py
@@ -224,7 +224,7 @@ async def test_domain_blueprints_caching(domain_bps):
     assert await domain_bps.async_get_blueprint("something") is obj
 
     obj_2 = object()
-    domain_bps.async_reset_cache()
+    await domain_bps.async_reset_cache()
 
     # Now we call this method again.
     with patch.object(domain_bps, "_load_blueprint", return_value=obj_2):
-- 
GitLab


From bff5d1123f14df66cd043d98622467098c10422c Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Fri, 14 Oct 2022 16:50:04 +0200
Subject: [PATCH 484/985] Deprecate CONF_UNIT_SYSTEM_*** constants (#80320)

* Deprecate CONF_UNIT_SYSTEM_*** constants

* Adjust pylint plugin

* Add tests

* Remove single-use function

* Revert logic change

* Revert "Revert logic change"

This reverts commit 60959a0050fbf5205e8e7d89654f4011256968d8.

* Tweak again
---
 homeassistant/components/config/core.py    | 13 +++++----
 homeassistant/config.py                    | 10 ++-----
 homeassistant/const.py                     |  2 ++
 homeassistant/core.py                      | 11 ++++---
 homeassistant/helpers/config_validation.py |  7 -----
 homeassistant/util/unit_system.py          | 25 +++++++++++++---
 pylint/plugins/hass_imports.py             |  4 +++
 tests/util/test_unit_system.py             | 34 ++++++++++++++++++----
 8 files changed, 72 insertions(+), 34 deletions(-)

diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py
index 3f665e475f0..d6d97b5caca 100644
--- a/homeassistant/components/config/core.py
+++ b/homeassistant/components/config/core.py
@@ -5,10 +5,9 @@ import voluptuous as vol
 from homeassistant.components import websocket_api
 from homeassistant.components.http import HomeAssistantView
 from homeassistant.config import async_check_ha_config_file
-from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
 from homeassistant.helpers import config_validation as cv
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.util import location
+from homeassistant.util import location, unit_system
 
 
 async def async_setup(hass):
@@ -41,7 +40,7 @@ class CheckConfigView(HomeAssistantView):
         vol.Optional("latitude"): cv.latitude,
         vol.Optional("longitude"): cv.longitude,
         vol.Optional("elevation"): int,
-        vol.Optional("unit_system"): cv.unit_system,
+        vol.Optional("unit_system"): unit_system.validate_unit_system,
         vol.Optional("location_name"): str,
         vol.Optional("time_zone"): cv.time_zone,
         vol.Optional("external_url"): vol.Any(cv.url_no_path, None),
@@ -77,10 +76,14 @@ async def websocket_detect_config(hass, connection, msg):
         connection.send_result(msg["id"], info)
         return
 
+    # We don't want any integrations to use the name of the unit system
+    # so we are using the private attribute here
     if location_info.use_metric:
-        info["unit_system"] = CONF_UNIT_SYSTEM_METRIC
+        # pylint: disable-next=protected-access
+        info["unit_system"] = unit_system._CONF_UNIT_SYSTEM_METRIC
     else:
-        info["unit_system"] = CONF_UNIT_SYSTEM_IMPERIAL
+        # pylint: disable-next=protected-access
+        info["unit_system"] = unit_system._CONF_UNIT_SYSTEM_IMPERIAL
 
     if location_info.latitude:
         info["latitude"] = location_info.latitude
diff --git a/homeassistant/config.py b/homeassistant/config.py
index e71b0dcd726..ab3ad0eb5c1 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -44,7 +44,6 @@ from .const import (
     CONF_TIME_ZONE,
     CONF_TYPE,
     CONF_UNIT_SYSTEM,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
     __version__,
 )
@@ -60,7 +59,7 @@ from .helpers.typing import ConfigType
 from .loader import Integration, IntegrationNotFound
 from .requirements import RequirementsNotFound, async_get_integration_with_requirements
 from .util.package import is_docker_env
-from .util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from .util.unit_system import get_unit_system, validate_unit_system
 from .util.yaml import SECRET_YAML, Secrets, load_yaml
 
 _LOGGER = logging.getLogger(__name__)
@@ -204,7 +203,7 @@ CORE_CONFIG_SCHEMA = vol.All(
             CONF_LONGITUDE: cv.longitude,
             CONF_ELEVATION: vol.Coerce(int),
             vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
-            CONF_UNIT_SYSTEM: cv.unit_system,
+            CONF_UNIT_SYSTEM: validate_unit_system,
             CONF_TIME_ZONE: cv.time_zone,
             vol.Optional(CONF_INTERNAL_URL): cv.url,
             vol.Optional(CONF_EXTERNAL_URL): cv.url,
@@ -602,10 +601,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non
     hass.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)
 
     if CONF_UNIT_SYSTEM in config:
-        if config[CONF_UNIT_SYSTEM] == CONF_UNIT_SYSTEM_IMPERIAL:
-            hac.units = IMPERIAL_SYSTEM
-        else:
-            hac.units = METRIC_SYSTEM
+        hac.units = get_unit_system(config[CONF_UNIT_SYSTEM])
 
 
 def _log_pkg_error(package: str, component: str, config: dict, message: str) -> None:
diff --git a/homeassistant/const.py b/homeassistant/const.py
index c89eca63623..99af9974ea0 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -396,7 +396,9 @@ ATTR_ICON: Final = "icon"
 ATTR_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement"
 
 CONF_UNIT_SYSTEM_METRIC: Final = "metric"
+"""Deprecated: please use a local constant."""
 CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial"
+"""Deprecated: please use a local constant."""
 
 # Electrical attributes
 ATTR_VOLTAGE: Final = "voltage"
diff --git a/homeassistant/core.py b/homeassistant/core.py
index 5a8ec07c1b9..872b4298c5d 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -49,7 +49,6 @@ from .const import (
     ATTR_FRIENDLY_NAME,
     ATTR_SERVICE,
     ATTR_SERVICE_DATA,
-    CONF_UNIT_SYSTEM_IMPERIAL,
     EVENT_CALL_SERVICE,
     EVENT_CORE_CONFIG_UPDATE,
     EVENT_HOMEASSISTANT_CLOSE,
@@ -82,7 +81,7 @@ from .util.async_ import (
 )
 from .util.read_only_dict import ReadOnlyDict
 from .util.timeout import TimeoutManager
-from .util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
+from .util.unit_system import METRIC_SYSTEM, UnitSystem, get_unit_system
 
 # Typing imports that create a circular dependency
 if TYPE_CHECKING:
@@ -1940,9 +1939,9 @@ class Config:
         if elevation is not None:
             self.elevation = elevation
         if unit_system is not None:
-            if unit_system == CONF_UNIT_SYSTEM_IMPERIAL:
-                self.units = IMPERIAL_SYSTEM
-            else:
+            try:
+                self.units = get_unit_system(unit_system)
+            except ValueError:
                 self.units = METRIC_SYSTEM
         if location_name is not None:
             self.location_name = location_name
@@ -2015,7 +2014,7 @@ class Config:
             "latitude": self.latitude,
             "longitude": self.longitude,
             "elevation": self.elevation,
-            # We don't want any components to use the name of the unit system
+            # We don't want any integrations to use the name of the unit system
             # so we are using the private attribute here
             "unit_system": self.units._name,  # pylint: disable=protected-access
             "location_name": self.location_name,
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index f6e77ef0018..35191d77042 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -71,8 +71,6 @@ from homeassistant.const import (
     CONF_TARGET,
     CONF_THEN,
     CONF_TIMEOUT,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    CONF_UNIT_SYSTEM_METRIC,
     CONF_UNTIL,
     CONF_VALUE_TEMPLATE,
     CONF_VARIABLES,
@@ -588,11 +586,6 @@ def temperature_unit(value: Any) -> str:
     raise vol.Invalid("invalid temperature unit (expected C or F)")
 
 
-unit_system = vol.All(
-    vol.Lower, vol.Any(CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL)
-)
-
-
 def template(value: Any | None) -> template_helper.Template:
     """Validate a jinja2 template."""
     if value is None:
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 5c1f6d647a1..8dad108ccbe 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -2,11 +2,12 @@
 from __future__ import annotations
 
 from numbers import Number
+from typing import Final
+
+import voluptuous as vol
 
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    CONF_UNIT_SYSTEM_METRIC,
     LENGTH,
     LENGTH_INCHES,
     LENGTH_KILOMETERS,
@@ -41,6 +42,9 @@ from .unit_conversion import (
     VolumeConverter,
 )
 
+_CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial"
+_CONF_UNIT_SYSTEM_METRIC: Final = "metric"
+
 LENGTH_UNITS = DistanceConverter.VALID_UNITS
 
 MASS_UNITS: set[str] = {MASS_POUNDS, MASS_OUNCES, MASS_KILOGRAMS, MASS_GRAMS}
@@ -207,8 +211,21 @@ class UnitSystem:
         }
 
 
+def get_unit_system(key: str) -> UnitSystem:
+    """Get unit system based on key."""
+    if key == _CONF_UNIT_SYSTEM_IMPERIAL:
+        return IMPERIAL_SYSTEM
+    if key == _CONF_UNIT_SYSTEM_METRIC:
+        return METRIC_SYSTEM
+    raise ValueError(f"`{key}` is not a valid unit system key")
+
+
+validate_unit_system = vol.All(
+    vol.Lower, vol.Any(_CONF_UNIT_SYSTEM_METRIC, _CONF_UNIT_SYSTEM_IMPERIAL)
+)
+
 METRIC_SYSTEM = UnitSystem(
-    CONF_UNIT_SYSTEM_METRIC,
+    _CONF_UNIT_SYSTEM_METRIC,
     TEMP_CELSIUS,
     LENGTH_KILOMETERS,
     SPEED_METERS_PER_SECOND,
@@ -219,7 +236,7 @@ METRIC_SYSTEM = UnitSystem(
 )
 
 IMPERIAL_SYSTEM = UnitSystem(
-    CONF_UNIT_SYSTEM_IMPERIAL,
+    _CONF_UNIT_SYSTEM_IMPERIAL,
     TEMP_FAHRENHEIT,
     LENGTH_MILES,
     SPEED_MILES_PER_HOUR,
diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py
index 45deecfc02e..3d2f747ca7b 100644
--- a/pylint/plugins/hass_imports.py
+++ b/pylint/plugins/hass_imports.py
@@ -267,6 +267,10 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
             reason="replaced by EntityCategory enum",
             constant=re.compile(r"^(ENTITY_CATEGORY_(\w*))|(ENTITY_CATEGORIES)$"),
         ),
+        ObsoleteImportMatch(
+            reason="replaced by local constants",
+            constant=re.compile(r"^(CONF_UNIT_SYSTEM_(\w*))$"),
+        ),
     ],
     "homeassistant.core": [
         ObsoleteImportMatch(
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 8c7a9cf2fc5..3019d6d0ff6 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -3,8 +3,6 @@ import pytest
 
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
-    CONF_UNIT_SYSTEM_IMPERIAL,
-    CONF_UNIT_SYSTEM_METRIC,
     LENGTH,
     LENGTH_KILOMETERS,
     LENGTH_METERS,
@@ -21,7 +19,14 @@ from homeassistant.const import (
     WIND_SPEED,
 )
 from homeassistant.exceptions import HomeAssistantError
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
+from homeassistant.util.unit_system import (
+    _CONF_UNIT_SYSTEM_IMPERIAL,
+    _CONF_UNIT_SYSTEM_METRIC,
+    IMPERIAL_SYSTEM,
+    METRIC_SYSTEM,
+    UnitSystem,
+    get_unit_system,
+)
 
 SYSTEM_NAME = "TEST"
 INVALID_UNIT = "INVALID"
@@ -317,8 +322,8 @@ def test_is_metric(
 @pytest.mark.parametrize(
     "unit_system, expected_name",
     [
-        (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC),
-        (IMPERIAL_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
+        (METRIC_SYSTEM, _CONF_UNIT_SYSTEM_METRIC),
+        (IMPERIAL_SYSTEM, _CONF_UNIT_SYSTEM_IMPERIAL),
     ],
 )
 def test_deprecated_name(
@@ -330,3 +335,22 @@ def test_deprecated_name(
         "Detected code that accesses the `name` property of the unit system."
         in caplog.text
     )
+
+
+@pytest.mark.parametrize(
+    "key, expected_system",
+    [
+        (_CONF_UNIT_SYSTEM_METRIC, METRIC_SYSTEM),
+        (_CONF_UNIT_SYSTEM_IMPERIAL, IMPERIAL_SYSTEM),
+    ],
+)
+def test_get_unit_system(key: str, expected_system: UnitSystem) -> None:
+    """Test get_unit_system."""
+    assert get_unit_system(key) is expected_system
+
+
+@pytest.mark.parametrize("key", [None, "", "invalid_custom"])
+def test_get_unit_system_invalid(key: str) -> None:
+    """Test get_unit_system with an invalid key."""
+    with pytest.raises(ValueError, match=f"`{key}` is not a valid unit system key"):
+        _ = get_unit_system(key)
-- 
GitLab


From 7e1a7bed69d9fda4124461a753d8fb37227955b0 Mon Sep 17 00:00:00 2001
From: Austin Mroczek <austin@mroczek.org>
Date: Fri, 14 Oct 2022 08:11:09 -0700
Subject: [PATCH 485/985] Bump total_connect_client to 2022.10 (#80331)

---
 homeassistant/components/totalconnect/manifest.json | 2 +-
 requirements_all.txt                                | 2 +-
 requirements_test_all.txt                           | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json
index 461bff0cfd0..71c11958d40 100644
--- a/homeassistant/components/totalconnect/manifest.json
+++ b/homeassistant/components/totalconnect/manifest.json
@@ -2,7 +2,7 @@
   "domain": "totalconnect",
   "name": "Total Connect",
   "documentation": "https://www.home-assistant.io/integrations/totalconnect",
-  "requirements": ["total_connect_client==2022.5"],
+  "requirements": ["total_connect_client==2022.10"],
   "dependencies": [],
   "codeowners": ["@austinmroczek"],
   "config_flow": true,
diff --git a/requirements_all.txt b/requirements_all.txt
index 92527521d97..911bd5573a7 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2425,7 +2425,7 @@ tololib==0.1.0b3
 toonapi==0.2.1
 
 # homeassistant.components.totalconnect
-total_connect_client==2022.5
+total_connect_client==2022.10
 
 # homeassistant.components.tplink_lte
 tp-connected==0.0.4
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 4ee1a844851..1ba291933cd 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1665,7 +1665,7 @@ tololib==0.1.0b3
 toonapi==0.2.1
 
 # homeassistant.components.totalconnect
-total_connect_client==2022.5
+total_connect_client==2022.10
 
 # homeassistant.components.transmission
 transmissionrpc==0.11
-- 
GitLab


From 6aa47e871de6c7382401fff515e1957bdb6851f5 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 14 Oct 2022 17:11:30 +0200
Subject: [PATCH 486/985] Little cleanup of Alert tests (#80333)

---
 tests/components/alert/test_init.py | 164 +++++++++++-----------------
 1 file changed, 62 insertions(+), 102 deletions(-)

diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py
index 3d2067a9ed9..f1543892b6b 100644
--- a/tests/components/alert/test_init.py
+++ b/tests/components/alert/test_init.py
@@ -28,9 +28,11 @@ from homeassistant.const import (
     STATE_OFF,
     STATE_ON,
 )
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant, ServiceCall
 from homeassistant.setup import async_setup_component
 
+from tests.common import async_mock_service
+
 NAME = "alert_test"
 DONE_MESSAGE = "alert_gone"
 NOTIFIER = "test"
@@ -71,49 +73,10 @@ TEST_NOACK = [
 ENTITY_ID = f"{DOMAIN}.{NAME}"
 
 
-@callback
-def async_turn_on(hass, entity_id):
-    """Async reset the alert.
-
-    This is a legacy helper method. Do not use it for new tests.
-    """
-    data = {ATTR_ENTITY_ID: entity_id}
-    hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
-
-
-@callback
-def async_turn_off(hass, entity_id):
-    """Async acknowledge the alert.
-
-    This is a legacy helper method. Do not use it for new tests.
-    """
-    data = {ATTR_ENTITY_ID: entity_id}
-    hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
-
-
-@callback
-def async_toggle(hass, entity_id):
-    """Async toggle acknowledgment of alert.
-
-    This is a legacy helper method. Do not use it for new tests.
-    """
-    data = {ATTR_ENTITY_ID: entity_id}
-    hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
-
-
 @pytest.fixture
-def mock_notifier(hass):
+def mock_notifier(hass: HomeAssistant) -> list[ServiceCall]:
     """Mock for notifier."""
-    events = []
-
-    @callback
-    def record_event(event):
-        """Add recorded event to set."""
-        events.append(event)
-
-    hass.services.async_register(notify.DOMAIN, NOTIFIER, record_event)
-
-    return events
+    return async_mock_service(hass, notify.DOMAIN, NOTIFIER)
 
 
 async def test_setup(hass):
@@ -135,8 +98,13 @@ async def test_silence(hass, mock_notifier):
     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)
-    await hass.async_block_till_done()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_TURN_OFF,
+        {ATTR_ENTITY_ID: ENTITY_ID},
+        blocking=True,
+    )
     assert hass.states.get(ENTITY_ID).state == STATE_OFF
 
     # alert should not be silenced on next fire
@@ -153,11 +121,22 @@ async def test_reset(hass, mock_notifier):
     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)
-    await hass.async_block_till_done()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_TURN_OFF,
+        {ATTR_ENTITY_ID: ENTITY_ID},
+        blocking=True,
+    )
+
     assert hass.states.get(ENTITY_ID).state == STATE_OFF
-    async_turn_on(hass, ENTITY_ID)
-    await hass.async_block_till_done()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_ENTITY_ID: ENTITY_ID},
+        blocking=True,
+    )
     assert hass.states.get(ENTITY_ID).state == STATE_ON
 
 
@@ -167,73 +146,63 @@ async def test_toggle(hass, mock_notifier):
     hass.states.async_set("sensor.test", STATE_ON)
     await hass.async_block_till_done()
     assert hass.states.get(ENTITY_ID).state == STATE_ON
-    async_toggle(hass, ENTITY_ID)
-    await hass.async_block_till_done()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_TOGGLE,
+        {ATTR_ENTITY_ID: ENTITY_ID},
+        blocking=True,
+    )
     assert hass.states.get(ENTITY_ID).state == STATE_OFF
-    async_toggle(hass, ENTITY_ID)
-    await hass.async_block_till_done()
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_TOGGLE,
+        {ATTR_ENTITY_ID: ENTITY_ID},
+        blocking=True,
+    )
     assert hass.states.get(ENTITY_ID).state == STATE_ON
 
 
-async def test_notification_no_done_message(hass):
+async def test_notification_no_done_message(
+    hass: HomeAssistant, mock_notifier: list[ServiceCall]
+) -> None:
     """Test notifications."""
-    events = []
     config = deepcopy(TEST_CONFIG)
     del config[DOMAIN][NAME][CONF_DONE_MESSAGE]
 
-    @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, config)
-    assert len(events) == 0
+    assert len(mock_notifier) == 0
 
     hass.states.async_set("sensor.test", STATE_ON)
     await hass.async_block_till_done()
-    assert len(events) == 1
+    assert len(mock_notifier) == 1
 
     hass.states.async_set("sensor.test", STATE_OFF)
     await hass.async_block_till_done()
-    assert len(events) == 1
+    assert len(mock_notifier) == 1
 
 
-async def test_notification(hass):
+async def test_notification(
+    hass: HomeAssistant, mock_notifier: list[ServiceCall]
+) -> None:
     """Test notifications."""
-    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, TEST_CONFIG)
-    assert len(events) == 0
+    assert len(mock_notifier) == 0
 
     hass.states.async_set("sensor.test", STATE_ON)
     await hass.async_block_till_done()
-    assert len(events) == 1
+    assert len(mock_notifier) == 1
 
     hass.states.async_set("sensor.test", STATE_OFF)
     await hass.async_block_till_done()
-    assert len(events) == 2
+    assert len(mock_notifier) == 2
 
 
-async def test_no_notifiers(hass: HomeAssistant) -> None:
+async def test_no_notifiers(
+    hass: HomeAssistant, mock_notifier: list[ServiceCall]
+) -> 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,
@@ -248,15 +217,15 @@ async def test_no_notifiers(hass: HomeAssistant) -> None:
             }
         },
     )
-    assert len(events) == 0
+    assert len(mock_notifier) == 0
 
     hass.states.async_set("sensor.test", STATE_ON)
     await hass.async_block_till_done()
-    assert len(events) == 0
+    assert len(mock_notifier) == 0
 
     hass.states.async_set("sensor.test", STATE_OFF)
     await hass.async_block_till_done()
-    assert len(events) == 0
+    assert len(mock_notifier) == 0
 
 
 async def test_sending_non_templated_notification(hass, mock_notifier):
@@ -324,25 +293,16 @@ async def test_sending_data_notification(hass, mock_notifier):
     assert last_event.data[notify.ATTR_DATA] == TEST_DATA
 
 
-async def test_skipfirst(hass):
+async def test_skipfirst(hass: HomeAssistant, mock_notifier: list[ServiceCall]) -> None:
     """Test skipping first notification."""
     config = deepcopy(TEST_CONFIG)
     config[DOMAIN][NAME][CONF_SKIP_FIRST] = True
-    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, config)
-    assert len(events) == 0
+    assert len(mock_notifier) == 0
 
     hass.states.async_set("sensor.test", STATE_ON)
     await hass.async_block_till_done()
-    assert len(events) == 0
+    assert len(mock_notifier) == 0
 
 
 async def test_done_message_state_tracker_reset_on_cancel(hass):
-- 
GitLab


From e01572bc44a28834b332845ec31249c9969f67f4 Mon Sep 17 00:00:00 2001
From: taiyeoguns <taiyeoguns@yahoo.com>
Date: Fri, 14 Oct 2022 16:59:09 +0100
Subject: [PATCH 487/985] Convert graphite tests to pytest (#79807)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 tests/components/graphite/test_init.py | 500 ++++++++++++++-----------
 1 file changed, 275 insertions(+), 225 deletions(-)

diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py
index 23a25b1623e..19c9ebd61e3 100644
--- a/tests/components/graphite/test_init.py
+++ b/tests/components/graphite/test_init.py
@@ -1,231 +1,281 @@
 """The tests for the Graphite component."""
+import asyncio
 import socket
-import unittest
 from unittest import mock
 from unittest.mock import patch
 
-import homeassistant.components.graphite as graphite
-from homeassistant.const import (
-    EVENT_HOMEASSISTANT_START,
-    EVENT_HOMEASSISTANT_STOP,
-    EVENT_STATE_CHANGED,
-    STATE_OFF,
-    STATE_ON,
+import pytest
+
+from homeassistant.components import graphite
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.setup import async_setup_component
+
+
+@pytest.fixture(name="mock_gf")
+def fixture_mock_gf():
+    """Mock Graphite Feeder fixture."""
+    with patch("homeassistant.components.graphite.GraphiteFeeder") as mock_gf:
+        yield mock_gf
+
+
+@pytest.fixture(name="mock_socket")
+def fixture_mock_socket():
+    """Mock socket fixture."""
+    with patch("socket.socket") as mock_socket:
+        yield mock_socket
+
+
+@pytest.fixture(name="mock_time")
+def fixture_mock_time():
+    """Mock time fixture."""
+    with patch("time.time") as mock_time:
+        yield mock_time
+
+
+async def test_setup(hass, mock_socket):
+    """Test setup."""
+    assert await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+    assert mock_socket.call_count == 1
+    assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
+
+
+async def test_setup_failure(hass, mock_socket):
+    """Test setup fails due to socket error."""
+    mock_socket.return_value.connect.side_effect = OSError
+    assert not await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+
+    assert mock_socket.call_count == 1
+    assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
+    assert mock_socket.return_value.connect.call_count == 1
+
+
+async def test_full_config(hass, mock_gf, mock_socket):
+    """Test setup with full configuration."""
+    config = {"graphite": {"host": "foo", "port": 123, "prefix": "me"}}
+
+    assert await async_setup_component(hass, graphite.DOMAIN, config)
+    assert mock_gf.call_count == 1
+    assert mock_gf.call_args == mock.call(hass, "foo", 123, "tcp", "me")
+    assert mock_socket.call_count == 1
+    assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
+
+
+async def test_full_udp_config(hass, mock_gf, mock_socket):
+    """Test setup with full configuration and UDP protocol."""
+    config = {
+        "graphite": {"host": "foo", "port": 123, "protocol": "udp", "prefix": "me"}
+    }
+
+    assert await async_setup_component(hass, graphite.DOMAIN, config)
+    assert mock_gf.call_count == 1
+    assert mock_gf.call_args == mock.call(hass, "foo", 123, "udp", "me")
+    assert mock_socket.call_count == 0
+
+
+async def test_config_port(hass, mock_gf, mock_socket):
+    """Test setup with invalid port."""
+    config = {"graphite": {"host": "foo", "port": 2003}}
+
+    assert await async_setup_component(hass, graphite.DOMAIN, config)
+    assert mock_gf.called
+    assert mock_socket.call_count == 1
+    assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
+
+
+async def test_start(hass, mock_socket, mock_time):
+    """Test the start."""
+    mock_time.return_value = 12345
+    assert await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+    await hass.async_block_till_done()
+    mock_socket.reset_mock()
+
+    await hass.async_start()
+
+    hass.states.async_set("test.entity", STATE_ON)
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 1
+    assert mock_socket.return_value.connect.call_args == mock.call(("localhost", 2003))
+    assert mock_socket.return_value.sendall.call_count == 1
+    assert mock_socket.return_value.sendall.call_args == mock.call(
+        b"ha.test.entity.state 1.000000 12345"
+    )
+    assert mock_socket.return_value.send.call_count == 1
+    assert mock_socket.return_value.send.call_args == mock.call(b"\n")
+    assert mock_socket.return_value.close.call_count == 1
+
+
+async def test_shutdown(hass, mock_socket, mock_time):
+    """Test the shutdown."""
+    mock_time.return_value = 12345
+    assert await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+    await hass.async_block_till_done()
+    mock_socket.reset_mock()
+
+    await hass.async_start()
+
+    hass.states.async_set("test.entity", STATE_ON)
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 1
+    assert mock_socket.return_value.connect.call_args == mock.call(("localhost", 2003))
+    assert mock_socket.return_value.sendall.call_count == 1
+    assert mock_socket.return_value.sendall.call_args == mock.call(
+        b"ha.test.entity.state 1.000000 12345"
+    )
+    assert mock_socket.return_value.send.call_count == 1
+    assert mock_socket.return_value.send.call_args == mock.call(b"\n")
+    assert mock_socket.return_value.close.call_count == 1
+
+    mock_socket.reset_mock()
+
+    await hass.async_stop()
+    await hass.async_block_till_done()
+
+    hass.states.async_set("test.entity", STATE_OFF)
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 0
+    assert mock_socket.return_value.sendall.call_count == 0
+
+
+async def test_report_attributes(hass, mock_socket, mock_time):
+    """Test the reporting with attributes."""
+    attrs = {"foo": 1, "bar": 2.0, "baz": True, "bat": "NaN"}
+    expected = [
+        "ha.test.entity.foo 1.000000 12345",
+        "ha.test.entity.bar 2.000000 12345",
+        "ha.test.entity.baz 1.000000 12345",
+        "ha.test.entity.state 1.000000 12345",
+    ]
+
+    mock_time.return_value = 12345
+    assert await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+    await hass.async_block_till_done()
+    mock_socket.reset_mock()
+
+    await hass.async_start()
+
+    hass.states.async_set("test.entity", STATE_ON, attrs)
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 1
+    assert mock_socket.return_value.connect.call_args == mock.call(("localhost", 2003))
+    assert mock_socket.return_value.sendall.call_count == 1
+    assert mock_socket.return_value.sendall.call_args == mock.call(
+        "\n".join(expected).encode("utf-8")
+    )
+    assert mock_socket.return_value.send.call_count == 1
+    assert mock_socket.return_value.send.call_args == mock.call(b"\n")
+    assert mock_socket.return_value.close.call_count == 1
+
+
+async def test_report_with_string_state(hass, mock_socket, mock_time):
+    """Test the reporting with strings."""
+    expected = [
+        "ha.test.entity.foo 1.000000 12345",
+        "ha.test.entity.state 1.000000 12345",
+    ]
+
+    mock_time.return_value = 12345
+    assert await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+    await hass.async_block_till_done()
+    mock_socket.reset_mock()
+
+    await hass.async_start()
+
+    hass.states.async_set("test.entity", "above_horizon", {"foo": 1.0})
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 1
+    assert mock_socket.return_value.connect.call_args == mock.call(("localhost", 2003))
+    assert mock_socket.return_value.sendall.call_count == 1
+    assert mock_socket.return_value.sendall.call_args == mock.call(
+        "\n".join(expected).encode("utf-8")
+    )
+    assert mock_socket.return_value.send.call_count == 1
+    assert mock_socket.return_value.send.call_args == mock.call(b"\n")
+    assert mock_socket.return_value.close.call_count == 1
+
+    mock_socket.reset_mock()
+
+    hass.states.async_set("test.entity", "not_float")
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 0
+    assert mock_socket.return_value.sendall.call_count == 0
+    assert mock_socket.return_value.send.call_count == 0
+    assert mock_socket.return_value.close.call_count == 0
+
+
+async def test_report_with_binary_state(hass, mock_socket, mock_time):
+    """Test the reporting with binary state."""
+    mock_time.return_value = 12345
+    assert await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+    await hass.async_block_till_done()
+    mock_socket.reset_mock()
+
+    await hass.async_start()
+
+    expected = [
+        "ha.test.entity.foo 1.000000 12345",
+        "ha.test.entity.state 1.000000 12345",
+    ]
+    hass.states.async_set("test.entity", STATE_ON, {"foo": 1.0})
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 1
+    assert mock_socket.return_value.connect.call_args == mock.call(("localhost", 2003))
+    assert mock_socket.return_value.sendall.call_count == 1
+    assert mock_socket.return_value.sendall.call_args == mock.call(
+        "\n".join(expected).encode("utf-8")
+    )
+    assert mock_socket.return_value.send.call_count == 1
+    assert mock_socket.return_value.send.call_args == mock.call(b"\n")
+    assert mock_socket.return_value.close.call_count == 1
+
+    mock_socket.reset_mock()
+
+    expected = [
+        "ha.test.entity.foo 1.000000 12345",
+        "ha.test.entity.state 0.000000 12345",
+    ]
+    hass.states.async_set("test.entity", STATE_OFF, {"foo": 1.0})
+    await asyncio.sleep(0.1)
+
+    assert mock_socket.return_value.connect.call_count == 1
+    assert mock_socket.return_value.connect.call_args == mock.call(("localhost", 2003))
+    assert mock_socket.return_value.sendall.call_count == 1
+    assert mock_socket.return_value.sendall.call_args == mock.call(
+        "\n".join(expected).encode("utf-8")
+    )
+    assert mock_socket.return_value.send.call_count == 1
+    assert mock_socket.return_value.send.call_args == mock.call(b"\n")
+    assert mock_socket.return_value.close.call_count == 1
+
+
+@pytest.mark.parametrize(
+    "error, log_text",
+    [
+        (OSError, "Failed to send data to graphite"),
+        (socket.gaierror, "Unable to connect to host"),
+        (Exception, "Failed to process STATE_CHANGED event"),
+    ],
 )
-import homeassistant.core as ha
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
-
-
-class TestGraphite(unittest.TestCase):
-    """Test the Graphite component."""
-
-    def setup_method(self, method):
-        """Set up things to be run when tests are started."""
-        self.hass = get_test_home_assistant()
-        self.gf = graphite.GraphiteFeeder(self.hass, "foo", 123, "tcp", "ha")
-
-    def teardown_method(self, method):
-        """Stop everything that was started."""
-        self.hass.stop()
-
-    @patch("socket.socket")
-    def test_setup(self, mock_socket):
-        """Test setup."""
-        assert setup_component(self.hass, graphite.DOMAIN, {"graphite": {}})
-        assert mock_socket.call_count == 1
-        assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
-
-    @patch("socket.socket")
-    @patch("homeassistant.components.graphite.GraphiteFeeder")
-    def test_full_config(self, mock_gf, mock_socket):
-        """Test setup with full configuration."""
-        config = {"graphite": {"host": "foo", "port": 123, "prefix": "me"}}
-
-        assert setup_component(self.hass, graphite.DOMAIN, config)
-        assert mock_gf.call_count == 1
-        assert mock_gf.call_args == mock.call(self.hass, "foo", 123, "tcp", "me")
-        assert mock_socket.call_count == 1
-        assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
-
-    @patch("socket.socket")
-    @patch("homeassistant.components.graphite.GraphiteFeeder")
-    def test_full_udp_config(self, mock_gf, mock_socket):
-        """Test setup with full configuration and UDP protocol."""
-        config = {
-            "graphite": {"host": "foo", "port": 123, "protocol": "udp", "prefix": "me"}
-        }
-
-        assert setup_component(self.hass, graphite.DOMAIN, config)
-        assert mock_gf.call_count == 1
-        assert mock_gf.call_args == mock.call(self.hass, "foo", 123, "udp", "me")
-        assert mock_socket.call_count == 0
-
-    @patch("socket.socket")
-    @patch("homeassistant.components.graphite.GraphiteFeeder")
-    def test_config_port(self, mock_gf, mock_socket):
-        """Test setup with invalid port."""
-        config = {"graphite": {"host": "foo", "port": 2003}}
-
-        assert setup_component(self.hass, graphite.DOMAIN, config)
-        assert mock_gf.called
-        assert mock_socket.call_count == 1
-        assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
-
-    def test_subscribe(self):
-        """Test the subscription."""
-        fake_hass = mock.MagicMock()
-        gf = graphite.GraphiteFeeder(fake_hass, "foo", 123, "tcp", "ha")
-        fake_hass.bus.listen_once.has_calls(
-            [
-                mock.call(EVENT_HOMEASSISTANT_START, gf.start_listen),
-                mock.call(EVENT_HOMEASSISTANT_STOP, gf.shutdown),
-            ]
-        )
-        assert fake_hass.bus.listen.call_count == 1
-        assert fake_hass.bus.listen.call_args == mock.call(
-            EVENT_STATE_CHANGED, gf.event_listener
-        )
-
-    def test_start(self):
-        """Test the start."""
-        with mock.patch.object(self.gf, "start") as mock_start:
-            self.gf.start_listen("event")
-            assert mock_start.call_count == 1
-            assert mock_start.call_args == mock.call()
-
-    def test_shutdown(self):
-        """Test the shutdown."""
-        with mock.patch.object(self.gf, "_queue") as mock_queue:
-            self.gf.shutdown("event")
-            assert mock_queue.put.call_count == 1
-            assert mock_queue.put.call_args == mock.call(self.gf._quit_object)
-
-    def test_event_listener(self):
-        """Test the event listener."""
-        with mock.patch.object(self.gf, "_queue") as mock_queue:
-            self.gf.event_listener("foo")
-            assert mock_queue.put.call_count == 1
-            assert mock_queue.put.call_args == mock.call("foo")
-
-    @patch("time.time")
-    def test_report_attributes(self, mock_time):
-        """Test the reporting with attributes."""
-        mock_time.return_value = 12345
-        attrs = {"foo": 1, "bar": 2.0, "baz": True, "bat": "NaN"}
-
-        expected = [
-            "ha.entity.state 0.000000 12345",
-            "ha.entity.foo 1.000000 12345",
-            "ha.entity.bar 2.000000 12345",
-            "ha.entity.baz 1.000000 12345",
-        ]
-
-        state = mock.MagicMock(state=0, attributes=attrs)
-        with mock.patch.object(self.gf, "_send_to_graphite") as mock_send:
-            self.gf._report_attributes("entity", state)
-            actual = mock_send.call_args_list[0][0][0].split("\n")
-            assert sorted(expected) == sorted(actual)
-
-    @patch("time.time")
-    def test_report_with_string_state(self, mock_time):
-        """Test the reporting with strings."""
-        mock_time.return_value = 12345
-        expected = ["ha.entity.foo 1.000000 12345", "ha.entity.state 1.000000 12345"]
-
-        state = mock.MagicMock(state="above_horizon", attributes={"foo": 1.0})
-        with mock.patch.object(self.gf, "_send_to_graphite") as mock_send:
-            self.gf._report_attributes("entity", state)
-            actual = mock_send.call_args_list[0][0][0].split("\n")
-            assert sorted(expected) == sorted(actual)
-
-    @patch("time.time")
-    def test_report_with_binary_state(self, mock_time):
-        """Test the reporting with binary state."""
-        mock_time.return_value = 12345
-        state = ha.State("domain.entity", STATE_ON, {"foo": 1.0})
-        with mock.patch.object(self.gf, "_send_to_graphite") as mock_send:
-            self.gf._report_attributes("entity", state)
-            expected = [
-                "ha.entity.foo 1.000000 12345",
-                "ha.entity.state 1.000000 12345",
-            ]
-            actual = mock_send.call_args_list[0][0][0].split("\n")
-            assert sorted(expected) == sorted(actual)
-
-        state.state = STATE_OFF
-        with mock.patch.object(self.gf, "_send_to_graphite") as mock_send:
-            self.gf._report_attributes("entity", state)
-            expected = [
-                "ha.entity.foo 1.000000 12345",
-                "ha.entity.state 0.000000 12345",
-            ]
-            actual = mock_send.call_args_list[0][0][0].split("\n")
-            assert sorted(expected) == sorted(actual)
-
-    @patch("time.time")
-    def test_send_to_graphite_errors(self, mock_time):
-        """Test the sending with errors."""
-        mock_time.return_value = 12345
-        state = ha.State("domain.entity", STATE_ON, {"foo": 1.0})
-        with mock.patch.object(self.gf, "_send_to_graphite") as mock_send:
-            mock_send.side_effect = socket.error
-            self.gf._report_attributes("entity", state)
-            mock_send.side_effect = socket.gaierror
-            self.gf._report_attributes("entity", state)
-
-    @patch("socket.socket")
-    def test_send_to_graphite(self, mock_socket):
-        """Test the sending of data."""
-        self.gf._send_to_graphite("foo")
-        assert mock_socket.call_count == 1
-        assert mock_socket.call_args == mock.call(socket.AF_INET, socket.SOCK_STREAM)
-        sock = mock_socket.return_value
-        assert sock.connect.call_count == 1
-        assert sock.connect.call_args == mock.call(("foo", 123))
-        assert sock.sendall.call_count == 1
-        assert sock.sendall.call_args == mock.call(b"foo")
-        assert sock.send.call_count == 1
-        assert sock.send.call_args == mock.call(b"\n")
-        assert sock.close.call_count == 1
-        assert sock.close.call_args == mock.call()
-
-    def test_run_stops(self):
-        """Test the stops."""
-        with mock.patch.object(self.gf, "_queue") as mock_queue:
-            mock_queue.get.return_value = self.gf._quit_object
-            assert self.gf.run() is None
-            assert mock_queue.get.call_count == 1
-            assert mock_queue.get.call_args == mock.call()
-            assert mock_queue.task_done.call_count == 1
-            assert mock_queue.task_done.call_args == mock.call()
-
-    def test_run(self):
-        """Test the running."""
-        runs = []
-        event = mock.MagicMock(
-            event_type=EVENT_STATE_CHANGED,
-            data={"entity_id": "entity", "new_state": mock.MagicMock()},
-        )
-
-        def fake_get():
-            if len(runs) >= 2:
-                return self.gf._quit_object
-            if runs:
-                runs.append(1)
-                return mock.MagicMock(
-                    event_type="somethingelse", data={"new_event": None}
-                )
-            runs.append(1)
-            return event
-
-        with mock.patch.object(self.gf, "_queue") as mock_queue, mock.patch.object(
-            self.gf, "_report_attributes"
-        ) as mock_r:
-            mock_queue.get.side_effect = fake_get
-            self.gf.run()
-            # Twice for two events, once for the stop
-            assert mock_queue.task_done.call_count == 3
-            assert mock_r.call_count == 1
-            assert mock_r.call_args == mock.call("entity", event.data["new_state"])
+async def test_send_to_graphite_errors(
+    hass, mock_socket, mock_time, caplog, error, log_text
+):
+    """Test the sending with errors."""
+    mock_time.return_value = 12345
+    assert await async_setup_component(hass, graphite.DOMAIN, {"graphite": {}})
+    await hass.async_block_till_done()
+    mock_socket.reset_mock()
+
+    await hass.async_start()
+
+    mock_socket.return_value.connect.side_effect = error
+
+    hass.states.async_set("test.entity", STATE_ON)
+    await asyncio.sleep(0.1)
+
+    assert log_text in caplog.text
-- 
GitLab


From 4ebf9df90191225accadcbcfd7333fec6c2e8fdb Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Fri, 14 Oct 2022 18:03:43 +0200
Subject: [PATCH 488/985] Unconditionally call DomainBlueprints.populate
 (#80336)

---
 .../components/automation/__init__.py         | 26 +++++++++----------
 homeassistant/components/blueprint/models.py  |  4 +++
 homeassistant/components/script/__init__.py   | 15 +++++------
 3 files changed, 23 insertions(+), 22 deletions(-)

diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index 9ad1f79354e..fed941dea1a 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -242,11 +242,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     # we will create entities before firing EVENT_COMPONENT_LOADED
     await async_process_integration_platform_for_component(hass, DOMAIN)
 
-    # To register the automation blueprints
+    # Register automation as valid domain for Blueprint
     async_get_blueprints(hass)
 
-    if not await _async_process_config(hass, config, component):
-        await async_get_blueprints(hass).async_populate()
+    await _async_process_config(hass, config, component)
+
+    # Add some default blueprints to blueprints/automation, does nothing
+    # if blueprints/automation already exists
+    await async_get_blueprints(hass).async_populate()
 
     async def trigger_service_handler(
         entity: AutomationEntity, service_call: ServiceCall
@@ -676,10 +679,9 @@ class AutomationEntityConfig:
 async def _prepare_automation_config(
     hass: HomeAssistant,
     config: ConfigType,
-) -> tuple[bool, list[AutomationEntityConfig]]:
+) -> list[AutomationEntityConfig]:
     """Parse configuration and prepare automation entity configuration."""
     automation_configs: list[AutomationEntityConfig] = []
-    blueprints_used = False
 
     for config_key in extract_domain_configs(config, DOMAIN):
         conf: list[ConfigType | blueprint.BlueprintInputs] = config[config_key]
@@ -688,7 +690,6 @@ async def _prepare_automation_config(
             raw_blueprint_inputs = None
             raw_config = None
             if isinstance(config_block, blueprint.BlueprintInputs):
-                blueprints_used = True
                 blueprint_inputs = config_block
                 raw_blueprint_inputs = blueprint_inputs.config_with_inputs
 
@@ -715,7 +716,7 @@ async def _prepare_automation_config(
                 )
             )
 
-    return (blueprints_used, automation_configs)
+    return automation_configs
 
 
 def _automation_name(automation_config: AutomationEntityConfig) -> str:
@@ -797,11 +798,8 @@ async def _async_process_config(
     hass: HomeAssistant,
     config: dict[str, Any],
     component: EntityComponent[AutomationEntity],
-) -> bool:
-    """Process config and add automations.
-
-    Returns if blueprints were used.
-    """
+) -> None:
+    """Process config and add automations."""
 
     def automation_matches_config(
         automation: AutomationEntity, config: AutomationEntityConfig
@@ -836,7 +834,7 @@ async def _async_process_config(
 
         return automation_matches, config_matches
 
-    blueprints_used, automation_configs = await _prepare_automation_config(hass, config)
+    automation_configs = await _prepare_automation_config(hass, config)
     automations: list[AutomationEntity] = list(component.entities)
 
     # Find automations and configurations which have matches
@@ -860,7 +858,7 @@ async def _async_process_config(
     if entities:
         await component.async_add_entities(entities)
 
-    return blueprints_used
+    return
 
 
 async def _async_process_if(
diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py
index bc0938b1097..7547a701220 100644
--- a/homeassistant/components/blueprint/models.py
+++ b/homeassistant/components/blueprint/models.py
@@ -339,6 +339,10 @@ class DomainBlueprints:
 
     async def async_populate(self) -> None:
         """Create folder if it doesn't exist and populate with examples."""
+        if self._blueprints:
+            # If we have already loaded some blueprint the blueprint folder must exist
+            return
+
         integration = await loader.async_get_integration(self.hass, self.domain)
 
         def populate():
diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py
index 27bc2556dc5..105309268d1 100644
--- a/homeassistant/components/script/__init__.py
+++ b/homeassistant/components/script/__init__.py
@@ -168,11 +168,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     # we will create entities before firing EVENT_COMPONENT_LOADED
     await async_process_integration_platform_for_component(hass, DOMAIN)
 
-    # To register scripts as valid domain for Blueprint
+    # Register script as valid domain for Blueprint
     async_get_blueprints(hass)
 
-    if not await _async_process_config(hass, config, component):
-        await async_get_blueprints(hass).async_populate()
+    await _async_process_config(hass, config, component)
+
+    # Add some default blueprints to blueprints/script, does nothing
+    # if blueprints/script already exists
+    await async_get_blueprints(hass).async_populate()
 
     async def reload_service(service: ServiceCall) -> None:
         """Call a service to reload scripts."""
@@ -228,13 +231,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     return True
 
 
-async def _async_process_config(hass, config, component) -> bool:
+async def _async_process_config(hass, config, component) -> None:
     """Process script configuration.
 
     Return true, if Blueprints were used.
     """
     entities = []
-    blueprints_used = False
 
     for config_key in extract_domain_configs(config, DOMAIN):
         conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key]
@@ -244,7 +246,6 @@ async def _async_process_config(hass, config, component) -> bool:
             raw_config = None
 
             if isinstance(config_block, BlueprintInputs):
-                blueprints_used = True
                 blueprint_inputs = config_block
                 raw_blueprint_inputs = blueprint_inputs.config_with_inputs
 
@@ -271,8 +272,6 @@ async def _async_process_config(hass, config, component) -> bool:
 
     await component.async_add_entities(entities)
 
-    return blueprints_used
-
 
 class ScriptEntity(ToggleEntity, RestoreEntity):
     """Representation of a script entity."""
-- 
GitLab


From e3af2cb6b877b0612f6cbf58938171e1bd065cde Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 14 Oct 2022 18:23:49 +0200
Subject: [PATCH 489/985] Add some typing to common test helpers (#80337)

---
 tests/common.py | 42 +++++++++++++++++++++++++++++-------------
 1 file changed, 29 insertions(+), 13 deletions(-)

diff --git a/tests/common.py b/tests/common.py
index cc2bc454810..4b357fe7033 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -20,6 +20,7 @@ from typing import Any
 from unittest.mock import AsyncMock, Mock, patch
 
 from aiohttp.test_utils import unused_port as get_test_instance_port  # noqa: F401
+import voluptuous as vol
 
 from homeassistant import auth, config_entries, core as ha, loader
 from homeassistant.auth import (
@@ -42,7 +43,7 @@ from homeassistant.const import (
     STATE_OFF,
     STATE_ON,
 )
-from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant
+from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant, ServiceCall, State
 from homeassistant.helpers import (
     area_registry,
     device_registry,
@@ -57,6 +58,7 @@ from homeassistant.helpers import (
 )
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.json import JSONEncoder
+from homeassistant.helpers.typing import ConfigType
 from homeassistant.setup import setup_component
 from homeassistant.util.async_ import run_callback_threadsafe
 import homeassistant.util.dt as date_util
@@ -328,7 +330,9 @@ async def async_test_home_assistant(loop, load_registries=True):
     return hass
 
 
-def async_mock_service(hass, domain, service, schema=None):
+def async_mock_service(
+    hass: HomeAssistant, domain: str, service: str, schema: vol.Schema | None = None
+) -> list[ServiceCall]:
     """Set up a fake service & return a calls log list to this service."""
     calls = []
 
@@ -417,18 +421,20 @@ def get_fixture_path(filename: str, integration: str | None = None) -> pathlib.P
 
     if integration is None:
         return pathlib.Path(__file__).parent.joinpath("fixtures", filename)
-    else:
-        return pathlib.Path(__file__).parent.joinpath(
-            "components", integration, "fixtures", filename
-        )
+
+    return pathlib.Path(__file__).parent.joinpath(
+        "components", integration, "fixtures", filename
+    )
 
 
-def load_fixture(filename, integration=None):
+def load_fixture(filename: str, integration: str | None = None) -> str:
     """Load a fixture."""
     return get_fixture_path(filename, integration).read_text()
 
 
-def mock_state_change_event(hass, new_state, old_state=None):
+def mock_state_change_event(
+    hass: HomeAssistant, new_state: State, old_state: State | None = None
+) -> None:
     """Mock state change envent."""
     event_data = {"entity_id": new_state.entity_id, "new_state": new_state}
 
@@ -439,7 +445,7 @@ def mock_state_change_event(hass, new_state, old_state=None):
 
 
 @ha.callback
-def mock_component(hass, component):
+def mock_component(hass: HomeAssistant, component: str) -> None:
     """Mock a component is setup."""
     if component in hass.config.components:
         AssertionError(f"Integration {component} is already setup")
@@ -447,7 +453,10 @@ def mock_component(hass, component):
     hass.config.components.add(component)
 
 
-def mock_registry(hass, mock_entries=None):
+def mock_registry(
+    hass: HomeAssistant,
+    mock_entries: dict[str, entity_registry.RegistryEntry] | None = None,
+) -> entity_registry.EntityRegistry:
     """Mock the Entity Registry."""
     registry = entity_registry.EntityRegistry(hass)
     if mock_entries is None:
@@ -460,7 +469,9 @@ def mock_registry(hass, mock_entries=None):
     return registry
 
 
-def mock_area_registry(hass, mock_entries=None):
+def mock_area_registry(
+    hass: HomeAssistant, mock_entries: dict[str, area_registry.AreaEntry] | None = None
+) -> area_registry.AreaRegistry:
     """Mock the Area Registry."""
     registry = area_registry.AreaRegistry(hass)
     registry.areas = mock_entries or OrderedDict()
@@ -469,7 +480,10 @@ def mock_area_registry(hass, mock_entries=None):
     return registry
 
 
-def mock_device_registry(hass, mock_entries=None):
+def mock_device_registry(
+    hass: HomeAssistant,
+    mock_entries: dict[str, device_registry.DeviceEntry] | None = None,
+) -> device_registry.DeviceRegistry:
     """Mock the Device Registry."""
     registry = device_registry.DeviceRegistry(hass)
     registry.devices = device_registry.DeviceRegistryItems()
@@ -545,7 +559,9 @@ class MockUser(auth_models.User):
         self._permissions = auth_permissions.PolicyPermissions(policy, self.perm_lookup)
 
 
-async def register_auth_provider(hass, config):
+async def register_auth_provider(
+    hass: HomeAssistant, config: ConfigType
+) -> auth_providers.AuthProvider:
     """Register an auth provider."""
     provider = await auth_providers.auth_provider_from_config(
         hass, hass.auth._store, config
-- 
GitLab


From a68bd8df6faec85242c5eccdcdc152ec836876b6 Mon Sep 17 00:00:00 2001
From: Charles Garwood <cgarwood@gmail.com>
Date: Fri, 14 Oct 2022 13:21:58 -0400
Subject: [PATCH 490/985] Add start_application service to fully_kiosk
 integration (#80226)

---
 homeassistant/components/fully_kiosk/const.py |  2 ++
 .../components/fully_kiosk/services.py        | 34 ++++++++++++++++++-
 .../components/fully_kiosk/services.yaml      | 15 ++++++++
 tests/components/fully_kiosk/test_services.py | 11 ++++++
 4 files changed, 61 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py
index b722d7fb4ca..b4fe90e01eb 100644
--- a/homeassistant/components/fully_kiosk/const.py
+++ b/homeassistant/components/fully_kiosk/const.py
@@ -24,5 +24,7 @@ MEDIA_SUPPORT_FULLYKIOSK = (
 )
 
 SERVICE_LOAD_URL = "load_url"
+SERVICE_START_APPLICATION = "start_application"
 
 ATTR_URL = "url"
+ATTR_APPLICATION = "application"
diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py
index 3d7d564f0b2..2283904dfa9 100644
--- a/homeassistant/components/fully_kiosk/services.py
+++ b/homeassistant/components/fully_kiosk/services.py
@@ -8,7 +8,13 @@ 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
+from .const import (
+    ATTR_APPLICATION,
+    ATTR_URL,
+    DOMAIN,
+    SERVICE_LOAD_URL,
+    SERVICE_START_APPLICATION,
+)
 
 
 async def async_setup_services(hass: HomeAssistant) -> None:
@@ -24,6 +30,16 @@ async def async_setup_services(hass: HomeAssistant) -> None:
                 coordinator = hass.data[DOMAIN][list(device.config_entries)[0]]
                 await coordinator.fully.loadUrl(call.data[ATTR_URL])
 
+    async def async_start_app(call: ServiceCall) -> None:
+        """Start an app on the device."""
+        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.startApplication(call.data[ATTR_APPLICATION])
+
     hass.services.async_register(
         DOMAIN,
         SERVICE_LOAD_URL,
@@ -39,3 +55,19 @@ async def async_setup_services(hass: HomeAssistant) -> None:
             )
         ),
     )
+
+    hass.services.async_register(
+        DOMAIN,
+        SERVICE_START_APPLICATION,
+        async_start_app,
+        schema=vol.Schema(
+            vol.All(
+                {
+                    vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
+                    vol.Required(
+                        ATTR_APPLICATION,
+                    ): cv.string,
+                },
+            )
+        ),
+    )
diff --git a/homeassistant/components/fully_kiosk/services.yaml b/homeassistant/components/fully_kiosk/services.yaml
index 53ce0a8aec8..b8ea6b371d7 100644
--- a/homeassistant/components/fully_kiosk/services.yaml
+++ b/homeassistant/components/fully_kiosk/services.yaml
@@ -12,3 +12,18 @@ load_url:
       required: true
       selector:
         text:
+
+start_application:
+  name: Start Application
+  description: Start an application on the device running Fully Kiosk Browser.
+  target:
+    device:
+      integration: fully_kiosk
+  fields:
+    url:
+      name: Application
+      description: Package name of the application to start.
+      example: "de.ozerov.fully"
+      required: true
+      selector:
+        text:
diff --git a/tests/components/fully_kiosk/test_services.py b/tests/components/fully_kiosk/test_services.py
index e3b63dad341..386bc542e3c 100644
--- a/tests/components/fully_kiosk/test_services.py
+++ b/tests/components/fully_kiosk/test_services.py
@@ -2,9 +2,11 @@
 from unittest.mock import MagicMock
 
 from homeassistant.components.fully_kiosk.const import (
+    ATTR_APPLICATION,
     ATTR_URL,
     DOMAIN,
     SERVICE_LOAD_URL,
+    SERVICE_START_APPLICATION,
 )
 from homeassistant.const import ATTR_DEVICE_ID
 from homeassistant.core import HomeAssistant
@@ -34,3 +36,12 @@ async def test_services(
     )
 
     assert len(mock_fully_kiosk.loadUrl.mock_calls) == 1
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_START_APPLICATION,
+        {ATTR_DEVICE_ID: [device_entry.id], ATTR_APPLICATION: "de.ozerov.fully"},
+        blocking=True,
+    )
+
+    assert len(mock_fully_kiosk.startApplication.mock_calls) == 1
-- 
GitLab


From 0c76e3a97e6bc36a5693565bce37b255561010b3 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 14 Oct 2022 08:39:18 -1000
Subject: [PATCH 491/985] Automatically determine the advertising interval for
 bluetooth devices (#79669)

---
 .../components/bluetooth/__init__.py          |   2 +
 .../bluetooth/advertisement_tracker.py        |  68 +++
 homeassistant/components/bluetooth/const.py   |  16 +-
 homeassistant/components/bluetooth/manager.py | 175 +++++---
 homeassistant/components/bluetooth/models.py  |   7 +-
 homeassistant/components/bluetooth/scanner.py |   6 +-
 .../components/esphome/bluetooth/scanner.py   |  25 +-
 .../bluetooth/test_advertisement_tracker.py   | 405 ++++++++++++++++++
 .../components/bluetooth/test_diagnostics.py  |  10 +
 tests/components/bluetooth/test_init.py       |   4 +-
 tests/components/bluetooth/test_manager.py    |   6 +-
 tests/components/bluetooth/test_models.py     |   4 +-
 12 files changed, 626 insertions(+), 102 deletions(-)
 create mode 100644 homeassistant/components/bluetooth/advertisement_tracker.py
 create mode 100644 tests/components/bluetooth/test_advertisement_tracker.py

diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py
index f175b01b798..1d0b8824fb5 100644
--- a/homeassistant/components/bluetooth/__init__.py
+++ b/homeassistant/components/bluetooth/__init__.py
@@ -39,6 +39,7 @@ from .const import (
     DATA_MANAGER,
     DEFAULT_ADDRESS,
     DOMAIN,
+    FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
     SOURCE_LOCAL,
     AdapterDetails,
 )
@@ -81,6 +82,7 @@ __all__ = [
     "BluetoothCallback",
     "HaBluetoothConnector",
     "SOURCE_LOCAL",
+    "FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS",
 ]
 
 _LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/bluetooth/advertisement_tracker.py b/homeassistant/components/bluetooth/advertisement_tracker.py
new file mode 100644
index 00000000000..f4577496e04
--- /dev/null
+++ b/homeassistant/components/bluetooth/advertisement_tracker.py
@@ -0,0 +1,68 @@
+"""The bluetooth integration advertisement tracker."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.core import callback
+
+from .models import BluetoothServiceInfoBleak
+
+ADVERTISING_TIMES_NEEDED = 16
+
+
+class AdvertisementTracker:
+    """Tracker to determine the interval that a device is advertising."""
+
+    def __init__(self) -> None:
+        """Initialize the tracker."""
+        self.intervals: dict[str, float] = {}
+        self.sources: dict[str, str] = {}
+        self._timings: dict[str, list[float]] = {}
+
+    @callback
+    def async_diagnostics(self) -> dict[str, dict[str, Any]]:
+        """Return diagnostics."""
+        return {
+            "intervals": self.intervals,
+            "sources": self.sources,
+            "timings": self._timings,
+        }
+
+    @callback
+    def async_collect(self, service_info: BluetoothServiceInfoBleak) -> None:
+        """Collect timings for the tracker.
+
+        For performance reasons, it is the responsibility of the
+        caller to check if the device already has an interval set or
+        the source has changed before calling this function.
+        """
+        address = service_info.address
+        self.sources[address] = service_info.source
+        timings = self._timings.setdefault(address, [])
+        timings.append(service_info.time)
+        if len(timings) != ADVERTISING_TIMES_NEEDED:
+            return
+
+        max_time_between_advertisements = timings[1] - timings[0]
+        for i in range(2, len(timings)):
+            time_between_advertisements = timings[i] - timings[i - 1]
+            if time_between_advertisements > max_time_between_advertisements:
+                max_time_between_advertisements = time_between_advertisements
+
+        # We now know the maximum time between advertisements
+        self.intervals[address] = max_time_between_advertisements
+        del self._timings[address]
+
+    @callback
+    def async_remove_address(self, address: str) -> None:
+        """Remove the tracker."""
+        self.intervals.pop(address, None)
+        self.sources.pop(address, None)
+        self._timings.pop(address, None)
+
+    @callback
+    def async_remove_source(self, source: str) -> None:
+        """Remove the tracker."""
+        for address, tracked_source in list(self.sources.items()):
+            if tracked_source == source:
+                self.async_remove_address(address)
diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py
index 4d4a096bb66..2ad05d80c7a 100644
--- a/homeassistant/components/bluetooth/const.py
+++ b/homeassistant/components/bluetooth/const.py
@@ -31,11 +31,17 @@ UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5
 
 START_TIMEOUT = 15
 
-MAX_DBUS_SETUP_SECONDS = 5
-
-# Anything after 30s is considered stale, we have buffer
-# for start timeouts and execution time
-STALE_ADVERTISEMENT_SECONDS: Final = 30 + START_TIMEOUT + MAX_DBUS_SETUP_SECONDS
+# The maximum time between advertisements for a device to be considered
+# stale when the advertisement tracker cannot determine the interval.
+#
+# We have to set this quite high as we don't know
+# when devices fall out of the ESPHome device (and other non-local scanners)'s
+# stack like we do with BlueZ so its safer to assume its available
+# since if it does go out of range and it is in range
+# of another device the timeout is much shorter and it will
+# switch over to using that adapter anyways.
+#
+FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 60 * 15
 
 
 # We must recover before we hit the 180s mark
diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index f0152f5ae5e..07330396bbd 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -6,6 +6,7 @@ from collections.abc import Callable, Iterable
 from datetime import datetime, timedelta
 import itertools
 import logging
+import time
 from typing import TYPE_CHECKING, Any, Final
 
 from bleak.backends.scanner import AdvertisementDataCallback
@@ -20,11 +21,12 @@ from homeassistant.core import (
 from homeassistant.helpers import discovery_flow
 from homeassistant.helpers.event import async_track_time_interval
 
+from .advertisement_tracker import AdvertisementTracker
 from .const import (
     ADAPTER_ADDRESS,
     ADAPTER_PASSIVE_SCAN,
+    FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
     NO_RSSI_VALUE,
-    STALE_ADVERTISEMENT_SECONDS,
     UNAVAILABLE_TRACK_SECONDS,
     AdapterDetails,
 )
@@ -66,47 +68,9 @@ APPLE_START_BYTES_WANTED: Final = {
 
 RSSI_SWITCH_THRESHOLD = 6
 
-_LOGGER = logging.getLogger(__name__)
-
+MONOTONIC_TIME: Final = time.monotonic
 
-def _prefer_previous_adv(
-    old: BluetoothServiceInfoBleak, new: BluetoothServiceInfoBleak
-) -> bool:
-    """Prefer previous advertisement if it is better."""
-    if new.time - old.time > STALE_ADVERTISEMENT_SECONDS:
-        # If the old advertisement is stale, any new advertisement is preferred
-        if new.source != old.source:
-            _LOGGER.debug(
-                "%s (%s): Switching from %s[%s] to %s[%s] (time elapsed:%s > stale seconds:%s)",
-                new.advertisement.local_name,
-                new.device.address,
-                old.source,
-                old.connectable,
-                new.source,
-                new.connectable,
-                new.time - old.time,
-                STALE_ADVERTISEMENT_SECONDS,
-            )
-        return False
-    if new.device.rssi - RSSI_SWITCH_THRESHOLD > (old.device.rssi or NO_RSSI_VALUE):
-        # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred
-        if new.source != old.source:
-            _LOGGER.debug(
-                "%s (%s): Switching from %s[%s] to %s[%s] (new rssi:%s - threshold:%s > old rssi:%s)",
-                new.advertisement.local_name,
-                new.device.address,
-                old.source,
-                old.connectable,
-                new.source,
-                new.connectable,
-                new.device.rssi,
-                RSSI_SWITCH_THRESHOLD,
-                old.device.rssi,
-            )
-        return False
-    # If the source is the different, the old one is preferred because its
-    # not stale and its RSSI_SWITCH_THRESHOLD less than the new one
-    return old.source != new.source
+_LOGGER = logging.getLogger(__name__)
 
 
 def _dispatch_bleak_callback(
@@ -142,13 +106,17 @@ class BluetoothManager:
         """Init bluetooth manager."""
         self.hass = hass
         self._integration_matcher = integration_matcher
-        self._cancel_unavailable_tracking: list[CALLBACK_TYPE] = []
+        self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None
+
+        self._advertisement_tracker = AdvertisementTracker()
+
         self._unavailable_callbacks: dict[
             str, list[Callable[[BluetoothServiceInfoBleak], None]]
         ] = {}
         self._connectable_unavailable_callbacks: dict[
             str, list[Callable[[BluetoothServiceInfoBleak], None]]
         ] = {}
+
         self._callback_index = BluetoothCallbackMatcherIndex()
         self._bleak_callbacks: list[
             tuple[AdvertisementDataCallback, dict[str, set[str]]]
@@ -190,6 +158,7 @@ class BluetoothManager:
             "history": [
                 service_info.as_dict() for service_info in self._history.values()
             ],
+            "advertisement_tracker": self._advertisement_tracker.async_diagnostics(),
         }
 
     def _find_adapter_by_address(self, address: str) -> str | None:
@@ -229,9 +198,8 @@ class BluetoothManager:
         """Stop the Bluetooth integration at shutdown."""
         _LOGGER.debug("Stopping bluetooth manager")
         if self._cancel_unavailable_tracking:
-            for cancel in self._cancel_unavailable_tracking:
-                cancel()
-            self._cancel_unavailable_tracking.clear()
+            self._cancel_unavailable_tracking()
+            self._cancel_unavailable_tracking = None
         uninstall_multiple_bleak_catcher()
 
     async def async_get_devices_by_address(
@@ -274,18 +242,24 @@ class BluetoothManager:
     @hass_callback
     def async_setup_unavailable_tracking(self) -> None:
         """Set up the unavailable tracking."""
-        self._async_setup_unavailable_tracking(True)
-        self._async_setup_unavailable_tracking(False)
+        self._cancel_unavailable_tracking = async_track_time_interval(
+            self.hass,
+            self._async_check_unavailable,
+            timedelta(seconds=UNAVAILABLE_TRACK_SECONDS),
+        )
 
     @hass_callback
-    def _async_setup_unavailable_tracking(self, connectable: bool) -> None:
-        """Set up the unavailable tracking."""
-        unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable)
-        history = self._get_history_by_type(connectable)
-
-        @hass_callback
-        def _async_check_unavailable(now: datetime) -> None:
-            """Watch for unavailable devices."""
+    def _async_check_unavailable(self, now: datetime) -> None:
+        """Watch for unavailable devices and cleanup state history."""
+        monotonic_now = MONOTONIC_TIME()
+        connectable_history = self._connectable_history
+        all_history = self._history
+        removed_addresses: set[str] = set()
+
+        for connectable in (True, False):
+            unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable)
+            intervals = self._advertisement_tracker.intervals
+            history = connectable_history if connectable else all_history
             history_set = set(history)
             active_addresses = {
                 device.address
@@ -293,35 +267,79 @@ class BluetoothManager:
             }
             disappeared = history_set.difference(active_addresses)
             for address in disappeared:
+                #
+                # For non-connectable devices we also check the device has exceeded
+                # the advertising interval before we mark it as unavailable
+                # since it may have gone to sleep and since we do not need an active connection
+                # to it we can only determine its availability by the lack of advertisements
+                #
+                if not connectable and (advertising_interval := intervals.get(address)):
+                    time_since_seen = monotonic_now - history[address].time
+                    if time_since_seen <= advertising_interval:
+                        continue
+
                 service_info = history.pop(address)
+                removed_addresses.add(address)
+
                 if not (callbacks := unavailable_callbacks.get(address)):
                     continue
+
                 for callback in callbacks:
                     try:
                         callback(service_info)
                     except Exception:  # pylint: disable=broad-except
                         _LOGGER.exception("Error in unavailable callback")
 
-        self._cancel_unavailable_tracking.append(
-            async_track_time_interval(
-                self.hass,
-                _async_check_unavailable,
-                timedelta(seconds=UNAVAILABLE_TRACK_SECONDS),
+        # If we removed the device from both the connectable history
+        # and all history then we can remove it from the advertisement tracker
+        for address in removed_addresses:
+            if address not in connectable_history and address not in all_history:
+                self._advertisement_tracker.async_remove_address(address)
+
+    def _prefer_previous_adv_from_different_source(
+        self, old: BluetoothServiceInfoBleak, new: BluetoothServiceInfoBleak
+    ) -> bool:
+        """Prefer previous advertisement from a different source if it is better."""
+        if new.time - old.time > (
+            stale_seconds := self._advertisement_tracker.intervals.get(
+                new.address, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
             )
-        )
+        ):
+            # If the old advertisement is stale, any new advertisement is preferred
+            _LOGGER.debug(
+                "%s (%s): Switching from %s[%s] to %s[%s] (time elapsed:%s > stale seconds:%s)",
+                new.advertisement.local_name,
+                new.device.address,
+                old.source,
+                old.connectable,
+                new.source,
+                new.connectable,
+                new.time - old.time,
+                stale_seconds,
+            )
+            return False
+        if new.device.rssi - RSSI_SWITCH_THRESHOLD > (old.device.rssi or NO_RSSI_VALUE):
+            # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred
+            _LOGGER.debug(
+                "%s (%s): Switching from %s[%s] to %s[%s] (new rssi:%s - threshold:%s > old rssi:%s)",
+                new.advertisement.local_name,
+                new.device.address,
+                old.source,
+                old.connectable,
+                new.source,
+                new.connectable,
+                new.device.rssi,
+                RSSI_SWITCH_THRESHOLD,
+                old.device.rssi,
+            )
+            return False
+        return True
 
     @hass_callback
     def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None:
         """Handle a new advertisement from any scanner.
 
         Callbacks from all the scanners arrive here.
-
-        In the future we will only process callbacks if
-
-        - The device is not in the history
-        - The RSSI is above a certain threshold better than
-          than the source from the history or the timestamp
-          in the history is older than 180s
         """
 
         # Pre-filter noisy apple devices as they can account for 20-35% of the
@@ -340,8 +358,14 @@ class BluetoothManager:
         connectable = service_info.connectable
         address = device.address
         all_history = self._connectable_history if connectable else self._history
-        old_service_info = all_history.get(address)
-        if old_service_info and _prefer_previous_adv(old_service_info, service_info):
+        source = service_info.source
+        if (
+            (old_service_info := all_history.get(address))
+            and source != old_service_info.source
+            and self._prefer_previous_adv_from_different_source(
+                old_service_info, service_info
+            )
+        ):
             return
 
         self._history[address] = service_info
@@ -350,6 +374,15 @@ class BluetoothManager:
             self._connectable_history[address] = service_info
             # Bleak callbacks must get a connectable device
 
+        # Track advertisement intervals to determine when we need to
+        # switch adapters or mark a device as unavailable
+        tracker = self._advertisement_tracker
+        if (last_source := tracker.sources.get(address)) and last_source != source:
+            # Source changed, remove the old address from the tracker
+            tracker.async_remove_address(address)
+        if address not in tracker.intervals:
+            tracker.async_collect(service_info)
+
         # If the advertisement data is the same as the last time we saw it, we
         # don't need to do anything else.
         if old_service_info and not (
@@ -360,7 +393,6 @@ class BluetoothManager:
         ):
             return
 
-        source = service_info.source
         if connectable:
             # Bleak callbacks must get a connectable device
             for callback_filters in self._bleak_callbacks:
@@ -515,6 +547,7 @@ class BluetoothManager:
         scanners = self._get_scanners_by_type(connectable)
 
         def _unregister_scanner() -> None:
+            self._advertisement_tracker.async_remove_source(scanner.source)
             scanners.remove(scanner)
 
         scanners.append(scanner)
diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py
index 9e93ea4d142..852ce4e47d3 100644
--- a/homeassistant/components/bluetooth/models.py
+++ b/homeassistant/components/bluetooth/models.py
@@ -20,7 +20,7 @@ from bleak.backends.scanner import (
 )
 from bleak_retry_connector import freshen_ble_device
 
-from homeassistant.core import CALLBACK_TYPE, callback as hass_callback
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
 from homeassistant.helpers.frame import report
 from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
 
@@ -105,6 +105,11 @@ class _HaWrappedBleakBackend:
 class BaseHaScanner:
     """Base class for Ha Scanners."""
 
+    def __init__(self, hass: HomeAssistant, source: str) -> None:
+        """Initialize the scanner."""
+        self.hass = hass
+        self.source = source
+
     @property
     @abstractmethod
     def discovered_devices(self) -> list[BLEDevice]:
diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py
index 9bc68059a7f..87c6a48380b 100644
--- a/homeassistant/components/bluetooth/scanner.py
+++ b/homeassistant/components/bluetooth/scanner.py
@@ -50,8 +50,6 @@ PASSIVE_SCANNER_ARGS = BlueZScannerArgs(
 _LOGGER = logging.getLogger(__name__)
 
 
-MONOTONIC_TIME = time.monotonic
-
 # If the adapter is in a stuck state the following errors are raised:
 NEED_RESET_ERRORS = [
     "org.bluez.Error.Failed",
@@ -130,7 +128,8 @@ class HaScanner(BaseHaScanner):
         address: str,
     ) -> None:
         """Init bluetooth discovery."""
-        self.hass = hass
+        source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL
+        super().__init__(hass, source)
         self.mode = mode
         self.adapter = adapter
         self._start_stop_lock = asyncio.Lock()
@@ -139,7 +138,6 @@ class HaScanner(BaseHaScanner):
         self._start_time = 0.0
         self._callbacks: list[Callable[[BluetoothServiceInfoBleak], None]] = []
         self.name = adapter_human_name(adapter, address)
-        self.source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL
 
     @property
     def discovered_devices(self) -> list[BLEDevice]:
diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py
index 36138192f8f..82a6bdfbece 100644
--- a/homeassistant/components/esphome/bluetooth/scanner.py
+++ b/homeassistant/components/esphome/bluetooth/scanner.py
@@ -11,19 +11,15 @@ from aioesphomeapi import BluetoothLEAdvertisement
 from bleak.backends.device import BLEDevice
 from bleak.backends.scanner import AdvertisementData
 
-from homeassistant.components.bluetooth import BaseHaScanner, HaBluetoothConnector
-from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
+from homeassistant.components.bluetooth import (
+    FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
+    BaseHaScanner,
+    BluetoothServiceInfoBleak,
+    HaBluetoothConnector,
+)
 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
 from homeassistant.helpers.event import async_track_time_interval
 
-# We have to set this quite high as we don't know
-# when devices fall out of the esphome device's stack
-# like we do with BlueZ so its safer to assume its available
-# since if it does go out of range and it is in range
-# of another device the timeout is much shorter and it will
-# switch over to using that adapter anyways.
-ADV_STALE_TIME = 60 * 15  # seconds
-
 TWO_CHAR = re.compile("..")
 
 
@@ -39,11 +35,10 @@ class ESPHomeScanner(BaseHaScanner):
         connectable: bool,
     ) -> None:
         """Initialize the scanner."""
-        self._hass = hass
+        super().__init__(hass, scanner_id)
         self._new_info_callback = new_info_callback
         self._discovered_devices: dict[str, BLEDevice] = {}
         self._discovered_device_timestamps: dict[str, float] = {}
-        self._source = scanner_id
         self._connector = connector
         self._connectable = connectable
         self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id}
@@ -54,7 +49,7 @@ class ESPHomeScanner(BaseHaScanner):
     def async_setup(self) -> CALLBACK_TYPE:
         """Set up the scanner."""
         return async_track_time_interval(
-            self._hass, self._async_expire_devices, timedelta(seconds=30)
+            self.hass, self._async_expire_devices, timedelta(seconds=30)
         )
 
     def _async_expire_devices(self, _datetime: datetime.datetime) -> None:
@@ -63,7 +58,7 @@ class ESPHomeScanner(BaseHaScanner):
         expired = [
             address
             for address, timestamp in self._discovered_device_timestamps.items()
-            if now - timestamp > ADV_STALE_TIME
+            if now - timestamp > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
         ]
         for address in expired:
             del self._discovered_devices[address]
@@ -113,7 +108,7 @@ class ESPHomeScanner(BaseHaScanner):
                 manufacturer_data=advertisement_data.manufacturer_data,
                 service_data=advertisement_data.service_data,
                 service_uuids=advertisement_data.service_uuids,
-                source=self._source,
+                source=self.source,
                 device=device,
                 advertisement=advertisement_data,
                 connectable=self._connectable,
diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py
new file mode 100644
index 00000000000..6e9671cfe4e
--- /dev/null
+++ b/tests/components/bluetooth/test_advertisement_tracker.py
@@ -0,0 +1,405 @@
+"""Tests for the Bluetooth integration advertisement tracking."""
+
+from datetime import timedelta
+import time
+from unittest.mock import patch
+
+from bleak.backends.scanner import AdvertisementData, BLEDevice
+
+from homeassistant.components.bluetooth import (
+    async_register_scanner,
+    async_track_unavailable,
+)
+from homeassistant.components.bluetooth.advertisement_tracker import (
+    ADVERTISING_TIMES_NEEDED,
+)
+from homeassistant.components.bluetooth.const import (
+    SOURCE_LOCAL,
+    UNAVAILABLE_TRACK_SECONDS,
+)
+from homeassistant.components.bluetooth.models import BaseHaScanner
+from homeassistant.core import callback
+from homeassistant.util import dt as dt_util
+
+from . import inject_advertisement_with_time_and_source
+
+from tests.common import async_fire_time_changed
+
+ONE_HOUR_SECONDS = 3600
+
+
+async def test_advertisment_interval_shorter_than_adapter_stack_timeout(
+    hass, caplog, enable_bluetooth, macos_adapter
+):
+    """Test we can determine the advertisement interval."""
+    start_monotonic_time = time.monotonic()
+    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
+    switchbot_adv = AdvertisementData(
+        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    )
+    switchbot_device_went_unavailable = False
+
+    @callback
+    def _switchbot_device_unavailable_callback(_address: str) -> None:
+        """Switchbot device unavailable callback."""
+        nonlocal switchbot_device_went_unavailable
+        switchbot_device_went_unavailable = True
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * 2),
+            SOURCE_LOCAL,
+        )
+
+    switchbot_device_unavailable_cancel = async_track_unavailable(
+        hass, _switchbot_device_unavailable_callback, switchbot_device.address
+    )
+
+    monotonic_now = start_monotonic_time + ((ADVERTISING_TIMES_NEEDED - 1) * 2)
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is True
+    switchbot_device_unavailable_cancel()
+
+
+async def test_advertisment_interval_longer_than_adapter_stack_timeout_connectable(
+    hass, caplog, enable_bluetooth, macos_adapter
+):
+    """Test device with a long advertisement interval."""
+    start_monotonic_time = time.monotonic()
+    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
+    switchbot_adv = AdvertisementData(
+        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    )
+    switchbot_device_went_unavailable = False
+
+    @callback
+    def _switchbot_device_unavailable_callback(_address: str) -> None:
+        """Switchbot device unavailable callback."""
+        nonlocal switchbot_device_went_unavailable
+        switchbot_device_went_unavailable = True
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * ONE_HOUR_SECONDS),
+            SOURCE_LOCAL,
+        )
+
+    switchbot_device_unavailable_cancel = async_track_unavailable(
+        hass, _switchbot_device_unavailable_callback, switchbot_device.address
+    )
+
+    monotonic_now = start_monotonic_time + (
+        (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS
+    )
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is True
+    switchbot_device_unavailable_cancel()
+
+
+async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_change_connectable(
+    hass, caplog, enable_bluetooth, macos_adapter
+):
+    """Test device with a long advertisement interval with an adapter change."""
+    start_monotonic_time = time.monotonic()
+    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
+    switchbot_adv = AdvertisementData(
+        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    )
+    switchbot_device_went_unavailable = False
+
+    @callback
+    def _switchbot_device_unavailable_callback(_address: str) -> None:
+        """Switchbot device unavailable callback."""
+        nonlocal switchbot_device_went_unavailable
+        switchbot_device_went_unavailable = True
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * 2),
+            "original",
+        )
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * ONE_HOUR_SECONDS),
+            "new",
+        )
+
+    switchbot_device_unavailable_cancel = async_track_unavailable(
+        hass, _switchbot_device_unavailable_callback, switchbot_device.address
+    )
+
+    monotonic_now = start_monotonic_time + (
+        (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS
+    )
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is True
+    switchbot_device_unavailable_cancel()
+
+
+async def test_advertisment_interval_longer_than_adapter_stack_timeout_not_connectable(
+    hass, caplog, enable_bluetooth, macos_adapter
+):
+    """Test device with a long advertisement interval that is not connectable not reaching the advertising interval."""
+    start_monotonic_time = time.monotonic()
+    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
+    switchbot_adv = AdvertisementData(
+        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    )
+    switchbot_device_went_unavailable = False
+
+    @callback
+    def _switchbot_device_unavailable_callback(_address: str) -> None:
+        """Switchbot device unavailable callback."""
+        nonlocal switchbot_device_went_unavailable
+        switchbot_device_went_unavailable = True
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * ONE_HOUR_SECONDS),
+            SOURCE_LOCAL,
+        )
+
+    switchbot_device_unavailable_cancel = async_track_unavailable(
+        hass,
+        _switchbot_device_unavailable_callback,
+        switchbot_device.address,
+        connectable=False,
+    )
+
+    monotonic_now = start_monotonic_time + (
+        (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS
+    )
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is False
+    switchbot_device_unavailable_cancel()
+
+
+async def test_advertisment_interval_shorter_than_adapter_stack_timeout_adapter_change_not_connectable(
+    hass, caplog, enable_bluetooth, macos_adapter
+):
+    """Test device with a short advertisement interval with an adapter change that is not connectable."""
+    start_monotonic_time = time.monotonic()
+    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
+    switchbot_adv = AdvertisementData(
+        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    )
+    switchbot_device_went_unavailable = False
+
+    @callback
+    def _switchbot_device_unavailable_callback(_address: str) -> None:
+        """Switchbot device unavailable callback."""
+        nonlocal switchbot_device_went_unavailable
+        switchbot_device_went_unavailable = True
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * ONE_HOUR_SECONDS),
+            "original",
+        )
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass, switchbot_device, switchbot_adv, start_monotonic_time + (i * 2), "new"
+        )
+
+    switchbot_device_unavailable_cancel = async_track_unavailable(
+        hass,
+        _switchbot_device_unavailable_callback,
+        switchbot_device.address,
+        connectable=False,
+    )
+
+    monotonic_now = start_monotonic_time + (
+        (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS
+    )
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is True
+    switchbot_device_unavailable_cancel()
+
+
+async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_change_not_connectable(
+    hass, caplog, enable_bluetooth, macos_adapter
+):
+    """Test device with a long advertisement interval with an adapter change that is not connectable."""
+    start_monotonic_time = time.monotonic()
+    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
+    switchbot_adv = AdvertisementData(
+        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    )
+    switchbot_device_went_unavailable = False
+
+    class FakeScanner(BaseHaScanner):
+        """Fake scanner."""
+
+        @property
+        def discovered_devices(self) -> list[BLEDevice]:
+            return []
+
+    scanner = FakeScanner(hass, "new")
+    cancel_scanner = async_register_scanner(hass, scanner, False)
+
+    @callback
+    def _switchbot_device_unavailable_callback(_address: str) -> None:
+        """Switchbot device unavailable callback."""
+        nonlocal switchbot_device_went_unavailable
+        switchbot_device_went_unavailable = True
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * 2),
+            "original",
+        )
+
+    for i in range(ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i * ONE_HOUR_SECONDS),
+            "new",
+        )
+
+    switchbot_device_unavailable_cancel = async_track_unavailable(
+        hass,
+        _switchbot_device_unavailable_callback,
+        switchbot_device.address,
+        connectable=False,
+    )
+
+    monotonic_now = start_monotonic_time + (
+        (ADVERTISING_TIMES_NEEDED - 1) * ONE_HOUR_SECONDS
+    )
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is False
+    cancel_scanner()
+
+    # Now that the scanner is gone we should go back to the stack default timeout
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is True
+
+    switchbot_device_unavailable_cancel()
+
+
+async def test_advertisment_interval_longer_increasing_than_adapter_stack_timeout_adapter_change_not_connectable(
+    hass, caplog, enable_bluetooth, macos_adapter
+):
+    """Test device with a increasing advertisement interval with an adapter change that is not connectable."""
+    start_monotonic_time = time.monotonic()
+    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
+    switchbot_adv = AdvertisementData(
+        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    )
+    switchbot_device_went_unavailable = False
+
+    @callback
+    def _switchbot_device_unavailable_callback(_address: str) -> None:
+        """Switchbot device unavailable callback."""
+        nonlocal switchbot_device_went_unavailable
+        switchbot_device_went_unavailable = True
+
+    for i in range(ADVERTISING_TIMES_NEEDED, 2 * ADVERTISING_TIMES_NEEDED):
+        inject_advertisement_with_time_and_source(
+            hass,
+            switchbot_device,
+            switchbot_adv,
+            start_monotonic_time + (i**2),
+            "new",
+        )
+
+    switchbot_device_unavailable_cancel = async_track_unavailable(
+        hass,
+        _switchbot_device_unavailable_callback,
+        switchbot_device.address,
+        connectable=False,
+    )
+
+    monotonic_now = start_monotonic_time + UNAVAILABLE_TRACK_SECONDS + 1
+    with patch(
+        "homeassistant.components.bluetooth.manager.MONOTONIC_TIME",
+        return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS,
+    ):
+        async_fire_time_changed(
+            hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS)
+        )
+        await hass.async_block_till_done()
+
+    assert switchbot_device_went_unavailable is False
+    switchbot_device_unavailable_cancel()
diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py
index 1da071a76ab..7e2f15a984f 100644
--- a/tests/components/bluetooth/test_diagnostics.py
+++ b/tests/components/bluetooth/test_diagnostics.py
@@ -96,6 +96,11 @@ async def test_diagnostics(
                 }
             },
             "manager": {
+                "advertisement_tracker": {
+                    "intervals": {},
+                    "sources": {},
+                    "timings": {},
+                },
                 "adapters": {
                     "hci0": {
                         "address": "00:00:00:00:00:01",
@@ -198,6 +203,11 @@ async def test_diagnostics_macos(
                 }
             },
             "manager": {
+                "advertisement_tracker": {
+                    "intervals": {},
+                    "sources": {"44:44:33:11:23:45": "local"},
+                    "timings": {"44:44:33:11:23:45": [ANY]},
+                },
                 "adapters": {
                     "Core Bluetooth": {
                         "address": "00:00:00:00:00:00",
diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py
index 2e311d9d97e..746f3004c30 100644
--- a/tests/components/bluetooth/test_init.py
+++ b/tests/components/bluetooth/test_init.py
@@ -2595,7 +2595,7 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu
 
 async def test_scanner_count_connectable(hass, enable_bluetooth):
     """Test getting the connectable scanner count."""
-    scanner = models.BaseHaScanner()
+    scanner = models.BaseHaScanner(hass, "any")
     cancel = bluetooth.async_register_scanner(hass, scanner, False)
     assert bluetooth.async_scanner_count(hass, connectable=True) == 1
     cancel()
@@ -2603,7 +2603,7 @@ async def test_scanner_count_connectable(hass, enable_bluetooth):
 
 async def test_scanner_count(hass, enable_bluetooth):
     """Test getting the connectable and non-connectable scanner count."""
-    scanner = models.BaseHaScanner()
+    scanner = models.BaseHaScanner(hass, "any")
     cancel = bluetooth.async_register_scanner(hass, scanner, False)
     assert bluetooth.async_scanner_count(hass, connectable=False) == 2
     cancel()
diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py
index f3f3d1b3664..4e5ab24b80f 100644
--- a/tests/components/bluetooth/test_manager.py
+++ b/tests/components/bluetooth/test_manager.py
@@ -6,7 +6,9 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice
 from bluetooth_adapters import AdvertisementHistory
 
 from homeassistant.components import bluetooth
-from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS
+from homeassistant.components.bluetooth.manager import (
+    FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
+)
 from homeassistant.setup import async_setup_component
 
 from . import (
@@ -227,7 +229,7 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth):
         hass,
         switchbot_device_poor_signal_hci1,
         switchbot_adv_poor_signal_hci1,
-        start_time_monotonic + STALE_ADVERTISEMENT_SECONDS + 1,
+        start_time_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1,
         "hci1",
     )
 
diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py
index d126dcac301..e0e782dff84 100644
--- a/tests/components/bluetooth/test_models.py
+++ b/tests/components/bluetooth/test_models.py
@@ -204,7 +204,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
                 return switchbot_proxy_device_has_connection_slot
             return None
 
-    scanner = FakeScanner()
+    scanner = FakeScanner(hass, "esp32")
     cancel = manager.async_register_scanner(scanner, True)
     assert manager.async_discovered_devices(True) == [
         switchbot_proxy_device_no_connection_slot
@@ -290,7 +290,7 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
                 return switchbot_proxy_device_has_connection_slot
             return None
 
-    scanner = FakeScanner()
+    scanner = FakeScanner(hass, "esp32")
     cancel = manager.async_register_scanner(scanner, True)
     assert manager.async_discovered_devices(True) == [
         switchbot_proxy_device_no_connection_slot
-- 
GitLab


From 20f1f8aabb51a897d555996645419b976f774a98 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 14 Oct 2022 21:26:55 +0200
Subject: [PATCH 492/985] Upgrade demetriek to 0.3.0 (#80350)

---
 homeassistant/components/lametric/manifest.json | 2 +-
 requirements_all.txt                            | 2 +-
 requirements_test_all.txt                       | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json
index fb9f0bf03b2..50c70943a88 100644
--- a/homeassistant/components/lametric/manifest.json
+++ b/homeassistant/components/lametric/manifest.json
@@ -2,7 +2,7 @@
   "domain": "lametric",
   "name": "LaMetric",
   "documentation": "https://www.home-assistant.io/integrations/lametric",
-  "requirements": ["demetriek==0.2.4"],
+  "requirements": ["demetriek==0.3.0"],
   "codeowners": ["@robbiet480", "@frenck", "@bachya"],
   "iot_class": "local_polling",
   "dependencies": ["application_credentials"],
diff --git a/requirements_all.txt b/requirements_all.txt
index 911bd5573a7..af46d0643d3 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -563,7 +563,7 @@ defusedxml==0.7.1
 deluge-client==1.7.1
 
 # homeassistant.components.lametric
-demetriek==0.2.4
+demetriek==0.3.0
 
 # homeassistant.components.denonavr
 denonavr==0.10.11
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 1ba291933cd..7ecdb7bd6aa 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -437,7 +437,7 @@ defusedxml==0.7.1
 deluge-client==1.7.1
 
 # homeassistant.components.lametric
-demetriek==0.2.4
+demetriek==0.3.0
 
 # homeassistant.components.denonavr
 denonavr==0.10.11
-- 
GitLab


From 3b33e0d832b238b40360383099391e2093ea05cb Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 14 Oct 2022 09:58:09 -1000
Subject: [PATCH 493/985] Add support for restoring HomeKit IIDs (#79913)

---
 homeassistant/components/homekit/__init__.py  |  64 +++++++++--
 .../components/homekit/accessories.py         |  38 ++++++-
 .../components/homekit/diagnostics.py         |   2 +-
 .../components/homekit/iidmanager.py          |  96 +++++++++++++++++
 homeassistant/components/homekit/type_fans.py |   4 +-
 .../components/homekit/type_media_players.py  |  16 ++-
 .../components/homekit/type_remotes.py        |   2 +-
 .../components/homekit/type_switches.py       |   2 +-
 .../components/homekit/type_triggers.py       |   6 +-
 homeassistant/components/homekit/util.py      |  29 +++--
 tests/components/homekit/conftest.py          |  63 +++++++++--
 tests/components/homekit/test_accessories.py  |  38 +++++--
 tests/components/homekit/test_config_flow.py  |  33 +++++-
 tests/components/homekit/test_homekit.py      |  89 ++++++++++-----
 tests/components/homekit/test_iidmanager.py   | 101 ++++++++++++++++++
 tests/components/homekit/test_type_cameras.py |  16 ---
 tests/components/homekit/test_type_covers.py  |   4 +-
 tests/components/homekit/test_type_fans.py    |   2 +-
 .../homekit/test_type_media_players.py        |   2 +-
 .../homekit/test_type_security_systems.py     |   5 +-
 tests/components/homekit/test_type_sensors.py |   6 +-
 .../components/homekit/test_type_switches.py  |   8 +-
 .../homekit/test_type_thermostats.py          |   4 +-
 23 files changed, 527 insertions(+), 103 deletions(-)
 create mode 100644 homeassistant/components/homekit/iidmanager.py
 create mode 100644 tests/components/homekit/test_iidmanager.py

diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index 36c7bac9d0a..b809f6db205 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -74,7 +74,13 @@ from . import (  # noqa: F401
     type_switches,
     type_thermostats,
 )
-from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
+from .accessories import (
+    HomeAccessory,
+    HomeBridge,
+    HomeDriver,
+    HomeIIDManager,
+    get_accessory,
+)
 from .aidmanager import AccessoryAidStorage
 from .const import (
     ATTR_INTEGRATION,
@@ -107,6 +113,7 @@ from .const import (
     SERVICE_HOMEKIT_UNPAIR,
     SHUTDOWN_TIMEOUT,
 )
+from .iidmanager import AccessoryIIDStorage
 from .type_triggers import DeviceTriggerAccessory
 from .util import (
     accessory_friendly_name,
@@ -489,8 +496,6 @@ def _async_register_events_and_services(hass: HomeAssistant) -> None:
 class HomeKit:
     """Class to handle all actions between HomeKit and Home Assistant."""
 
-    driver: HomeDriver
-
     def __init__(
         self,
         hass: HomeAssistant,
@@ -520,12 +525,14 @@ class HomeKit:
         self._homekit_mode = homekit_mode
         self._devices = devices or []
         self.aid_storage: AccessoryAidStorage | None = None
+        self.iid_storage: AccessoryIIDStorage | None = None
         self.status = STATUS_READY
-
+        self.driver: HomeDriver | None = None
         self.bridge: HomeBridge | None = None
 
     def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
         """Set up bridge and accessory driver."""
+        assert self.iid_storage is not None
         persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id)
 
         self.driver = HomeDriver(
@@ -541,6 +548,7 @@ class HomeKit:
             async_zeroconf_instance=async_zeroconf_instance,
             zeroconf_server=f"{uuid}-hap.local.",
             loader=get_loader(),
+            iid_manager=HomeIIDManager(self.iid_storage),
         )
 
         # If we do not load the mac address will be wrong
@@ -555,14 +563,27 @@ class HomeKit:
             return
         await self.async_reset_accessories_in_bridge_mode(entity_ids)
 
+    async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
+        """Shutdown an accessory."""
+        assert self.driver is not None
+        await accessory.stop()
+        # Deallocate the IIDs for the accessory
+        iid_manager = self.driver.iid_manager
+        for service in accessory.services:
+            iid_manager.remove_iid(iid_manager.remove_obj(service))
+            for char in service.characteristics:
+                iid_manager.remove_iid(iid_manager.remove_obj(char))
+
     async def async_reset_accessories_in_accessory_mode(
         self, entity_ids: Iterable[str]
     ) -> None:
         """Reset accessories in accessory mode."""
+        assert self.driver is not None
+
         acc = cast(HomeAccessory, self.driver.accessory)
+        await self._async_shutdown_accessory(acc)
         if acc.entity_id not in entity_ids:
             return
-        await acc.stop()
         if not (state := self.hass.states.get(acc.entity_id)):
             _LOGGER.warning(
                 "The underlying entity %s disappeared during reset", acc.entity_id
@@ -579,6 +600,8 @@ class HomeKit:
         """Reset accessories in bridge mode."""
         assert self.aid_storage is not None
         assert self.bridge is not None
+        assert self.driver is not None
+
         new = []
         acc: HomeAccessory | None
         for entity_id in entity_ids:
@@ -590,9 +613,10 @@ class HomeKit:
                 self._name,
                 entity_id,
             )
-            if (acc := await self.async_remove_bridge_accessory(aid)) and (
-                state := self.hass.states.get(acc.entity_id)
-            ):
+            acc = await self.async_remove_bridge_accessory(aid)
+            if acc:
+                await self._async_shutdown_accessory(acc)
+            if acc and (state := self.hass.states.get(acc.entity_id)):
                 new.append(state)
             else:
                 _LOGGER.warning(
@@ -612,10 +636,13 @@ class HomeKit:
 
     async def async_config_changed(self) -> None:
         """Call config changed which writes out the new config to disk."""
+        assert self.driver is not None
         await self.hass.async_add_executor_job(self.driver.config_changed)
 
     def add_bridge_accessory(self, state: State) -> HomeAccessory | None:
         """Try adding accessory to bridge if configured beforehand."""
+        assert self.driver is not None
+
         if self._would_exceed_max_devices(state.entity_id):
             return None
 
@@ -694,7 +721,6 @@ class HomeKit:
         """Try adding accessory to bridge if configured beforehand."""
         assert self.bridge is not None
         if acc := self.bridge.accessories.pop(aid, None):
-            await acc.stop()
             return cast(HomeAccessory, acc)
         return None
 
@@ -741,9 +767,14 @@ class HomeKit:
         self.status = STATUS_WAIT
         async_zc_instance = await zeroconf.async_get_async_instance(self.hass)
         uuid = await instance_id.async_get(self.hass)
-        await self.hass.async_add_executor_job(self.setup, async_zc_instance, uuid)
         self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id)
+        self.iid_storage = AccessoryIIDStorage(self.hass, self._entry_id)
+        # Avoid gather here since it will be I/O bound anyways
         await self.aid_storage.async_initialize()
+        await self.iid_storage.async_initialize()
+        await self.hass.async_add_executor_job(self.setup, async_zc_instance, uuid)
+        assert self.driver is not None
+
         if not await self._async_create_accessories():
             return
         self._async_register_bridge()
@@ -760,6 +791,8 @@ class HomeKit:
     @callback
     def _async_show_setup_message(self) -> None:
         """Show the pairing setup message."""
+        assert self.driver is not None
+
         async_show_setup_message(
             self.hass,
             self._entry_id,
@@ -771,6 +804,8 @@ class HomeKit:
     @callback
     def async_unpair(self) -> None:
         """Remove all pairings for an accessory so it can be repaired."""
+        assert self.driver is not None
+
         state = self.driver.state
         for client_uuid in list(state.paired_clients):
             # We need to check again since removing a single client
@@ -842,6 +877,8 @@ class HomeKit:
         self, entity_states: list[State]
     ) -> HomeAccessory | None:
         """Create a single HomeKit accessory (accessory mode)."""
+        assert self.driver is not None
+
         if not entity_states:
             _LOGGER.error(
                 "HomeKit %s cannot startup: entity not available: %s",
@@ -864,6 +901,8 @@ class HomeKit:
         self, entity_states: Iterable[State]
     ) -> HomeAccessory:
         """Create a HomeKit bridge with accessories. (bridge mode)."""
+        assert self.driver is not None
+
         self.bridge = HomeBridge(self.hass, self.driver, self._name)
         for state in entity_states:
             self.add_bridge_accessory(state)
@@ -892,6 +931,8 @@ class HomeKit:
 
     async def _async_create_accessories(self) -> bool:
         """Create the accessories."""
+        assert self.driver is not None
+
         entity_states = await self.async_configure_accessories()
         if self._homekit_mode == HOMEKIT_MODE_ACCESSORY:
             acc = self._async_create_single_accessory(entity_states)
@@ -910,7 +951,8 @@ class HomeKit:
             return
         self.status = STATUS_STOPPED
         _LOGGER.debug("Driver stop for %s", self._name)
-        await self.driver.async_stop()
+        if self.driver:
+            await self.driver.async_stop()
 
     @callback
     def _async_configure_linked_sensors(
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index 9d428573b5b..61c2e3cd5dd 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -7,7 +7,10 @@ from uuid import UUID
 
 from pyhap.accessory import Accessory, Bridge
 from pyhap.accessory_driver import AccessoryDriver
+from pyhap.characteristic import Characteristic
 from pyhap.const import CATEGORY_OTHER
+from pyhap.iid_manager import IIDManager
+from pyhap.service import Service
 from pyhap.util import callback as pyhap_callback
 
 from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
@@ -83,6 +86,7 @@ from .const import (
     TYPE_SWITCH,
     TYPE_VALVE,
 )
+from .iidmanager import AccessoryIIDStorage
 from .util import (
     accessory_friendly_name,
     async_dismiss_setup_message,
@@ -266,6 +270,7 @@ class HomeAccessory(Accessory):  # type: ignore[misc]
             driver=driver,
             display_name=cleanup_name_for_homekit(name),
             aid=aid,
+            iid_manager=driver.iid_manager,
             *args,
             **kwargs,
         )
@@ -316,8 +321,8 @@ class HomeAccessory(Accessory):  # type: ignore[misc]
             serv_info.configure_char(
                 CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH]
             )
-            self.iid_manager.assign(char)
             char.broker = self
+            self.iid_manager.assign(char)
 
         self.category = category
         self.entity_id = entity_id
@@ -565,7 +570,7 @@ class HomeBridge(Bridge):  # type: ignore[misc]
 
     def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
         """Initialize a Bridge object."""
-        super().__init__(driver, name)
+        super().__init__(driver, name, iid_manager=driver.iid_manager)
         self.set_info_service(
             firmware_revision=format_version(__version__),
             manufacturer=MANUFACTURER,
@@ -598,6 +603,7 @@ class HomeDriver(AccessoryDriver):  # type: ignore[misc]
         entry_id: str,
         bridge_name: str,
         entry_title: str,
+        iid_manager: HomeIIDManager,
         **kwargs: Any,
     ) -> None:
         """Initialize a AccessoryDriver object."""
@@ -606,6 +612,7 @@ class HomeDriver(AccessoryDriver):  # type: ignore[misc]
         self._entry_id = entry_id
         self._bridge_name = bridge_name
         self._entry_title = entry_title
+        self.iid_manager = iid_manager
 
     @pyhap_callback  # type: ignore[misc]
     def pair(
@@ -632,3 +639,30 @@ class HomeDriver(AccessoryDriver):  # type: ignore[misc]
             self.state.pincode,
             self.accessory.xhm_uri(),
         )
+
+
+class HomeIIDManager(IIDManager):  # type: ignore[misc]
+    """IID Manager that remembers IIDs between restarts."""
+
+    def __init__(self, iid_storage: AccessoryIIDStorage) -> None:
+        """Initialize a IIDManager object."""
+        super().__init__()
+        self._iid_storage = iid_storage
+
+    def get_iid_for_obj(self, obj: Characteristic | Service) -> int:
+        """Get IID for object."""
+        aid = obj.broker.aid
+        if isinstance(obj, Characteristic):
+            service = obj.service
+            iid = self._iid_storage.get_or_allocate_iid(
+                aid, service.type_id, service.unique_id, obj.type_id, obj.unique_id
+            )
+        else:
+            iid = self._iid_storage.get_or_allocate_iid(
+                aid, obj.type_id, obj.unique_id, None, None
+            )
+        if iid in self.objs:
+            raise RuntimeError(
+                f"Cannot assign IID {iid} to {obj} as it is already in use by: {self.objs[iid]}"
+            )
+        return iid
diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py
index f717f02ea02..aadac9b4acc 100644
--- a/homeassistant/components/homekit/diagnostics.py
+++ b/homeassistant/components/homekit/diagnostics.py
@@ -27,7 +27,7 @@ async def async_get_config_entry_diagnostics(
             "options": dict(entry.options),
         },
     }
-    if not hasattr(homekit, "driver"):
+    if not homekit.driver:  # not started yet or startup failed
         return data
     driver: AccessoryDriver = homekit.driver
     data.update(driver.get_accessories())
diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py
new file mode 100644
index 00000000000..1b5cc7d6722
--- /dev/null
+++ b/homeassistant/components/homekit/iidmanager.py
@@ -0,0 +1,96 @@
+"""
+Manage allocation of instance ID's.
+
+HomeKit needs to allocate unique numbers to each accessory. These need to
+be stable between reboots and upgrades.
+
+This module generates and stores them in a HA storage.
+"""
+from __future__ import annotations
+
+from uuid import UUID
+
+from pyhap.util import uuid_to_hap_type
+
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.storage import Store
+
+from .util import get_iid_storage_filename_for_entry_id
+
+IID_MANAGER_STORAGE_VERSION = 1
+IID_MANAGER_SAVE_DELAY = 2
+
+ALLOCATIONS_KEY = "allocations"
+
+IID_MIN = 1
+IID_MAX = 18446744073709551615
+
+
+class AccessoryIIDStorage:
+    """
+    Provide stable allocation of IIDs for the lifetime of an accessory.
+
+    Will generate new ID's, ensure they are unique and store them to make sure they
+    persist over reboots.
+    """
+
+    def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
+        """Create a new iid store."""
+        self.hass = hass
+        self.allocations: dict[str, int] = {}
+        self.allocated_iids: list[int] = []
+        self.entry_id = entry_id
+        self.store: Store | None = None
+
+    async def async_initialize(self) -> None:
+        """Load the latest IID data."""
+        iid_store = get_iid_storage_filename_for_entry_id(self.entry_id)
+        self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
+
+        if not (raw_storage := await self.store.async_load()):
+            # There is no data about iid allocations yet
+            return
+
+        assert isinstance(raw_storage, dict)
+        self.allocations = raw_storage.get(ALLOCATIONS_KEY, {})
+        self.allocated_iids = sorted(self.allocations.values())
+
+    def get_or_allocate_iid(
+        self,
+        aid: int,
+        service_uuid: UUID,
+        service_unique_id: str | None,
+        char_uuid: UUID | None,
+        char_unique_id: str | None,
+    ) -> int:
+        """Generate a stable iid."""
+        service_hap_type: str = uuid_to_hap_type(service_uuid)
+        char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None
+        # Allocation key must be a string since we are saving it to JSON
+        allocation_key = (
+            f'{aid}_{service_hap_type}_{service_unique_id or ""}_'
+            f'{char_hap_type or ""}_{char_unique_id or ""}'
+        )
+        if allocation_key in self.allocations:
+            return self.allocations[allocation_key]
+        next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1
+        self.allocations[allocation_key] = next_iid
+        self.allocated_iids.append(next_iid)
+        self._async_schedule_save()
+        return next_iid
+
+    @callback
+    def _async_schedule_save(self) -> None:
+        """Schedule saving the iid allocations."""
+        assert self.store is not None
+        self.store.async_delay_save(self._data_to_save, IID_MANAGER_SAVE_DELAY)
+
+    async def async_save(self) -> None:
+        """Save the iid allocations."""
+        assert self.store is not None
+        return await self.store.async_save(self._data_to_save())
+
+    @callback
+    def _data_to_save(self) -> dict[str, dict[str, int]]:
+        """Return data of entity map to store in a file."""
+        return {ALLOCATIONS_KEY: self.allocations}
diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py
index ecc73f5e731..e3116c99e26 100644
--- a/homeassistant/components/homekit/type_fans.py
+++ b/homeassistant/components/homekit/type_fans.py
@@ -105,7 +105,9 @@ class Fan(HomeAccessory):
             )
         elif self.preset_modes:
             for preset_mode in self.preset_modes:
-                preset_serv = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
+                preset_serv = self.add_preload_service(
+                    SERV_SWITCH, CHAR_NAME, unique_id=preset_mode
+                )
                 serv_fan.add_linked_service(preset_serv)
                 preset_serv.configure_char(
                     CHAR_NAME,
diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py
index b26016b8adc..55519fdf6f7 100644
--- a/homeassistant/components/homekit/type_media_players.py
+++ b/homeassistant/components/homekit/type_media_players.py
@@ -96,7 +96,9 @@ class MediaPlayer(HomeAccessory):
 
         if FEATURE_ON_OFF in feature_list:
             name = self.generate_service_name(FEATURE_ON_OFF)
-            serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
+            serv_on_off = self.add_preload_service(
+                SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_ON_OFF
+            )
             serv_on_off.configure_char(CHAR_NAME, value=name)
             self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char(
                 CHAR_ON, value=False, setter_callback=self.set_on_off
@@ -104,7 +106,9 @@ class MediaPlayer(HomeAccessory):
 
         if FEATURE_PLAY_PAUSE in feature_list:
             name = self.generate_service_name(FEATURE_PLAY_PAUSE)
-            serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
+            serv_play_pause = self.add_preload_service(
+                SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_PLAY_PAUSE
+            )
             serv_play_pause.configure_char(CHAR_NAME, value=name)
             self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char(
                 CHAR_ON, value=False, setter_callback=self.set_play_pause
@@ -112,7 +116,9 @@ class MediaPlayer(HomeAccessory):
 
         if FEATURE_PLAY_STOP in feature_list:
             name = self.generate_service_name(FEATURE_PLAY_STOP)
-            serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
+            serv_play_stop = self.add_preload_service(
+                SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_PLAY_STOP
+            )
             serv_play_stop.configure_char(CHAR_NAME, value=name)
             self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char(
                 CHAR_ON, value=False, setter_callback=self.set_play_stop
@@ -120,7 +126,9 @@ class MediaPlayer(HomeAccessory):
 
         if FEATURE_TOGGLE_MUTE in feature_list:
             name = self.generate_service_name(FEATURE_TOGGLE_MUTE)
-            serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
+            serv_toggle_mute = self.add_preload_service(
+                SERV_SWITCH, CHAR_NAME, unique_id=FEATURE_TOGGLE_MUTE
+            )
             serv_toggle_mute.configure_char(CHAR_NAME, value=name)
             self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char(
                 CHAR_ON, value=False, setter_callback=self.set_toggle_mute
diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py
index aa064cfc012..fb808eff8b0 100644
--- a/homeassistant/components/homekit/type_remotes.py
+++ b/homeassistant/components/homekit/type_remotes.py
@@ -131,7 +131,7 @@ class RemoteInputSelectAccessory(HomeAccessory):
         )
         for index, source in enumerate(self.sources):
             serv_input = self.add_preload_service(
-                SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME]
+                SERV_INPUT_SOURCE, [CHAR_IDENTIFIER, CHAR_NAME], unique_id=source
             )
             serv_tv.add_linked_service(serv_input)
             serv_input.configure_char(CHAR_CONFIGURED_NAME, value=source)
diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py
index 1598df015c5..f79185c64b1 100644
--- a/homeassistant/components/homekit/type_switches.py
+++ b/homeassistant/components/homekit/type_switches.py
@@ -259,7 +259,7 @@ class SelectSwitch(HomeAccessory):
         options = state.attributes[ATTR_OPTIONS]
         for option in options:
             serv_option = self.add_preload_service(
-                SERV_OUTLET, [CHAR_NAME, CHAR_IN_USE]
+                SERV_OUTLET, [CHAR_NAME, CHAR_IN_USE], unique_id=option
             )
             serv_option.configure_char(
                 CHAR_NAME, value=cleanup_name_for_homekit(option)
diff --git a/homeassistant/components/homekit/type_triggers.py b/homeassistant/components/homekit/type_triggers.py
index 776fe6f3110..b9b2ad6ce8f 100644
--- a/homeassistant/components/homekit/type_triggers.py
+++ b/homeassistant/components/homekit/type_triggers.py
@@ -42,12 +42,14 @@ class DeviceTriggerAccessory(HomeAccessory):
         for idx, trigger in enumerate(device_triggers):
             type_ = trigger["type"]
             subtype = trigger.get("subtype")
+            unique_id = f'{type_}-{subtype or ""}'
             trigger_name = (
                 f"{type_.title()} {subtype.title()}" if subtype else type_.title()
             )
             serv_stateless_switch = self.add_preload_service(
                 SERV_STATELESS_PROGRAMMABLE_SWITCH,
                 [CHAR_NAME, CHAR_SERVICE_LABEL_INDEX],
+                unique_id=unique_id,
             )
             self.triggers.append(
                 serv_stateless_switch.configure_char(
@@ -60,7 +62,9 @@ class DeviceTriggerAccessory(HomeAccessory):
             serv_stateless_switch.configure_char(
                 CHAR_SERVICE_LABEL_INDEX, value=idx + 1
             )
-            serv_service_label = self.add_preload_service(SERV_SERVICE_LABEL)
+            serv_service_label = self.add_preload_service(
+                SERV_SERVICE_LABEL, unique_id=unique_id
+            )
             serv_service_label.configure_char(CHAR_SERVICE_LABEL_NAMESPACE, value=1)
             serv_stateless_switch.add_linked_service(serv_service_label)
 
diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py
index 445b73cccbe..ee02ea1a576 100644
--- a/homeassistant/components/homekit/util.py
+++ b/homeassistant/components/homekit/util.py
@@ -431,10 +431,15 @@ def get_persist_filename_for_entry_id(entry_id: str) -> str:
 
 
 def get_aid_storage_filename_for_entry_id(entry_id: str) -> str:
-    """Determine the ilename of homekit aid storage file."""
+    """Determine the filename of homekit aid storage file."""
     return f"{DOMAIN}.{entry_id}.aids"
 
 
+def get_iid_storage_filename_for_entry_id(entry_id: str) -> str:
+    """Determine the filename of homekit iid storage file."""
+    return f"{DOMAIN}.{entry_id}.iids"
+
+
 def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str) -> str:
     """Determine the path to the homekit state file."""
     return hass.config.path(STORAGE_DIR, get_persist_filename_for_entry_id(entry_id))
@@ -447,6 +452,13 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str) ->
     )
 
 
+def get_iid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str) -> str:
+    """Determine the path to the homekit iid storage file."""
+    return hass.config.path(
+        STORAGE_DIR, get_iid_storage_filename_for_entry_id(entry_id)
+    )
+
+
 def _format_version_part(version_part: str) -> str:
     return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part))))
 
@@ -466,14 +478,15 @@ def _is_zero_but_true(value: Any) -> bool:
     return convert_to_float(value) == 0
 
 
-def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str) -> bool:
+def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str) -> None:
     """Remove the state files from disk."""
-    persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id)
-    aid_storage_path = get_aid_storage_fullpath_for_entry_id(hass, entry_id)
-    os.unlink(persist_file_path)
-    if os.path.exists(aid_storage_path):
-        os.unlink(aid_storage_path)
-    return True
+    for path in (
+        get_persist_fullpath_for_entry_id(hass, entry_id),
+        get_aid_storage_fullpath_for_entry_id(hass, entry_id),
+        get_iid_storage_fullpath_for_entry_id(hass, entry_id),
+    ):
+        if os.path.exists(path):
+            os.unlink(path)
 
 
 def _get_test_socket() -> socket.socket:
diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py
index 5e2acbcd9db..7b79e0f9b6b 100644
--- a/tests/components/homekit/conftest.py
+++ b/tests/components/homekit/conftest.py
@@ -3,17 +3,50 @@ from contextlib import suppress
 import os
 from unittest.mock import patch
 
-from pyhap.accessory_driver import AccessoryDriver
 import pytest
 
 from homeassistant.components.device_tracker.legacy import YAML_DEVICES
-from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED
+from homeassistant.components.homekit.accessories import HomeDriver, HomeIIDManager
+from homeassistant.components.homekit.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED
+from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage
 
 from tests.common import async_capture_events, mock_device_registry, mock_registry
 
 
 @pytest.fixture
-def hk_driver(loop):
+def iid_storage(hass):
+    """Mock the iid storage."""
+    with patch.object(AccessoryIIDStorage, "_async_schedule_save"):
+        yield AccessoryIIDStorage(hass, "")
+
+
+@pytest.fixture()
+def run_driver(hass, loop, iid_storage):
+    """Return a custom AccessoryDriver instance for HomeKit accessory init.
+
+    This mock does not mock async_stop, so the driver will not be stopped
+    """
+    with patch("pyhap.accessory_driver.AsyncZeroconf"), patch(
+        "pyhap.accessory_driver.AccessoryEncoder"
+    ), patch("pyhap.accessory_driver.HAPServer"), patch(
+        "pyhap.accessory_driver.AccessoryDriver.publish"
+    ), patch(
+        "pyhap.accessory_driver.AccessoryDriver.persist"
+    ):
+        yield HomeDriver(
+            hass,
+            pincode=b"123-45-678",
+            entry_id="",
+            entry_title="mock entry",
+            bridge_name=BRIDGE_NAME,
+            iid_manager=HomeIIDManager(iid_storage),
+            address="127.0.0.1",
+            loop=loop,
+        )
+
+
+@pytest.fixture
+def hk_driver(hass, loop, iid_storage):
     """Return a custom AccessoryDriver instance for HomeKit accessory init."""
     with patch("pyhap.accessory_driver.AsyncZeroconf"), patch(
         "pyhap.accessory_driver.AccessoryEncoder"
@@ -24,11 +57,20 @@ def hk_driver(loop):
     ), patch(
         "pyhap.accessory_driver.AccessoryDriver.persist"
     ):
-        yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop)
+        yield HomeDriver(
+            hass,
+            pincode=b"123-45-678",
+            entry_id="",
+            entry_title="mock entry",
+            bridge_name=BRIDGE_NAME,
+            iid_manager=HomeIIDManager(iid_storage),
+            address="127.0.0.1",
+            loop=loop,
+        )
 
 
 @pytest.fixture
-def mock_hap(loop, mock_zeroconf):
+def mock_hap(hass, loop, iid_storage, mock_zeroconf):
     """Return a custom AccessoryDriver instance for HomeKit accessory init."""
     with patch("pyhap.accessory_driver.AsyncZeroconf"), patch(
         "pyhap.accessory_driver.AccessoryEncoder"
@@ -43,7 +85,16 @@ def mock_hap(loop, mock_zeroconf):
     ), patch(
         "pyhap.accessory_driver.AccessoryDriver.persist"
     ):
-        yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop)
+        yield HomeDriver(
+            hass,
+            pincode=b"123-45-678",
+            entry_id="",
+            entry_title="mock entry",
+            bridge_name=BRIDGE_NAME,
+            iid_manager=HomeIIDManager(iid_storage),
+            address="127.0.0.1",
+            loop=loop,
+        )
 
 
 @pytest.fixture
diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py
index 6d7de6eb696..2a0f3f2f718 100644
--- a/tests/components/homekit/test_accessories.py
+++ b/tests/components/homekit/test_accessories.py
@@ -10,6 +10,7 @@ from homeassistant.components.homekit.accessories import (
     HomeAccessory,
     HomeBridge,
     HomeDriver,
+    HomeIIDManager,
 )
 from homeassistant.components.homekit.const import (
     ATTR_DISPLAY_NAME,
@@ -107,7 +108,7 @@ async def test_home_accessory(hass, hk_driver):
         hk_driver,
         "Home Accessory that exceeds the maximum maximum maximum maximum maximum maximum length",
         entity_id2,
-        3,
+        4,
         {
             ATTR_MODEL: "Awesome Model that exceeds the maximum maximum maximum maximum maximum maximum length",
             ATTR_MANUFACTURER: "Lux Brands that exceeds the maximum maximum maximum maximum maximum maximum length",
@@ -140,7 +141,7 @@ async def test_home_accessory(hass, hk_driver):
         hk_driver,
         "Home Accessory that exceeds the maximum maximum maximum maximum maximum maximum length",
         entity_id2,
-        3,
+        5,
         {
             ATTR_MODEL: "Awesome Model that exceeds the maximum maximum maximum maximum maximum maximum length",
             ATTR_MANUFACTURER: "Lux Brands that exceeds the maximum maximum maximum maximum maximum maximum length",
@@ -191,7 +192,7 @@ async def test_home_accessory(hass, hk_driver):
     entity_id = "test_model.demo"
     hass.states.async_set(entity_id, None)
     await hass.async_block_till_done()
-    acc = HomeAccessory(hass, hk_driver, "test_name", entity_id, 2, None)
+    acc = HomeAccessory(hass, hk_driver, "test_name", entity_id, 6, None)
     serv = acc.services[0]  # SERV_ACCESSORY_INFO
     assert serv.get_characteristic(CHAR_MODEL).value == "Test Model"
 
@@ -317,7 +318,7 @@ async def test_battery_service(hass, hk_driver, caplog):
     with patch(
         "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state"
     ):
-        acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 2, None)
+        acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 3, None)
         assert acc._char_battery.value == 0
         assert acc._char_low_battery.value == 0
         assert acc._char_charging.value == 2
@@ -405,7 +406,7 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog):
         hk_driver,
         "Battery Service",
         entity_id,
-        2,
+        3,
         {CONF_LINKED_BATTERY_SENSOR: linked_battery, CONF_LOW_BATTERY_THRESHOLD: 50},
     )
     with patch(
@@ -700,16 +701,17 @@ def test_home_bridge(hk_driver):
     assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL
     assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER
 
+
+def test_home_bridge_setup_message(hk_driver):
+    """Test HomeBridge setup message."""
     bridge = HomeBridge("hass", hk_driver, "test_name")
     assert bridge.display_name == "test_name"
     assert len(bridge.services) == 2
-    serv = bridge.services[0]  # SERV_ACCESSORY_INFO
-
     # setup_message
     bridge.setup_message()
 
 
-def test_home_driver():
+def test_home_driver(iid_storage):
     """Test HomeDriver class."""
     ip_address = "127.0.0.1"
     port = 51826
@@ -722,6 +724,7 @@ def test_home_driver():
             "entry_id",
             "name",
             "title",
+            iid_manager=HomeIIDManager(iid_storage),
             address=ip_address,
             port=port,
             persist_file=path,
@@ -749,3 +752,22 @@ def test_home_driver():
 
     mock_unpair.assert_called_with("client_uuid")
     mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0")
+
+
+async def test_iid_collision_raises(hass, hk_driver):
+    """Test iid collision raises.
+
+    If we try to allocate the same IID to the an accessory twice, we should
+    raise an exception.
+    """
+
+    entity_id = "light.accessory"
+    entity_id2 = "light.accessory2"
+
+    hass.states.async_set(entity_id, STATE_OFF)
+    hass.states.async_set(entity_id2, STATE_OFF)
+
+    HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {})
+
+    with pytest.raises(RuntimeError):
+        HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 2, {})
diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py
index 1b2a0b4e211..144a97853c5 100644
--- a/tests/components/homekit/test_config_flow.py
+++ b/tests/components/homekit/test_config_flow.py
@@ -387,8 +387,9 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip):
     }
 
 
+@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
 async def test_options_flow_devices(
-    mock_hap,
+    port_mock,
     hass,
     demo_cleanup,
     device_reg,
@@ -473,9 +474,13 @@ async def test_options_flow_devices(
         },
     }
 
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
+
 
+@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
 async def test_options_flow_devices_preserved_when_advanced_off(
-    mock_hap, hass, mock_get_source_ip, mock_async_zeroconf
+    port_mock, hass, mock_get_source_ip, mock_async_zeroconf
 ):
     """Test devices are preserved if they were added in advanced mode but it was turned off."""
     config_entry = MockConfigEntry(
@@ -542,6 +547,8 @@ async def test_options_flow_devices_preserved_when_advanced_off(
             "include_entities": [],
         },
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_options_flow_include_mode_with_non_existant_entity(
@@ -600,6 +607,8 @@ async def test_options_flow_include_mode_with_non_existant_entity(
             "include_entities": ["climate.new", "climate.front_gate"],
         },
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_options_flow_exclude_mode_with_non_existant_entity(
@@ -659,6 +668,8 @@ async def test_options_flow_exclude_mode_with_non_existant_entity(
             "include_entities": [],
         },
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_options_flow_include_mode_basic(hass, mock_get_source_ip):
@@ -704,6 +715,7 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip):
             "include_entities": ["climate.new"],
         },
     }
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip):
@@ -809,6 +821,8 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip):
         },
         "entity_config": {"camera.native_h264": {"video_codec": "copy"}},
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip):
@@ -941,6 +955,8 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip):
         },
         "mode": "bridge",
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_options_flow_with_camera_audio(hass, mock_get_source_ip):
@@ -1073,6 +1089,8 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip):
         },
         "mode": "bridge",
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip):
@@ -1112,6 +1130,7 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip):
             user_input={},
         )
         assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
@@ -1211,6 +1230,8 @@ async def test_options_flow_include_mode_basic_accessory(
             "include_entities": ["media_player.tv"],
         },
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_source_ip):
@@ -1317,6 +1338,8 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou
         },
     }
     assert len(mock_setup_entry.mock_calls) == 1
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 def _get_schema_default(schema, key_name):
@@ -1423,6 +1446,8 @@ async def test_options_flow_exclude_mode_skips_category_entities(
             "include_entities": [],
         },
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
@@ -1501,6 +1526,8 @@ async def test_options_flow_exclude_mode_skips_hidden_entities(
             "include_entities": [],
         },
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
 
 
 @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
@@ -1583,3 +1610,5 @@ async def test_options_flow_include_mode_allows_hidden_entities(
             ],
         },
     }
+    await hass.async_block_till_done()
+    await hass.config_entries.async_unload(config_entry.entry_id)
diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py
index dbb63ba690a..21dc94a4b54 100644
--- a/tests/components/homekit/test_homekit.py
+++ b/tests/components/homekit/test_homekit.py
@@ -2,7 +2,6 @@
 from __future__ import annotations
 
 import asyncio
-import os
 from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
 
 from pyhap.accessory import Accessory
@@ -60,7 +59,6 @@ from homeassistant.helpers.entityfilter import (
     convert_filter,
 )
 from homeassistant.setup import async_setup_component
-from homeassistant.util import json as json_util
 
 from .util import PATH_HOMEKIT, async_init_entry, async_init_integration
 
@@ -122,6 +120,7 @@ def _mock_homekit(hass, entry, homekit_mode, entity_filter=None, devices=None):
 def _mock_homekit_bridge(hass, entry):
     homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
     homekit.driver = MagicMock()
+    homekit.iid_storage = MagicMock()
     return homekit
 
 
@@ -177,6 +176,49 @@ async def test_setup_min(hass, mock_async_zeroconf):
     assert mock_homekit().async_start.called is True
 
 
+@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True)
+async def test_removing_entry(port_mock, hass, mock_async_zeroconf):
+    """Test removing a config entry."""
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT},
+        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 hass.config_entries.async_setup(entry.entry_id)
+        await hass.async_block_till_done()
+
+    mock_homekit.assert_any_call(
+        hass,
+        BRIDGE_NAME,
+        DEFAULT_PORT,
+        "1.2.3.4",
+        ANY,
+        ANY,
+        {},
+        HOMEKIT_MODE_BRIDGE,
+        None,
+        entry.entry_id,
+        entry.title,
+        devices=[],
+    )
+
+    # Test auto start enabled
+    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+    await hass.async_block_till_done()
+    assert mock_homekit().async_start.called is True
+
+    await hass.config_entries.async_remove(entry.entry_id)
+    await hass.async_block_till_done()
+
+
 async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf):
     """Test setup of bridge and driver."""
     entry = MockConfigEntry(
@@ -203,6 +245,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf):
     zeroconf_mock = MagicMock()
     uuid = await instance_id.async_get(hass)
     with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver:
+        homekit.iid_storage = MagicMock()
         await hass.async_add_executor_job(homekit.setup, zeroconf_mock, uuid)
 
     path = get_persist_fullpath_for_entry_id(hass, entry.entry_id)
@@ -219,6 +262,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf):
         async_zeroconf_instance=zeroconf_mock,
         zeroconf_server=f"{uuid}-hap.local.",
         loader=ANY,
+        iid_manager=ANY,
     )
     assert homekit.driver.safe_mode is False
 
@@ -247,6 +291,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf):
     path = get_persist_fullpath_for_entry_id(hass, entry.entry_id)
     uuid = await instance_id.async_get(hass)
     with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver:
+        homekit.iid_storage = MagicMock()
         await hass.async_add_executor_job(homekit.setup, mock_async_zeroconf, uuid)
     mock_driver.assert_called_with(
         hass,
@@ -261,6 +306,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf):
         async_zeroconf_instance=mock_async_zeroconf,
         zeroconf_server=f"{uuid}-hap.local.",
         loader=ANY,
+        iid_manager=ANY,
     )
 
 
@@ -289,6 +335,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf):
     path = get_persist_fullpath_for_entry_id(hass, entry.entry_id)
     uuid = await instance_id.async_get(hass)
     with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver:
+        homekit.iid_storage = MagicMock()
         await hass.async_add_executor_job(homekit.setup, async_zeroconf_instance, uuid)
     mock_driver.assert_called_with(
         hass,
@@ -303,10 +350,11 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf):
         async_zeroconf_instance=async_zeroconf_instance,
         zeroconf_server=f"{uuid}-hap.local.",
         loader=ANY,
+        iid_manager=ANY,
     )
 
 
-async def test_homekit_add_accessory(hass, mock_async_zeroconf):
+async def test_homekit_add_accessory(hass, mock_async_zeroconf, mock_hap):
     """Add accessory if config exists and get_acc returns an accessory."""
 
     entry = MockConfigEntry(
@@ -340,10 +388,12 @@ async def test_homekit_add_accessory(hass, mock_async_zeroconf):
         mock_get_acc.assert_called_with(hass, ANY, ANY, 1467253281, {})
         assert homekit.bridge.add_accessory.called
 
+        await homekit.async_stop()
+
 
 @pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA])
 async def test_homekit_warn_add_accessory_bridge(
-    hass, acc_category, mock_async_zeroconf, caplog
+    hass, acc_category, mock_async_zeroconf, mock_hap, caplog
 ):
     """Test we warn when adding cameras or tvs to a bridge."""
 
@@ -367,6 +417,7 @@ async def test_homekit_warn_add_accessory_bridge(
         homekit.add_bridge_accessory(state)
         mock_get_acc.assert_called_with(hass, ANY, ANY, 1508819236, {})
         assert not homekit.bridge.add_accessory.called
+        await homekit.async_stop()
 
     assert "accessory mode" in caplog.text
 
@@ -385,7 +436,6 @@ async def test_homekit_remove_accessory(hass, mock_async_zeroconf):
 
     acc = await homekit.async_remove_bridge_accessory(6)
     assert acc is acc_mock
-    assert acc_mock.stop.called
     assert len(homekit.bridge.accessories) == 0
 
 
@@ -722,7 +772,7 @@ async def test_homekit_stop(hass):
     assert homekit.driver.async_stop.called is True
 
 
-async def test_homekit_reset_accessories(hass, mock_async_zeroconf):
+async def test_homekit_reset_accessories(hass, mock_async_zeroconf, mock_hap):
     """Test resetting HomeKit accessories."""
 
     entry = MockConfigEntry(
@@ -736,20 +786,15 @@ async def test_homekit_reset_accessories(hass, mock_async_zeroconf):
         "pyhap.accessory.Bridge.add_accessory"
     ) as mock_add_accessory, patch(
         "pyhap.accessory_driver.AccessoryDriver.config_changed"
-    ) as hk_driver_config_changed, patch(
+    ), patch(
         "pyhap.accessory_driver.AccessoryDriver.async_start"
     ), patch(
         f"{PATH_HOMEKIT}.accessories.HomeAccessory.run"
-    ) as mock_run, patch.object(
+    ), patch.object(
         homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0
     ):
         await async_init_entry(hass, entry)
 
-        acc_mock = MagicMock()
-        acc_mock.entity_id = entity_id
-        acc_mock.stop = AsyncMock()
-        aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
-        homekit.bridge.accessories = {aid: acc_mock}
         homekit.status = STATUS_RUNNING
         homekit.driver.aio_stop_event = MagicMock()
 
@@ -761,10 +806,9 @@ async def test_homekit_reset_accessories(hass, mock_async_zeroconf):
         )
         await hass.async_block_till_done()
 
-        assert hk_driver_config_changed.call_count == 2
         assert mock_add_accessory.called
-        assert mock_run.called
         homekit.status = STATUS_READY
+        await homekit.async_stop()
 
 
 async def test_homekit_unpair(hass, device_reg, mock_async_zeroconf):
@@ -1028,7 +1072,7 @@ async def test_homekit_reset_accessories_not_bridged(hass, mock_async_zeroconf):
         homekit.status = STATUS_STOPPED
 
 
-async def test_homekit_reset_single_accessory(hass, mock_async_zeroconf):
+async def test_homekit_reset_single_accessory(hass, mock_hap, mock_async_zeroconf):
     """Test resetting HomeKit single accessory."""
 
     entry = MockConfigEntry(
@@ -1046,13 +1090,7 @@ async def test_homekit_reset_single_accessory(hass, mock_async_zeroconf):
         f"{PATH_HOMEKIT}.accessories.HomeAccessory.run"
     ) as mock_run:
         await async_init_entry(hass, entry)
-
         homekit.status = STATUS_RUNNING
-        acc_mock = MagicMock()
-        acc_mock.entity_id = entity_id
-        acc_mock.stop = AsyncMock()
-
-        homekit.driver.accessory = acc_mock
         homekit.driver.aio_stop_event = MagicMock()
 
         await hass.services.async_call(
@@ -1065,6 +1103,7 @@ async def test_homekit_reset_single_accessory(hass, mock_async_zeroconf):
         assert mock_run.called
         assert hk_driver_config_changed.call_count == 1
         homekit.status = STATUS_READY
+        await homekit.async_stop()
 
 
 async def test_homekit_reset_single_accessory_unsupported(hass, mock_async_zeroconf):
@@ -1494,12 +1533,6 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_async_zeroconf
         await hass.async_block_till_done()
 
 
-def _write_data(path: str, data: dict) -> None:
-    """Write the data."""
-    os.makedirs(os.path.dirname(path), exist_ok=True)
-    json_util.save_json(path, data)
-
-
 async def test_homekit_ignored_missing_devices(
     hass, hk_driver, device_reg, entity_reg, mock_async_zeroconf
 ):
diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py
new file mode 100644
index 00000000000..a791c30a341
--- /dev/null
+++ b/tests/components/homekit/test_iidmanager.py
@@ -0,0 +1,101 @@
+"""Tests for the HomeKit IID manager."""
+
+
+from uuid import UUID
+
+from homeassistant.components.homekit.const import DOMAIN
+from homeassistant.components.homekit.iidmanager import (
+    AccessoryIIDStorage,
+    get_iid_storage_filename_for_entry_id,
+)
+from homeassistant.util.uuid import random_uuid_hex
+
+from tests.common import MockConfigEntry
+
+
+async def test_iid_generation_and_restore(hass, iid_storage, hass_storage):
+    """Test generating iids and restoring them from storage."""
+    entry = MockConfigEntry(domain=DOMAIN)
+
+    iid_storage = AccessoryIIDStorage(hass, entry.entry_id)
+    await iid_storage.async_initialize()
+
+    random_service_uuid = UUID(random_uuid_hex())
+    random_characteristic_uuid = UUID(random_uuid_hex())
+
+    iid1 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, None, random_characteristic_uuid, None
+    )
+    iid2 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, None, random_characteristic_uuid, None
+    )
+    assert iid1 == iid2
+
+    service_only_iid1 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, None, None, None
+    )
+    service_only_iid2 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, None, None, None
+    )
+    assert service_only_iid1 == service_only_iid2
+    assert service_only_iid1 != iid1
+
+    service_only_iid_with_unique_id1 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, "any", None, None
+    )
+    service_only_iid_with_unique_id2 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, "any", None, None
+    )
+    assert service_only_iid_with_unique_id1 == service_only_iid_with_unique_id2
+    assert service_only_iid_with_unique_id1 != service_only_iid1
+
+    unique_char_iid1 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, None, random_characteristic_uuid, "any"
+    )
+    unique_char_iid2 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, None, random_characteristic_uuid, "any"
+    )
+    assert unique_char_iid1 == unique_char_iid2
+    assert unique_char_iid1 != iid1
+
+    unique_service_unique_char_iid1 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, "any", random_characteristic_uuid, "any"
+    )
+    unique_service_unique_char_iid2 = iid_storage.get_or_allocate_iid(
+        1, random_service_uuid, "any", random_characteristic_uuid, "any"
+    )
+    assert unique_service_unique_char_iid1 == unique_service_unique_char_iid2
+    assert unique_service_unique_char_iid1 != iid1
+
+    unique_service_unique_char_new_aid_iid1 = iid_storage.get_or_allocate_iid(
+        2, random_service_uuid, "any", random_characteristic_uuid, "any"
+    )
+    unique_service_unique_char_new_aid_iid2 = iid_storage.get_or_allocate_iid(
+        2, random_service_uuid, "any", random_characteristic_uuid, "any"
+    )
+    assert (
+        unique_service_unique_char_new_aid_iid1
+        == unique_service_unique_char_new_aid_iid2
+    )
+    assert unique_service_unique_char_new_aid_iid1 != iid1
+    assert unique_service_unique_char_new_aid_iid1 != unique_service_unique_char_iid1
+
+    await iid_storage.async_save()
+
+    iid_storage2 = AccessoryIIDStorage(hass, entry.entry_id)
+    await iid_storage2.async_initialize()
+    iid3 = iid_storage2.get_or_allocate_iid(
+        1, random_service_uuid, None, random_characteristic_uuid, None
+    )
+    assert iid3 == iid1
+
+
+async def test_iid_storage_filename(hass, iid_storage, hass_storage):
+    """Test iid storage uses the expected filename."""
+    entry = MockConfigEntry(domain=DOMAIN)
+
+    iid_storage = AccessoryIIDStorage(hass, entry.entry_id)
+    await iid_storage.async_initialize()
+    assert iid_storage.store.path.endswith(
+        get_iid_storage_filename_for_entry_id(entry.entry_id)
+    )
diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py
index f6855ca3cbb..80a4f3c4e88 100644
--- a/tests/components/homekit/test_type_cameras.py
+++ b/tests/components/homekit/test_type_cameras.py
@@ -4,7 +4,6 @@ import asyncio
 from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
 from uuid import UUID
 
-from pyhap.accessory_driver import AccessoryDriver
 import pytest
 
 from homeassistant.components import camera, ffmpeg
@@ -78,21 +77,6 @@ async def _async_stop_stream(hass, acc, session_info):
     await hass.async_block_till_done()
 
 
-@pytest.fixture()
-def run_driver(hass):
-    """Return a custom AccessoryDriver instance for HomeKit accessory init."""
-    with patch("pyhap.accessory_driver.AsyncZeroconf"), patch(
-        "pyhap.accessory_driver.AccessoryEncoder"
-    ), patch("pyhap.accessory_driver.HAPServer"), patch(
-        "pyhap.accessory_driver.AccessoryDriver.publish"
-    ), patch(
-        "pyhap.accessory_driver.AccessoryDriver.persist"
-    ):
-        yield AccessoryDriver(
-            pincode=b"123-45-678", address="127.0.0.1", loop=hass.loop
-        )
-
-
 def _mock_reader():
     """Mock ffmpeg reader."""
 
diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py
index bc512a4b162..5d261886248 100644
--- a/tests/components/homekit/test_type_covers.py
+++ b/tests/components/homekit/test_type_covers.py
@@ -573,7 +573,7 @@ async def test_windowcovering_basic_restore(hass, hk_driver, events):
     assert acc.char_target_position is not None
     assert acc.char_position_state is not None
 
-    acc = WindowCoveringBasic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
+    acc = WindowCoveringBasic(hass, hk_driver, "Cover", "cover.all_info_set", 3, None)
     assert acc.category == 14
     assert acc.char_current_position is not None
     assert acc.char_target_position is not None
@@ -611,7 +611,7 @@ async def test_windowcovering_restore(hass, hk_driver, events):
     assert acc.char_target_position is not None
     assert acc.char_position_state is not None
 
-    acc = WindowCovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
+    acc = WindowCovering(hass, hk_driver, "Cover", "cover.all_info_set", 3, None)
     assert acc.category == 14
     assert acc.char_current_position is not None
     assert acc.char_target_position is not None
diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py
index fbcf6a00421..9b5f8286d8b 100644
--- a/tests/components/homekit/test_type_fans.py
+++ b/tests/components/homekit/test_type_fans.py
@@ -561,7 +561,7 @@ async def test_fan_restore(hass, hk_driver, events):
     assert acc.char_speed is None
     assert acc.char_swing is None
 
-    acc = Fan(hass, hk_driver, "Fan", "fan.all_info_set", 2, None)
+    acc = Fan(hass, hk_driver, "Fan", "fan.all_info_set", 3, None)
     assert acc.category == 3
     assert acc.char_active is not None
     assert acc.char_direction is not None
diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py
index dbdc2b0ba55..30b9bc77f5d 100644
--- a/tests/components/homekit/test_type_media_players.py
+++ b/tests/components/homekit/test_type_media_players.py
@@ -442,7 +442,7 @@ async def test_tv_restore(hass, hk_driver, events):
     assert not hasattr(acc, "char_input_source")
 
     acc = TelevisionMediaPlayer(
-        hass, hk_driver, "MediaPlayer", "media_player.all_info_set", 2, None
+        hass, hk_driver, "MediaPlayer", "media_player.all_info_set", 3, None
     )
     assert acc.category == 31
     assert acc.chars_tv == [CHAR_REMOTE_KEY]
diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py
index 64f1d82d123..920bf6d8a31 100644
--- a/tests/components/homekit/test_type_security_systems.py
+++ b/tests/components/homekit/test_type_security_systems.py
@@ -282,13 +282,16 @@ async def test_supported_states(hass, hk_driver, events):
         },
     ]
 
+    aid = 1
+
     for test_config in test_configs:
         attrs = {"supported_features": test_config.get("features")}
 
         hass.states.async_set(entity_id, None, attributes=attrs)
         await hass.async_block_till_done()
 
-        acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config)
+        aid += 1
+        acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, aid, config)
         await acc.run()
         await hass.async_block_till_done()
 
diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py
index b916d447d12..4997a35910d 100644
--- a/tests/components/homekit/test_type_sensors.py
+++ b/tests/components/homekit/test_type_sensors.py
@@ -423,12 +423,14 @@ async def test_motion_uses_bool(hass, hk_driver):
 async def test_binary_device_classes(hass, hk_driver):
     """Test if services and characteristics are assigned correctly."""
     entity_id = "binary_sensor.demo"
+    aid = 1
 
     for device_class, (service, char, _) in BINARY_SENSOR_SERVICE_MAP.items():
         hass.states.async_set(entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: device_class})
         await hass.async_block_till_done()
 
-        acc = BinarySensor(hass, hk_driver, "Binary Sensor", entity_id, 2, None)
+        aid += 1
+        acc = BinarySensor(hass, hk_driver, "Binary Sensor", entity_id, aid, None)
         assert acc.get_service(service).display_name == service
         assert acc.char_detected.display_name == char
 
@@ -460,7 +462,7 @@ async def test_sensor_restore(hass, hk_driver, events):
     acc = get_accessory(hass, hk_driver, hass.states.get("sensor.temperature"), 2, {})
     assert acc.category == 10
 
-    acc = get_accessory(hass, hk_driver, hass.states.get("sensor.humidity"), 2, {})
+    acc = get_accessory(hass, hk_driver, hass.states.get("sensor.humidity"), 3, {})
     assert acc.category == 10
 
 
diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py
index cc80201ae33..0d6f8f0d586 100644
--- a/tests/components/homekit/test_type_switches.py
+++ b/tests/components/homekit/test_type_switches.py
@@ -150,23 +150,23 @@ async def test_valve_set_state(hass, hk_driver, events):
     assert acc.category == 29  # Faucet
     assert acc.char_valve_type.value == 3  # Water faucet
 
-    acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_SHOWER})
+    acc = Valve(hass, hk_driver, "Valve", entity_id, 3, {CONF_TYPE: TYPE_SHOWER})
     await acc.run()
     await hass.async_block_till_done()
     assert acc.category == 30  # Shower
     assert acc.char_valve_type.value == 2  # Shower head
 
-    acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_SPRINKLER})
+    acc = Valve(hass, hk_driver, "Valve", entity_id, 4, {CONF_TYPE: TYPE_SPRINKLER})
     await acc.run()
     await hass.async_block_till_done()
     assert acc.category == 28  # Sprinkler
     assert acc.char_valve_type.value == 1  # Irrigation
 
-    acc = Valve(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_VALVE})
+    acc = Valve(hass, hk_driver, "Valve", entity_id, 5, {CONF_TYPE: TYPE_VALVE})
     await acc.run()
     await hass.async_block_till_done()
 
-    assert acc.aid == 2
+    assert acc.aid == 5
     assert acc.category == 29  # Faucet
 
     assert acc.char_active.value == 0
diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py
index a964568cc60..33b45e54081 100644
--- a/tests/components/homekit/test_type_thermostats.py
+++ b/tests/components/homekit/test_type_thermostats.py
@@ -1045,7 +1045,7 @@ async def test_thermostat_restore(hass, hk_driver, events):
         "off",
     }
 
-    acc = Thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None)
+    acc = Thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 3, None)
     assert acc.category == 9
     assert acc.get_temperature_range() == (60.0, 70.0)
     assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == {
@@ -1859,7 +1859,7 @@ async def test_water_heater_restore(hass, hk_driver, events):
     }
 
     acc = WaterHeater(
-        hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None
+        hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 3, None
     )
     assert acc.category == 9
     assert acc.get_temperature_range() == (60.0, 70.0)
-- 
GitLab


From 7bd9ce72f7c78413924cba9b8406b6492bb83494 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 14 Oct 2022 23:54:14 +0200
Subject: [PATCH 494/985] Add reauth support to LaMetric (#80355)

* Add reauth support to LaMetric

* Adjust docblock
---
 .../components/lametric/config_flow.py        |  41 +++-
 .../components/lametric/coordinator.py        |   5 +-
 .../components/lametric/strings.json          |   2 +
 .../components/lametric/translations/en.json  |   2 +
 tests/components/lametric/test_config_flow.py | 177 +++++++++++++++++-
 tests/components/lametric/test_init.py        |  35 +++-
 6 files changed, 253 insertions(+), 9 deletions(-)

diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py
index a317d835413..7496fc51a4e 100644
--- a/homeassistant/components/lametric/config_flow.py
+++ b/homeassistant/components/lametric/config_flow.py
@@ -1,6 +1,7 @@
 """Config flow to configure the LaMetric integration."""
 from __future__ import annotations
 
+from collections.abc import Mapping
 from ipaddress import ip_address
 import logging
 from typing import Any
@@ -27,6 +28,7 @@ from homeassistant.components.ssdp import (
     ATTR_UPNP_SERIAL,
     SsdpServiceInfo,
 )
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC
 from homeassistant.data_entry_flow import AbortFlow, FlowResult
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -56,6 +58,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
     discovered_host: str
     discovered_serial: str
     discovered: bool = False
+    reauth_entry: ConfigEntry | None = None
 
     @property
     def logger(self) -> logging.Logger:
@@ -103,6 +106,13 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
         self.discovered_serial = serial
         return await self.async_step_choice_enter_manual_or_fetch_cloud()
 
+    async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
+        """Handle initiation of re-authentication with LaMetric."""
+        self.reauth_entry = self.hass.config_entries.async_get_entry(
+            self.context["entry_id"]
+        )
+        return await self.async_step_choice_enter_manual_or_fetch_cloud()
+
     async def async_step_choice_enter_manual_or_fetch_cloud(
         self, user_input: dict[str, Any] | None = None
     ) -> FlowResult:
@@ -120,6 +130,8 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
         if user_input is not None:
             if self.discovered:
                 host = self.discovered_host
+            elif self.reauth_entry:
+                host = self.reauth_entry.data[CONF_HOST]
             else:
                 host = user_input[CONF_HOST]
 
@@ -142,7 +154,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
                 TextSelectorConfig(type=TextSelectorType.PASSWORD)
             )
         }
-        if not self.discovered:
+        if not self.discovered and not self.reauth_entry:
             schema = {vol.Required(CONF_HOST): TextSelector()} | schema
 
         return self.async_show_form(
@@ -173,6 +185,10 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
         """Handle device selection from devices offered by the cloud."""
         if self.discovered:
             user_input = {CONF_DEVICE: self.discovered_serial}
+        elif self.reauth_entry:
+            if self.reauth_entry.unique_id not in self.devices:
+                return self.async_abort(reason="reauth_device_not_found")
+            user_input = {CONF_DEVICE: self.reauth_entry.unique_id}
         elif len(self.devices) == 1:
             user_input = {CONF_DEVICE: list(self.devices.values())[0].serial_number}
 
@@ -223,10 +239,11 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
 
         device = await lametric.device()
 
-        await self.async_set_unique_id(device.serial_number)
-        self._abort_if_unique_id_configured(
-            updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key}
-        )
+        if not self.reauth_entry:
+            await self.async_set_unique_id(device.serial_number)
+            self._abort_if_unique_id_configured(
+                updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key}
+            )
 
         await lametric.notify(
             notification=Notification(
@@ -240,6 +257,20 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
             )
         )
 
+        if self.reauth_entry:
+            self.hass.config_entries.async_update_entry(
+                self.reauth_entry,
+                data={
+                    **self.reauth_entry.data,
+                    CONF_HOST: lametric.host,
+                    CONF_API_KEY: lametric.api_key,
+                },
+            )
+            self.hass.async_create_task(
+                self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
+            )
+            return self.async_abort(reason="reauth_successful")
+
         return self.async_create_entry(
             title=device.name,
             data={
diff --git a/homeassistant/components/lametric/coordinator.py b/homeassistant/components/lametric/coordinator.py
index 0a5e99e5668..88f34adf45c 100644
--- a/homeassistant/components/lametric/coordinator.py
+++ b/homeassistant/components/lametric/coordinator.py
@@ -1,11 +1,12 @@
 """DataUpdateCoordinator for the LaMatric integration."""
 from __future__ import annotations
 
-from demetriek import Device, LaMetricDevice, LaMetricError
+from demetriek import Device, LaMetricAuthenticationError, LaMetricDevice, LaMetricError
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_API_KEY, CONF_HOST
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryAuthFailed
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
@@ -32,6 +33,8 @@ class LaMetricDataUpdateCoordinator(DataUpdateCoordinator[Device]):
         """Fetch device information of the LaMetric device."""
         try:
             return await self.lametric.device()
+        except LaMetricAuthenticationError as err:
+            raise ConfigEntryAuthFailed from err
         except LaMetricError as ex:
             raise UpdateFailed(
                 "Could not fetch device information from LaMetric device"
diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json
index 433f70df18d..768f8e2b740 100644
--- a/homeassistant/components/lametric/strings.json
+++ b/homeassistant/components/lametric/strings.json
@@ -39,6 +39,8 @@
       "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.",
       "no_devices": "The authorized user has no LaMetric devices",
       "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
+      "reauth_device_not_found": "The device you are trying to re-authenticate is not found in this LaMetric account",
+      "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
       "unknown": "[%key:common::config_flow::error::unknown%]"
     }
   },
diff --git a/homeassistant/components/lametric/translations/en.json b/homeassistant/components/lametric/translations/en.json
index 52e483ec1f0..c36b490fcd2 100644
--- a/homeassistant/components/lametric/translations/en.json
+++ b/homeassistant/components/lametric/translations/en.json
@@ -8,6 +8,8 @@
             "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.",
             "no_devices": "The authorized user has no LaMetric devices",
             "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
+            "reauth_device_not_found": "The device you are trying to re-authenticate is not found in this LaMetric account",
+            "reauth_successful": "Re-authentication was successful",
             "unknown": "Unexpected error"
         },
         "error": {
diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py
index 338fe5052d1..a23b50c9813 100644
--- a/tests/components/lametric/test_config_flow.py
+++ b/tests/components/lametric/test_config_flow.py
@@ -18,7 +18,12 @@ from homeassistant.components.ssdp import (
     ATTR_UPNP_SERIAL,
     SsdpServiceInfo,
 )
-from homeassistant.config_entries import SOURCE_DHCP, SOURCE_SSDP, SOURCE_USER
+from homeassistant.config_entries import (
+    SOURCE_DHCP,
+    SOURCE_REAUTH,
+    SOURCE_SSDP,
+    SOURCE_USER,
+)
 from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC
 from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResultType
@@ -743,3 +748,173 @@ async def test_dhcp_unknown_device(
 
     assert result.get("type") == FlowResultType.ABORT
     assert result.get("reason") == "unknown"
+
+
+async def test_reauth_cloud_import(
+    hass: HomeAssistant,
+    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
+    aioclient_mock: AiohttpClientMocker,
+    current_request_with_host: None,
+    mock_setup_entry: MagicMock,
+    mock_lametric_cloud_config_flow: MagicMock,
+    mock_lametric_config_flow: MagicMock,
+    mock_config_entry: MockConfigEntry,
+) -> None:
+    """Test reauth flow importing api keys from the cloud."""
+    mock_config_entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={
+            "source": SOURCE_REAUTH,
+            "unique_id": mock_config_entry.unique_id,
+            "entry_id": mock_config_entry.entry_id,
+        },
+        data=mock_config_entry.data,
+    )
+
+    assert "flow_id" in result
+    flow_id = result["flow_id"]
+
+    await hass.config_entries.flow.async_configure(
+        flow_id, user_input={"next_step_id": "pick_implementation"}
+    )
+
+    # pylint: disable=protected-access
+    state = config_entry_oauth2_flow._encode_jwt(
+        hass,
+        {
+            "flow_id": flow_id,
+            "redirect_uri": "https://example.com/auth/external/callback",
+        },
+    )
+
+    client = await hass_client_no_auth()
+    await client.get(f"/auth/external/callback?code=abcd&state={state}")
+    aioclient_mock.post(
+        "https://developer.lametric.com/api/v2/oauth2/token",
+        json={
+            "refresh_token": "mock-refresh-token",
+            "access_token": "mock-access-token",
+            "type": "Bearer",
+            "expires_in": 60,
+        },
+    )
+
+    result2 = await hass.config_entries.flow.async_configure(flow_id)
+
+    assert result2.get("type") == FlowResultType.ABORT
+    assert result2.get("reason") == "reauth_successful"
+    assert mock_config_entry.data == {
+        CONF_HOST: "127.0.0.1",
+        CONF_API_KEY: "mock-api-key",
+        CONF_MAC: "AA:BB:CC:DD:EE:FF",
+    }
+
+    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
+    assert len(mock_lametric_config_flow.device.mock_calls) == 1
+    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
+
+
+async def test_reauth_cloud_abort_device_not_found(
+    hass: HomeAssistant,
+    hass_client_no_auth: Callable[[], Awaitable[TestClient]],
+    aioclient_mock: AiohttpClientMocker,
+    current_request_with_host: None,
+    mock_setup_entry: MagicMock,
+    mock_lametric_cloud_config_flow: MagicMock,
+    mock_lametric_config_flow: MagicMock,
+    mock_config_entry: MockConfigEntry,
+) -> None:
+    """Test reauth flow importing api keys from the cloud."""
+    mock_config_entry.unique_id = "UKNOWN_DEVICE"
+    mock_config_entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={
+            "source": SOURCE_REAUTH,
+            "unique_id": mock_config_entry.unique_id,
+            "entry_id": mock_config_entry.entry_id,
+        },
+        data=mock_config_entry.data,
+    )
+
+    assert "flow_id" in result
+    flow_id = result["flow_id"]
+
+    await hass.config_entries.flow.async_configure(
+        flow_id, user_input={"next_step_id": "pick_implementation"}
+    )
+
+    # pylint: disable=protected-access
+    state = config_entry_oauth2_flow._encode_jwt(
+        hass,
+        {
+            "flow_id": flow_id,
+            "redirect_uri": "https://example.com/auth/external/callback",
+        },
+    )
+
+    client = await hass_client_no_auth()
+    await client.get(f"/auth/external/callback?code=abcd&state={state}")
+    aioclient_mock.post(
+        "https://developer.lametric.com/api/v2/oauth2/token",
+        json={
+            "refresh_token": "mock-refresh-token",
+            "access_token": "mock-access-token",
+            "type": "Bearer",
+            "expires_in": 60,
+        },
+    )
+
+    result2 = await hass.config_entries.flow.async_configure(flow_id)
+
+    assert result2.get("type") == FlowResultType.ABORT
+    assert result2.get("reason") == "reauth_device_not_found"
+
+    assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1
+    assert len(mock_lametric_config_flow.device.mock_calls) == 0
+    assert len(mock_lametric_config_flow.notify.mock_calls) == 0
+
+
+async def test_reauth_manual(
+    hass: HomeAssistant,
+    mock_setup_entry: MagicMock,
+    mock_lametric_config_flow: MagicMock,
+    mock_config_entry: MockConfigEntry,
+) -> None:
+    """Test reauth flow with manual entry."""
+    mock_config_entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={
+            "source": SOURCE_REAUTH,
+            "unique_id": mock_config_entry.unique_id,
+            "entry_id": mock_config_entry.entry_id,
+        },
+        data=mock_config_entry.data,
+    )
+
+    assert "flow_id" in result
+    flow_id = result["flow_id"]
+
+    await hass.config_entries.flow.async_configure(
+        flow_id, user_input={"next_step_id": "manual_entry"}
+    )
+
+    result2 = await hass.config_entries.flow.async_configure(
+        flow_id, user_input={CONF_API_KEY: "mock-api-key"}
+    )
+
+    assert result2.get("type") == FlowResultType.ABORT
+    assert result2.get("reason") == "reauth_successful"
+    assert mock_config_entry.data == {
+        CONF_HOST: "127.0.0.1",
+        CONF_API_KEY: "mock-api-key",
+        CONF_MAC: "AA:BB:CC:DD:EE:FF",
+    }
+
+    assert len(mock_lametric_config_flow.device.mock_calls) == 1
+    assert len(mock_lametric_config_flow.notify.mock_calls) == 1
diff --git a/tests/components/lametric/test_init.py b/tests/components/lametric/test_init.py
index 965264e8917..50695fc4e55 100644
--- a/tests/components/lametric/test_init.py
+++ b/tests/components/lametric/test_init.py
@@ -3,11 +3,15 @@ from collections.abc import Awaitable, Callable
 from unittest.mock import MagicMock
 
 from aiohttp import ClientWebSocketResponse
-from demetriek import LaMetricConnectionError, LaMetricConnectionTimeoutError
+from demetriek import (
+    LaMetricAuthenticationError,
+    LaMetricConnectionError,
+    LaMetricConnectionTimeoutError,
+)
 import pytest
 
 from homeassistant.components.lametric.const import DOMAIN
-from homeassistant.config_entries import ConfigEntryState
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
 from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
 from homeassistant.core import HomeAssistant
 from homeassistant.setup import async_setup_component
@@ -70,3 +74,30 @@ async def test_yaml_config_raises_repairs(
     issues = await get_repairs(hass, hass_ws_client)
     assert len(issues) == 1
     assert issues[0]["issue_id"] == "manual_migration"
+
+
+async def test_config_entry_authentication_failed(
+    hass: HomeAssistant,
+    mock_config_entry: MockConfigEntry,
+    mock_lametric: MagicMock,
+) -> None:
+    """Test trigger reauthentication flow."""
+    mock_config_entry.add_to_hass(hass)
+
+    mock_lametric.device.side_effect = LaMetricAuthenticationError
+
+    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_ERROR
+
+    flows = hass.config_entries.flow.async_progress()
+    assert len(flows) == 1
+
+    flow = flows[0]
+    assert flow.get("step_id") == "choice_enter_manual_or_fetch_cloud"
+    assert flow.get("handler") == DOMAIN
+
+    assert "context" in flow
+    assert flow["context"].get("source") == SOURCE_REAUTH
+    assert flow["context"].get("entry_id") == mock_config_entry.entry_id
-- 
GitLab


From 9b06b572d0e3644204af2c0c6b286a2cefb48714 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Sat, 15 Oct 2022 00:48:44 +0200
Subject: [PATCH 495/985] Mark LaMetric as Platinum integration (#80360)

---
 homeassistant/components/lametric/manifest.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json
index 50c70943a88..f3d48c53058 100644
--- a/homeassistant/components/lametric/manifest.json
+++ b/homeassistant/components/lametric/manifest.json
@@ -13,5 +13,6 @@
       "deviceType": "urn:schemas-upnp-org:device:LaMetric:1"
     }
   ],
-  "dhcp": [{ "registered_devices": true }]
+  "dhcp": [{ "registered_devices": true }],
+  "quality_scale": "platinum"
 }
-- 
GitLab


From 2242d3c23408758614f877e45f29b8b34fa25a27 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Sat, 15 Oct 2022 00:33:41 +0000
Subject: [PATCH 496/985] [ci skip] Translation update

---
 .../components/coinbase/translations/ca.json  |  6 ++++
 .../components/coinbase/translations/de.json  |  6 ++++
 .../components/coinbase/translations/et.json  |  6 ++++
 .../components/coinbase/translations/ru.json  |  6 ++++
 .../components/coinbase/translations/tr.json  |  6 ++++
 .../coinbase/translations/zh-Hant.json        |  6 ++++
 .../devolo_home_network/translations/ja.json  |  8 +++++-
 .../devolo_home_network/translations/tr.json  |  8 +++++-
 .../components/ibeacon/translations/ja.json   | 21 ++++++++++++++
 .../components/kegtron/translations/ja.json   | 22 +++++++++++++++
 .../keymitt_ble/translations/ja.json          | 21 ++++++++++++++
 .../lametric/translations/select.ja.json      |  8 ++++++
 .../lametric/translations/select.tr.json      |  8 ++++++
 .../components/lidarr/translations/ja.json    | 28 +++++++++++++++++++
 .../lutron_caseta/translations/ja.json        |  3 ++
 .../lutron_caseta/translations/tr.json        |  3 ++
 .../components/nest/translations/tr.json      |  2 +-
 .../nibe_heatpump/translations/ja.json        | 13 +++++++++
 .../openexchangerates/translations/tr.json    |  4 +--
 .../components/overkiz/translations/tr.json   |  3 +-
 .../components/radarr/translations/ja.json    |  5 ++++
 .../components/snooz/translations/ja.json     | 21 ++++++++++++++
 .../components/snooz/translations/tr.json     | 27 ++++++++++++++++++
 .../volvooncall/translations/ja.json          |  1 +
 .../components/zwave_js/translations/ja.json  |  3 +-
 .../components/zwave_js/translations/tr.json  |  3 +-
 26 files changed, 240 insertions(+), 8 deletions(-)
 create mode 100644 homeassistant/components/ibeacon/translations/ja.json
 create mode 100644 homeassistant/components/kegtron/translations/ja.json
 create mode 100644 homeassistant/components/keymitt_ble/translations/ja.json
 create mode 100644 homeassistant/components/lametric/translations/select.ja.json
 create mode 100644 homeassistant/components/lametric/translations/select.tr.json
 create mode 100644 homeassistant/components/lidarr/translations/ja.json
 create mode 100644 homeassistant/components/snooz/translations/ja.json
 create mode 100644 homeassistant/components/snooz/translations/tr.json

diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json
index 116b611f272..a545f8a278f 100644
--- a/homeassistant/components/coinbase/translations/ca.json
+++ b/homeassistant/components/coinbase/translations/ca.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "La configuraci\u00f3 de Coinbase 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 Coinbase s'ha eliminat"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "L'API de Coinbase no proporciona algun/s dels saldos de moneda que has sol\u00b7licitat.",
diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json
index f6200633950..8f91208e58f 100644
--- a/homeassistant/components/coinbase/translations/de.json
+++ b/homeassistant/components/coinbase/translations/de.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Die Konfiguration von Coinbase mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.",
+            "title": "Die Coinbase YAML-Konfiguration wurde entfernt"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von deiner Coinbase-API nicht bereitgestellt.",
diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json
index 14bd1eea370..d91201b3eb0 100644
--- a/homeassistant/components/coinbase/translations/et.json
+++ b/homeassistant/components/coinbase/translations/et.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Coinbase'i seadistamine YAML-i abil on eemaldatud.\n\nHome Assistant ei kasuta olemasolevat YAML-i konfiguratsiooni.\n\nEemalda sidumine failist configuration.yaml ja taask\u00e4ivita selle probleemi lahendamiseks Home Assistant.",
+            "title": "Coinbase YAML konfiguratsioon on eemaldatud"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Coinbase API ei paku \u00fchte v\u00f5i mitut soovitud valuutasaldot.",
diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json
index cbdf39e61a6..85ec5646749 100644
--- a/homeassistant/components/coinbase/translations/ru.json
+++ b/homeassistant/components/coinbase/translations/ru.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Coinbase\" \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 Coinbase \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "\u041e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0441\u0442\u0430\u0442\u043a\u043e\u0432 \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0412\u0430\u0448\u0438\u043c API Coinbase.",
diff --git a/homeassistant/components/coinbase/translations/tr.json b/homeassistant/components/coinbase/translations/tr.json
index b84e2bf740e..ca91c29200e 100644
--- a/homeassistant/components/coinbase/translations/tr.json
+++ b/homeassistant/components/coinbase/translations/tr.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Coinbase'i 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": "Coinbase YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "\u0130stenen para birimi bakiyelerinden biri veya daha fazlas\u0131 Coinbase API'niz taraf\u0131ndan sa\u011flanm\u0131yor.",
diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json
index ea48d90fc7e..2b73d75d9bc 100644
--- a/homeassistant/components/coinbase/translations/zh-Hant.json
+++ b/homeassistant/components/coinbase/translations/zh-Hant.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Coinbase \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002",
+            "title": "Coinbase YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Coinbase API \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u8ca8\u5e63\u9918\u984d\u3002",
diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json
index 03612804d2d..eea08e9691f 100644
--- a/homeassistant/components/devolo_home_network/translations/ja.json
+++ b/homeassistant/components/devolo_home_network/translations/ja.json
@@ -2,7 +2,8 @@
     "config": {
         "abort": {
             "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059",
-            "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002"
+            "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u52d5\u4f5c\u3057\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",
@@ -10,6 +11,11 @@
         },
         "flow_title": "{product} ({name})",
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "\u30d1\u30b9\u30ef\u30fc\u30c9"
+                }
+            },
             "user": {
                 "data": {
                     "ip_address": "IP\u30a2\u30c9\u30ec\u30b9"
diff --git a/homeassistant/components/devolo_home_network/translations/tr.json b/homeassistant/components/devolo_home_network/translations/tr.json
index 841ae1773ca..def4954dc42 100644
--- a/homeassistant/components/devolo_home_network/translations/tr.json
+++ b/homeassistant/components/devolo_home_network/translations/tr.json
@@ -2,7 +2,8 @@
     "config": {
         "abort": {
             "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f",
-            "home_control": "devolo Ev Kontrol Merkezi Birimi bu entegrasyonla \u00e7al\u0131\u015fmaz."
+            "home_control": "devolo Ev Kontrol Merkezi Birimi bu entegrasyonla \u00e7al\u0131\u015fmaz.",
+            "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu"
         },
         "error": {
             "cannot_connect": "Ba\u011flanma hatas\u0131",
@@ -10,6 +11,11 @@
         },
         "flow_title": "{product} ({name})",
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "Parola"
+                }
+            },
             "user": {
                 "data": {
                     "ip_address": "IP Adresi"
diff --git a/homeassistant/components/ibeacon/translations/ja.json b/homeassistant/components/ibeacon/translations/ja.json
new file mode 100644
index 00000000000..e399d7b0026
--- /dev/null
+++ b/homeassistant/components/ibeacon/translations/ja.json
@@ -0,0 +1,21 @@
+{
+    "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"
+        },
+        "step": {
+            "user": {
+                "description": "iBeacon Tracker\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f"
+            }
+        }
+    },
+    "options": {
+        "step": {
+            "init": {
+                "data": {
+                    "min_rssi": "\u6700\u5c0fRSSI"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kegtron/translations/ja.json b/homeassistant/components/kegtron/translations/ja.json
new file mode 100644
index 00000000000..7e4f5db8e3b
--- /dev/null
+++ b/homeassistant/components/kegtron/translations/ja.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093",
+            "not_supported": "\u30c7\u30d0\u30a4\u30b9\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093"
+        },
+        "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/keymitt_ble/translations/ja.json b/homeassistant/components/keymitt_ble/translations/ja.json
new file mode 100644
index 00000000000..529dea51c30
--- /dev/null
+++ b/homeassistant/components/keymitt_ble/translations/ja.json
@@ -0,0 +1,21 @@
+{
+    "config": {
+        "abort": {
+            "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059",
+            "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f",
+            "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002",
+            "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc"
+        },
+        "flow_title": "{name}",
+        "step": {
+            "init": {
+                "data": {
+                    "name": "\u540d\u524d"
+                }
+            },
+            "link": {
+                "title": "\u30da\u30a2\u30ea\u30f3\u30b0"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/select.ja.json b/homeassistant/components/lametric/translations/select.ja.json
new file mode 100644
index 00000000000..b918cfa7a7a
--- /dev/null
+++ b/homeassistant/components/lametric/translations/select.ja.json
@@ -0,0 +1,8 @@
+{
+    "state": {
+        "lametric__brightness_mode": {
+            "auto": "\u81ea\u52d5",
+            "manual": "\u30de\u30cb\u30e5\u30a2\u30eb"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/select.tr.json b/homeassistant/components/lametric/translations/select.tr.json
new file mode 100644
index 00000000000..42b1db54d35
--- /dev/null
+++ b/homeassistant/components/lametric/translations/select.tr.json
@@ -0,0 +1,8 @@
+{
+    "state": {
+        "lametric__brightness_mode": {
+            "auto": "Otomatik",
+            "manual": "Manuel"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lidarr/translations/ja.json b/homeassistant/components/lidarr/translations/ja.json
new file mode 100644
index 00000000000..7ecdda821ea
--- /dev/null
+++ b/homeassistant/components/lidarr/translations/ja.json
@@ -0,0 +1,28 @@
+{
+    "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"
+        },
+        "error": {
+            "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f",
+            "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c",
+            "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc"
+        },
+        "step": {
+            "reauth_confirm": {
+                "data": {
+                    "api_key": "API\u30ad\u30fc"
+                },
+                "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/lutron_caseta/translations/ja.json b/homeassistant/components/lutron_caseta/translations/ja.json
index f32e47e9942..615ee90bded 100644
--- a/homeassistant/components/lutron_caseta/translations/ja.json
+++ b/homeassistant/components/lutron_caseta/translations/ja.json
@@ -33,6 +33,9 @@
             "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3",
             "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3",
             "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3",
+            "button_5": "5\u756a\u76ee\u306e\u30dc\u30bf\u30f3",
+            "button_6": "6\u756a\u76ee\u306e\u30dc\u30bf\u30f3",
+            "button_7": "7\u756a\u76ee\u306e\u30dc\u30bf\u30f3",
             "close_1": "\u30af\u30ed\u30fc\u30ba1",
             "close_2": "\u30af\u30ed\u30fc\u30ba2",
             "close_3": "\u30af\u30ed\u30fc\u30ba3",
diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json
index 54c5530224c..df94879f332 100644
--- a/homeassistant/components/lutron_caseta/translations/tr.json
+++ b/homeassistant/components/lutron_caseta/translations/tr.json
@@ -33,6 +33,9 @@
             "button_2": "\u0130kinci d\u00fc\u011fme",
             "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme",
             "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme",
+            "button_5": "Be\u015finci d\u00fc\u011fme",
+            "button_6": "Alt\u0131nc\u0131 d\u00fc\u011fme",
+            "button_7": "Yedinci d\u00fc\u011fme",
             "close_1": "Kapat 1",
             "close_2": "Kapat 2",
             "close_3": "Kapat 3",
diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json
index 986412798dc..4e52843ca51 100644
--- a/homeassistant/components/nest/translations/tr.json
+++ b/homeassistant/components/nest/translations/tr.json
@@ -52,7 +52,7 @@
                 "data": {
                     "project_id": "Cihaz Eri\u015fim Projesi Kimli\u011fi"
                 },
-                "description": "Kurmak i\u00e7n **5 ABD dolar\u0131 \u00fccret** gerektiren bir Nest Cihaz Eri\u015fimi projesi olu\u015fturun.\n 1. [Cihaz Eri\u015fim Konsolu]( {device_access_console_url} )'e gidin ve \u00f6deme ak\u0131\u015f\u0131ndan ge\u00e7in.\n 1. **Proje olu\u015ftur**'a t\u0131klay\u0131n\n 1. Cihaz Eri\u015fimi projenize bir ad verin ve **\u0130leri**'ye t\u0131klay\u0131n.\n 1. OAuth M\u00fc\u015fteri Kimli\u011finizi girin\n 1. **Etkinle\u015ftir** ve **Proje olu\u015ftur**'a t\u0131klayarak etkinlikleri etkinle\u015ftirin. \n\n Cihaz Eri\u015fim Projesi Kimli\u011finizi a\u015fa\u011f\u0131ya girin ([daha fazla bilgi]( {more_info_url} )).\n",
+                "description": "Kurulum i\u00e7in **Google'a 5 ABD dolar\u0131 tutar\u0131nda bir \u00fccret \u00f6denmesini gerektiren** bir Nest Cihaz Eri\u015fimi projesi olu\u015fturun.\n 1. [Cihaz Eri\u015fim Konsolu]( {device_access_console_url} )'e gidin ve \u00f6deme ak\u0131\u015f\u0131ndan ge\u00e7in.\n 1. **Proje olu\u015ftur**'a t\u0131klay\u0131n\n 1. Cihaz Eri\u015fimi projenize bir ad verin ve **\u0130leri**'ye t\u0131klay\u0131n.\n 1. OAuth M\u00fc\u015fteri Kimli\u011finizi girin\n 1. **Etkinle\u015ftir** ve **Proje olu\u015ftur**'a t\u0131klayarak etkinlikleri etkinle\u015ftirin. \n\n Cihaz Eri\u015fim Projesi Kimli\u011finizi a\u015fa\u011f\u0131ya girin ([daha fazla bilgi]( {more_info_url} )).\n",
                 "title": "Yuva: Bir Cihaz Eri\u015fim Projesi Olu\u015fturun"
             },
             "device_project_upgrade": {
diff --git a/homeassistant/components/nibe_heatpump/translations/ja.json b/homeassistant/components/nibe_heatpump/translations/ja.json
index 9ad7fd4a7aa..6ca4ad37a81 100644
--- a/homeassistant/components/nibe_heatpump/translations/ja.json
+++ b/homeassistant/components/nibe_heatpump/translations/ja.json
@@ -1,7 +1,20 @@
 {
     "config": {
+        "abort": {
+            "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059"
+        },
         "error": {
             "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "ip_address": "\u30ea\u30e2\u30fc\u30c8IP\u30a2\u30c9\u30ec\u30b9",
+                    "listening_port": "\u30ed\u30fc\u30ab\u30eb\u30ea\u30b9\u30cb\u30f3\u30b0\u30dd\u30fc\u30c8",
+                    "remote_read_port": "\u30ea\u30e2\u30fc\u30c8\u8aad\u307f\u53d6\u308a\u30dd\u30fc\u30c8",
+                    "remote_write_port": "\u30ea\u30e2\u30fc\u30c8\u66f8\u304d\u8fbc\u307f\u30dd\u30fc\u30c8"
+                }
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/openexchangerates/translations/tr.json b/homeassistant/components/openexchangerates/translations/tr.json
index 436e6bbb07b..4149d5bd52d 100644
--- a/homeassistant/components/openexchangerates/translations/tr.json
+++ b/homeassistant/components/openexchangerates/translations/tr.json
@@ -26,8 +26,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "YAML kullanarak A\u00e7\u0131k D\u00f6viz Kurlar\u0131n\u0131 yap\u0131land\u0131rma 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 Open Exchange Rates 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": "A\u00e7\u0131k D\u00f6viz Kurlar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor"
+            "description": "YAML kullanarak A\u00e7\u0131k D\u00f6viz Kurlar\u0131n\u0131 yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Open Exchange Rates 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": "A\u00e7\u0131k D\u00f6viz Kurlar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json
index 3981f7dbc8c..ed82e138673 100644
--- a/homeassistant/components/overkiz/translations/tr.json
+++ b/homeassistant/components/overkiz/translations/tr.json
@@ -12,7 +12,8 @@
             "too_many_attempts": "Ge\u00e7ersiz anahtarla \u00e7ok fazla deneme, ge\u00e7ici olarak yasakland\u0131",
             "too_many_requests": "\u00c7ok fazla istek var, daha sonra tekrar deneyin",
             "unknown": "Beklenmeyen hata",
-            "unknown_user": "Bilinmeyen kullan\u0131c\u0131. Somfy Protect hesaplar\u0131 bu entegrasyon taraf\u0131ndan desteklenmez."
+            "unknown_user": "Bilinmeyen kullan\u0131c\u0131. Somfy Protect hesaplar\u0131 bu entegrasyon taraf\u0131ndan desteklenmez.",
+            "unsupported_hardware": "{unsupported_device} donan\u0131m\u0131n\u0131z bu entegrasyon taraf\u0131ndan desteklenmiyor."
         },
         "flow_title": "A\u011f ge\u00e7idi: {gateway_id}",
         "step": {
diff --git a/homeassistant/components/radarr/translations/ja.json b/homeassistant/components/radarr/translations/ja.json
index 26281a46d6d..65d7d08865b 100644
--- a/homeassistant/components/radarr/translations/ja.json
+++ b/homeassistant/components/radarr/translations/ja.json
@@ -1,6 +1,11 @@
 {
     "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"
+        },
         "error": {
+            "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f",
             "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c",
             "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc"
         },
diff --git a/homeassistant/components/snooz/translations/ja.json b/homeassistant/components/snooz/translations/ja.json
new file mode 100644
index 00000000000..38f862bd2f6
--- /dev/null
+++ b/homeassistant/components/snooz/translations/ja.json
@@ -0,0 +1,21 @@
+{
+    "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",
+            "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093"
+        },
+        "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/snooz/translations/tr.json b/homeassistant/components/snooz/translations/tr.json
new file mode 100644
index 00000000000..8b4d9cc646c
--- /dev/null
+++ b/homeassistant/components/snooz/translations/tr.json
@@ -0,0 +1,27 @@
+{
+    "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"
+        },
+        "flow_title": "{name}",
+        "progress": {
+            "wait_for_pairing_mode": "Kurulumu tamamlamak i\u00e7in bu cihaz\u0131 e\u015fle\u015ftirme moduna al\u0131n. \n\n ### E\u015fle\u015ftirme moduna nas\u0131l girilir\n 1. SNOOZ mobil uygulamalar\u0131ndan \u00e7\u0131kmaya zorlay\u0131n.\n 2. Cihazdaki g\u00fc\u00e7 d\u00fc\u011fmesini bas\u0131l\u0131 tutun. I\u015f\u0131klar yan\u0131p s\u00f6nmeye ba\u015flad\u0131\u011f\u0131nda b\u0131rak\u0131n (yakla\u015f\u0131k 5 saniye)."
+        },
+        "step": {
+            "bluetooth_confirm": {
+                "description": "{name} kurulumunu yapmak istiyor musunuz?"
+            },
+            "pairing_timeout": {
+                "description": "Cihaz e\u015fle\u015ftirme moduna girmedi. Tekrar denemek i\u00e7in G\u00f6nder'i t\u0131klay\u0131n. \n\n ### Sorun giderme\n 1. Cihaz\u0131n mobil uygulamaya ba\u011fl\u0131 olmad\u0131\u011f\u0131n\u0131 kontrol edin.\n 2. Ayg\u0131t\u0131n fi\u015fini 5 saniyeli\u011fine \u00e7ekin, ard\u0131ndan tekrar tak\u0131n."
+            },
+            "user": {
+                "data": {
+                    "address": "Cihaz"
+                },
+                "description": "Kurulum i\u00e7in bir cihaz se\u00e7in"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volvooncall/translations/ja.json b/homeassistant/components/volvooncall/translations/ja.json
index 0f56a700da0..4127b966710 100644
--- a/homeassistant/components/volvooncall/translations/ja.json
+++ b/homeassistant/components/volvooncall/translations/ja.json
@@ -15,6 +15,7 @@
                     "password": "\u30d1\u30b9\u30ef\u30fc\u30c9",
                     "region": "\u30ea\u30fc\u30b8\u30e7\u30f3",
                     "scandinavian_miles": "\u30b9\u30ab\u30f3\u30b8\u30ca\u30d3\u30a2\u30de\u30a4\u30eb(Scandinavian Miles)\u3092\u4f7f\u7528\u3059\u308b",
+                    "unit_system": "\u5358\u4f4d\u30b7\u30b9\u30c6\u30e0",
                     "username": "\u30e6\u30fc\u30b6\u30fc\u540d"
                 }
             }
diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json
index 41568815193..c42fff18139 100644
--- a/homeassistant/components/zwave_js/translations/ja.json
+++ b/homeassistant/components/zwave_js/translations/ja.json
@@ -10,7 +10,8 @@
             "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",
             "discovery_requires_supervisor": "\u691c\u51fa\u306b\u306fSupervisor\u304c\u5fc5\u8981\u3067\u3059\u3002",
-            "not_zwave_device": "\u767a\u898b\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Z-Wave\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002"
+            "not_zwave_device": "\u767a\u898b\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Z-Wave\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002",
+            "not_zwave_js_addon": "\u767a\u898b\u3055\u308c\u305f\u30a2\u30c9\u30aa\u30f3\u306f\u3001Z-Wave JS\u306e\u516c\u5f0f\u30a2\u30c9\u30aa\u30f3\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002"
         },
         "error": {
             "addon_start_failed": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json
index edf75d1dfb7..ed45816c2dd 100644
--- a/homeassistant/components/zwave_js/translations/tr.json
+++ b/homeassistant/components/zwave_js/translations/tr.json
@@ -10,7 +10,8 @@
             "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor",
             "cannot_connect": "Ba\u011flanma hatas\u0131",
             "discovery_requires_supervisor": "Tarama, s\u00fcperviz\u00f6r\u00fc gerektirir.",
-            "not_zwave_device": "Bulunan cihaz bir Z-Wave cihaz\u0131 de\u011fil."
+            "not_zwave_device": "Bulunan cihaz bir Z-Wave cihaz\u0131 de\u011fil.",
+            "not_zwave_js_addon": "Ke\u015ffedilen eklenti, resmi Z-Wave JS eklentisi de\u011fildir."
         },
         "error": {
             "addon_start_failed": "Z-Wave JS eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin.",
-- 
GitLab


From eaeee96d75ef501547dbb8d391832eb2f1af27fd Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Sat, 15 Oct 2022 12:15:28 +0300
Subject: [PATCH 497/985] Fix Shelly EM negative power factor (#80348)

---
 homeassistant/components/shelly/sensor.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py
index 0e507b59431..3fabf69ad54 100644
--- a/homeassistant/components/shelly/sensor.py
+++ b/homeassistant/components/shelly/sensor.py
@@ -144,7 +144,7 @@ SENSORS: Final = {
         key="emeter|powerFactor",
         name="Power Factor",
         native_unit_of_measurement=PERCENTAGE,
-        value=lambda value: round(value * 100, 1),
+        value=lambda value: abs(round(value * 100, 1)),
         device_class=SensorDeviceClass.POWER_FACTOR,
         state_class=SensorStateClass.MEASUREMENT,
     ),
-- 
GitLab


From e1520a0d14a59657fbfb598446931fe964fbb68e Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Sat, 15 Oct 2022 12:17:53 +0300
Subject: [PATCH 498/985] Add support for Shelly Plus Addon sensors (#79954)

---
 homeassistant/components/shelly/const.py  |  3 ---
 homeassistant/components/shelly/sensor.py | 26 ++++++++++++++++++++---
 homeassistant/components/shelly/utils.py  | 21 ++----------------
 3 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py
index 89820a289ed..401b8131487 100644
--- a/homeassistant/components/shelly/const.py
+++ b/homeassistant/components/shelly/const.py
@@ -158,9 +158,6 @@ KELVIN_MIN_VALUE_COLOR: Final = 3000
 
 UPTIME_DEVIATION: Final = 5
 
-# Max RPC switch/input key instances
-MAX_RPC_KEY_INSTANCES = 4
-
 # Time to wait before reloading entry upon device config change
 ENTRY_RELOAD_COOLDOWN = 60
 
diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py
index 3fabf69ad54..b4516b098a2 100644
--- a/homeassistant/components/shelly/sensor.py
+++ b/homeassistant/components/shelly/sensor.py
@@ -348,7 +348,7 @@ RPC_SENSORS: Final = {
     "temperature": RpcSensorDescription(
         key="switch",
         sub_key="temperature",
-        name="Temperature",
+        name="Device Temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         value=lambda status, _: round(status["tC"], 1),
         device_class=SensorDeviceClass.TEMPERATURE,
@@ -358,7 +358,7 @@ RPC_SENSORS: Final = {
         use_polling_coordinator=True,
     ),
     "temperature_0": RpcSensorDescription(
-        key="temperature:0",
+        key="temperature",
         sub_key="tC",
         name="Temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
@@ -389,7 +389,7 @@ RPC_SENSORS: Final = {
         use_polling_coordinator=True,
     ),
     "humidity_0": RpcSensorDescription(
-        key="humidity:0",
+        key="humidity",
         sub_key="rh",
         name="Humidity",
         native_unit_of_measurement=PERCENTAGE,
@@ -410,6 +410,26 @@ RPC_SENSORS: Final = {
         entity_registry_enabled_default=True,
         entity_category=EntityCategory.DIAGNOSTIC,
     ),
+    "voltmeter": RpcSensorDescription(
+        key="voltmeter",
+        sub_key="voltage",
+        name="Voltmeter",
+        native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
+        value=lambda status, _: round(float(status), 2),
+        device_class=SensorDeviceClass.VOLTAGE,
+        state_class=SensorStateClass.MEASUREMENT,
+        entity_registry_enabled_default=True,
+        available=lambda status: status is not None,
+    ),
+    "analoginput": RpcSensorDescription(
+        key="analoginput",
+        sub_key="percent",
+        name="Analog Input",
+        native_unit_of_measurement=PERCENTAGE,
+        device_class=SensorDeviceClass.BATTERY,
+        state_class=SensorStateClass.MEASUREMENT,
+        entity_registry_enabled_default=True,
+    ),
 }
 
 
diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py
index 7eeb93f2918..985935b3939 100644
--- a/homeassistant/components/shelly/utils.py
+++ b/homeassistant/components/shelly/utils.py
@@ -21,7 +21,6 @@ from .const import (
     DEFAULT_COAP_PORT,
     DOMAIN,
     LOGGER,
-    MAX_RPC_KEY_INSTANCES,
     RPC_INPUTS_EVENTS_TYPES,
     SHBTN_INPUTS_EVENTS_TYPES,
     SHBTN_MODELS,
@@ -303,28 +302,12 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]:
     if key == "switch" and "cover:0" in keys_dict:
         key = "cover"
 
-    keys_list: list[str] = []
-    for i in range(MAX_RPC_KEY_INSTANCES):
-        key_inst = f"{key}:{i}"
-        if key_inst not in keys_dict:
-            return keys_list
-
-        keys_list.append(key_inst)
-
-    return keys_list
+    return [k for k in keys_dict if k.startswith(key)]
 
 
 def get_rpc_key_ids(keys_dict: dict[str, Any], key: str) -> list[int]:
     """Return list of key ids for RPC device from a dict."""
-    key_ids: list[int] = []
-    for i in range(MAX_RPC_KEY_INSTANCES):
-        key_inst = f"{key}:{i}"
-        if key_inst not in keys_dict:
-            return key_ids
-
-        key_ids.append(i)
-
-    return key_ids
+    return [int(k.split(":")[1]) for k in keys_dict if k.startswith(key)]
 
 
 def is_rpc_momentary_input(
-- 
GitLab


From 3460e0b074881ae62fe3019538325f71eb3c7a88 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Sat, 15 Oct 2022 15:38:47 +0200
Subject: [PATCH 499/985] Add type hints to aqualogic (#80328)

---
 .strict-typing                                |  1 +
 .../components/aqualogic/__init__.py          | 23 +++++++++++--------
 homeassistant/components/aqualogic/sensor.py  | 12 ++++++----
 homeassistant/components/aqualogic/switch.py  | 11 ++++-----
 mypy.ini                                      | 10 ++++++++
 5 files changed, 37 insertions(+), 20 deletions(-)

diff --git a/.strict-typing b/.strict-typing
index 623653ac203..2e6d68edc4c 100644
--- a/.strict-typing
+++ b/.strict-typing
@@ -57,6 +57,7 @@ homeassistant.components.ambient_station.*
 homeassistant.components.amcrest.*
 homeassistant.components.ampio.*
 homeassistant.components.anthemav.*
+homeassistant.components.aqualogic.*
 homeassistant.components.aseko_pool_live.*
 homeassistant.components.asuswrt.*
 homeassistant.components.auth.*
diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py
index 94941b30713..28e57c2b351 100644
--- a/homeassistant/components/aqualogic/__init__.py
+++ b/homeassistant/components/aqualogic/__init__.py
@@ -1,4 +1,6 @@
 """Support for AquaLogic devices."""
+from __future__ import annotations
+
 from datetime import timedelta
 import logging
 import threading
@@ -13,7 +15,7 @@ from homeassistant.const import (
     EVENT_HOMEASSISTANT_START,
     EVENT_HOMEASSISTANT_STOP,
 )
-from homeassistant.core import HomeAssistant
+from homeassistant.core import Event, HomeAssistant
 from homeassistant.helpers import config_validation as cv
 from homeassistant.helpers.dispatcher import dispatcher_send
 from homeassistant.helpers.typing import ConfigType
@@ -50,7 +52,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
 class AquaLogicProcessor(threading.Thread):
     """AquaLogic event processor thread."""
 
-    def __init__(self, hass, host, port):
+    def __init__(self, hass: HomeAssistant, host: str, port: int) -> None:
         """Initialize the data object."""
         super().__init__(daemon=True)
         self._hass = hass
@@ -59,27 +61,28 @@ class AquaLogicProcessor(threading.Thread):
         self._shutdown = False
         self._panel = None
 
-    def start_listen(self, event):
+    def start_listen(self, event: Event) -> None:
         """Start event-processing thread."""
         _LOGGER.debug("Event processing thread started")
         self.start()
 
-    def shutdown(self, event):
+    def shutdown(self, event: Event) -> None:
         """Signal shutdown of processing event."""
         _LOGGER.debug("Event processing signaled exit")
         self._shutdown = True
 
-    def data_changed(self, panel):
+    def data_changed(self, panel: AquaLogic) -> None:
         """Aqualogic data changed callback."""
         dispatcher_send(self._hass, UPDATE_TOPIC)
 
-    def run(self):
+    def run(self) -> None:
         """Event thread."""
 
         while True:
-            self._panel = AquaLogic()
-            self._panel.connect(self._host, self._port)
-            self._panel.process(self.data_changed)
+            panel = AquaLogic()
+            self._panel = panel
+            panel.connect(self._host, self._port)
+            panel.process(self.data_changed)
 
             if self._shutdown:
                 return
@@ -88,6 +91,6 @@ class AquaLogicProcessor(threading.Thread):
             time.sleep(RECONNECT_INTERVAL.total_seconds())
 
     @property
-    def panel(self):
+    def panel(self) -> AquaLogic | None:
         """Retrieve the AquaLogic object."""
         return self._panel
diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py
index d575beb0367..e8abc3bae62 100644
--- a/homeassistant/components/aqualogic/sensor.py
+++ b/homeassistant/components/aqualogic/sensor.py
@@ -24,7 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
-from . import DOMAIN, UPDATE_TOPIC
+from . import DOMAIN, UPDATE_TOPIC, AquaLogicProcessor
 
 
 @dataclass
@@ -120,7 +120,7 @@ async def async_setup_platform(
     discovery_info: DiscoveryInfoType | None = None,
 ) -> None:
     """Set up the sensor platform."""
-    processor = hass.data[DOMAIN]
+    processor: AquaLogicProcessor = hass.data[DOMAIN]
     monitored_conditions = config[CONF_MONITORED_CONDITIONS]
 
     entities = [
@@ -138,7 +138,11 @@ class AquaLogicSensor(SensorEntity):
     entity_description: AquaLogicSensorEntityDescription
     _attr_should_poll = False
 
-    def __init__(self, processor, description: AquaLogicSensorEntityDescription):
+    def __init__(
+        self,
+        processor: AquaLogicProcessor,
+        description: AquaLogicSensorEntityDescription,
+    ) -> None:
         """Initialize sensor."""
         self.entity_description = description
         self._processor = processor
@@ -153,7 +157,7 @@ class AquaLogicSensor(SensorEntity):
         )
 
     @callback
-    def async_update_callback(self):
+    def async_update_callback(self) -> None:
         """Update callback."""
         if (panel := self._processor.panel) is not None:
             if panel.is_metric:
diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py
index e04bc8595fa..e693df0a0c1 100644
--- a/homeassistant/components/aqualogic/switch.py
+++ b/homeassistant/components/aqualogic/switch.py
@@ -14,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
-from . import DOMAIN, UPDATE_TOPIC
+from . import DOMAIN, UPDATE_TOPIC, AquaLogicProcessor
 
 SWITCH_TYPES = {
     "lights": "Lights",
@@ -47,7 +47,7 @@ async def async_setup_platform(
     """Set up the switch platform."""
     switches = []
 
-    processor = hass.data[DOMAIN]
+    processor: AquaLogicProcessor = hass.data[DOMAIN]
     for switch_type in config[CONF_MONITORED_CONDITIONS]:
         switches.append(AquaLogicSwitch(processor, switch_type))
 
@@ -59,7 +59,7 @@ class AquaLogicSwitch(SwitchEntity):
 
     _attr_should_poll = False
 
-    def __init__(self, processor, switch_type):
+    def __init__(self, processor: AquaLogicProcessor, switch_type: str) -> None:
         """Initialize switch."""
         self._processor = processor
         self._state_name = {
@@ -77,12 +77,11 @@ class AquaLogicSwitch(SwitchEntity):
         self._attr_name = f"AquaLogic {SWITCH_TYPES[switch_type]}"
 
     @property
-    def is_on(self):
+    def is_on(self) -> bool:
         """Return true if device is on."""
         if (panel := self._processor.panel) is None:
             return False
-        state = panel.get_state(self._state_name)
-        return state
+        return panel.get_state(self._state_name)  # type: ignore[no-any-return]
 
     def turn_on(self, **kwargs: Any) -> None:
         """Turn the device on."""
diff --git a/mypy.ini b/mypy.ini
index 7e5133f38e9..6f6e1921bb5 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -322,6 +322,16 @@ disallow_untyped_defs = true
 warn_return_any = true
 warn_unreachable = true
 
+[mypy-homeassistant.components.aqualogic.*]
+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.aseko_pool_live.*]
 check_untyped_defs = true
 disallow_incomplete_defs = true
-- 
GitLab


From d12cbab6c4c61c48bc72ebbc3fe9483db107023c Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sat, 15 Oct 2022 07:57:23 -1000
Subject: [PATCH 500/985] Bump bleak to 0.19.0 (#80349)

---
 homeassistant/components/bluetooth/const.py   |   2 +-
 homeassistant/components/bluetooth/manager.py | 153 ++++++------
 .../components/bluetooth/manifest.json        |   4 +-
 homeassistant/components/bluetooth/models.py  |  30 ++-
 homeassistant/components/bluetooth/scanner.py |  18 +-
 .../components/esphome/bluetooth/scanner.py   |  41 +++-
 .../components/homekit_controller/sensor.py   |   9 +-
 .../components/switchbot/coordinator.py       |   2 +
 homeassistant/components/switchbot/sensor.py  |   3 +-
 homeassistant/package_constraints.txt         |   6 +-
 pyproject.toml                                |   2 +-
 requirements.txt                              |   2 +-
 requirements_all.txt                          |   4 +-
 requirements_test_all.txt                     |   4 +-
 tests/components/airthings_ble/__init__.py    |   7 +-
 tests/components/bluetooth/__init__.py        |  34 ++-
 .../bluetooth/test_advertisement_tracker.py   |  65 +++--
 .../components/bluetooth/test_diagnostics.py  |  67 +++---
 tests/components/bluetooth/test_init.py       | 224 +++++++++++-------
 tests/components/bluetooth/test_manager.py    | 152 +++++++++---
 tests/components/bluetooth/test_models.py     |  82 +++++--
 .../test_passive_update_coordinator.py        |   4 +-
 .../test_passive_update_processor.py          |   4 +-
 tests/components/bluetooth/test_scanner.py    |  10 +-
 .../test_device_tracker.py                    |  13 +-
 tests/components/bthome/__init__.py           |  17 +-
 tests/components/fjaraskupan/__init__.py      |   5 +-
 tests/components/keymitt_ble/__init__.py      |   5 +-
 tests/components/led_ble/__init__.py          |   9 +-
 tests/components/melnor/conftest.py           |   6 +-
 tests/components/switchbot/__init__.py        |  16 +-
 tests/components/xiaomi_ble/__init__.py       |  17 +-
 tests/components/yalexs_ble/__init__.py       |  11 +-
 33 files changed, 638 insertions(+), 390 deletions(-)

diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py
index 2ad05d80c7a..9ec505bd6bf 100644
--- a/homeassistant/components/bluetooth/const.py
+++ b/homeassistant/components/bluetooth/const.py
@@ -74,4 +74,4 @@ ADAPTER_HW_VERSION: Final = "hw_version"
 ADAPTER_PASSIVE_SCAN: Final = "passive_scan"
 
 
-NO_RSSI_VALUE: Final = -1000
+NO_RSSI_VALUE: Final = -127
diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index 07330396bbd..9c7954eb86a 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 
 import asyncio
 from collections.abc import Callable, Iterable
+from dataclasses import replace
 from datetime import datetime, timedelta
 import itertools
 import logging
@@ -121,7 +122,8 @@ class BluetoothManager:
         self._bleak_callbacks: list[
             tuple[AdvertisementDataCallback, dict[str, set[str]]]
         ] = []
-        self._history: dict[str, BluetoothServiceInfoBleak] = {}
+        self._all_history: dict[str, BluetoothServiceInfoBleak] = {}
+        self._non_connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
         self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
         self._non_connectable_scanners: list[BaseHaScanner] = []
         self._connectable_scanners: list[BaseHaScanner] = []
@@ -155,8 +157,9 @@ class BluetoothManager:
                 service_info.as_dict()
                 for service_info in self._connectable_history.values()
             ],
-            "history": [
-                service_info.as_dict() for service_info in self._history.values()
+            "non_connectable_history": [
+                service_info.as_dict()
+                for service_info in self._non_connectable_history.values()
             ],
             "advertisement_tracker": self._advertisement_tracker.async_diagnostics(),
         }
@@ -189,7 +192,7 @@ class BluetoothManager:
         # Everything is connectable so it fall into both
         # buckets since the host system can only provide
         # connectable devices
-        self._history = history.copy()
+        self._all_history = history.copy()
         self._connectable_history = history.copy()
         self.async_setup_unavailable_tracking()
 
@@ -202,32 +205,32 @@ class BluetoothManager:
             self._cancel_unavailable_tracking = None
         uninstall_multiple_bleak_catcher()
 
-    async def async_get_devices_by_address(
+    @hass_callback
+    def async_get_discovered_devices_and_advertisement_data_by_address(
         self, address: str, connectable: bool
-    ) -> list[BLEDevice]:
-        """Get devices by address."""
+    ) -> list[tuple[BLEDevice, AdvertisementData]]:
+        """Get devices and advertisement_data by address."""
         types_ = (True,) if connectable else (True, False)
         return [
-            device
-            for device in await asyncio.gather(
-                *(
-                    scanner.async_get_device_by_address(address)
-                    for type_ in types_
-                    for scanner in self._get_scanners_by_type(type_)
-                )
+            device_advertisement_data
+            for device_advertisement_data in (
+                scanner.discovered_devices_and_advertisement_data.get(address)
+                for type_ in types_
+                for scanner in self._get_scanners_by_type(type_)
             )
-            if device is not None
+            if device_advertisement_data is not None
         ]
 
     @hass_callback
-    def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]:
-        """Return all of discovered devices from all the scanners including duplicates."""
+    def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]:
+        """Return all of discovered addresses from all the scanners including duplicates."""
         yield from itertools.chain.from_iterable(
-            scanner.discovered_devices for scanner in self._get_scanners_by_type(True)
+            scanner.discovered_devices_and_advertisement_data
+            for scanner in self._get_scanners_by_type(True)
         )
         if not connectable:
             yield from itertools.chain.from_iterable(
-                scanner.discovered_devices
+                scanner.discovered_devices_and_advertisement_data
                 for scanner in self._get_scanners_by_type(False)
             )
 
@@ -253,33 +256,38 @@ class BluetoothManager:
         """Watch for unavailable devices and cleanup state history."""
         monotonic_now = MONOTONIC_TIME()
         connectable_history = self._connectable_history
-        all_history = self._history
-        removed_addresses: set[str] = set()
+        non_connectable_history = self._non_connectable_history
+        all_history = self._all_history
+        tracker = self._advertisement_tracker
+        intervals = tracker.intervals
 
         for connectable in (True, False):
             unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable)
-            intervals = self._advertisement_tracker.intervals
             history = connectable_history if connectable else all_history
-            history_set = set(history)
-            active_addresses = {
-                device.address
-                for device in self.async_all_discovered_devices(connectable)
-            }
-            disappeared = history_set.difference(active_addresses)
+            disappeared = set(history).difference(
+                self._async_all_discovered_addresses(connectable)
+            )
             for address in disappeared:
-                #
-                # For non-connectable devices we also check the device has exceeded
-                # the advertising interval before we mark it as unavailable
-                # since it may have gone to sleep and since we do not need an active connection
-                # to it we can only determine its availability by the lack of advertisements
-                #
-                if not connectable and (advertising_interval := intervals.get(address)):
-                    time_since_seen = monotonic_now - history[address].time
-                    if time_since_seen <= advertising_interval:
-                        continue
+                if not connectable:
+                    #
+                    # For non-connectable devices we also check the device has exceeded
+                    # the advertising interval before we mark it as unavailable
+                    # since it may have gone to sleep and since we do not need an active connection
+                    # to it we can only determine its availability by the lack of advertisements
+                    #
+                    if advertising_interval := intervals.get(address):
+                        time_since_seen = monotonic_now - all_history[address].time
+                        if time_since_seen <= advertising_interval:
+                            continue
+
+                    non_connectable_history.pop(address, None)
+
+                    # The second loop (connectable=False) is responsible for removing
+                    # the device from all the interval tracking since it is no longer
+                    # available for both connectable and non-connectable
+                    tracker.async_remove_address(address)
 
                 service_info = history.pop(address)
-                removed_addresses.add(address)
 
                 if not (callbacks := unavailable_callbacks.get(address)):
                     continue
@@ -290,14 +298,10 @@ class BluetoothManager:
                     except Exception:  # pylint: disable=broad-except
                         _LOGGER.exception("Error in unavailable callback")
 
-        # If we removed the device from both the connectable history
-        # and all history then we can remove it from the advertisement tracker
-        for address in removed_addresses:
-            if address not in connectable_history and address not in all_history:
-                self._advertisement_tracker.async_remove_address(address)
-
     def _prefer_previous_adv_from_different_source(
-        self, old: BluetoothServiceInfoBleak, new: BluetoothServiceInfoBleak
+        self,
+        old: BluetoothServiceInfoBleak,
+        new: BluetoothServiceInfoBleak,
     ) -> bool:
         """Prefer previous advertisement from a different source if it is better."""
         if new.time - old.time > (
@@ -308,8 +312,8 @@ class BluetoothManager:
             # If the old advertisement is stale, any new advertisement is preferred
             _LOGGER.debug(
                 "%s (%s): Switching from %s[%s] to %s[%s] (time elapsed:%s > stale seconds:%s)",
-                new.advertisement.local_name,
-                new.device.address,
+                new.name,
+                new.address,
                 old.source,
                 old.connectable,
                 new.source,
@@ -318,19 +322,21 @@ class BluetoothManager:
                 stale_seconds,
             )
             return False
-        if new.device.rssi - RSSI_SWITCH_THRESHOLD > (old.device.rssi or NO_RSSI_VALUE):
+        if (new.rssi or NO_RSSI_VALUE) - RSSI_SWITCH_THRESHOLD > (
+            old.rssi or NO_RSSI_VALUE
+        ):
             # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred
             _LOGGER.debug(
                 "%s (%s): Switching from %s[%s] to %s[%s] (new rssi:%s - threshold:%s > old rssi:%s)",
-                new.advertisement.local_name,
-                new.device.address,
+                new.name,
+                new.address,
                 old.source,
                 old.connectable,
                 new.source,
                 new.connectable,
-                new.device.rssi,
+                new.rssi,
                 RSSI_SWITCH_THRESHOLD,
-                old.device.rssi,
+                old.rssi,
             )
             return False
         return True
@@ -355,9 +361,9 @@ class BluetoothManager:
             return
 
         device = service_info.device
-        connectable = service_info.connectable
         address = device.address
-        all_history = self._connectable_history if connectable else self._history
+        all_history = self._all_history
+
         source = service_info.source
         if (
             (old_service_info := all_history.get(address))
@@ -368,11 +374,11 @@ class BluetoothManager:
         ):
             return
 
-        self._history[address] = service_info
-
-        if connectable:
+        if connectable := service_info.connectable:
             self._connectable_history[address] = service_info
-            # Bleak callbacks must get a connectable device
+        else:
+            self._non_connectable_history[address] = service_info
+        all_history[address] = service_info
 
         # Track advertisement intervals to determine when we need to
         # switch adapters or mark a device as unavailable
@@ -393,11 +399,18 @@ class BluetoothManager:
         ):
             return
 
-        if connectable:
+        if is_connectable_by_any_source := address in self._connectable_history:
             # Bleak callbacks must get a connectable device
             for callback_filters in self._bleak_callbacks:
                 _dispatch_bleak_callback(*callback_filters, device, advertisement_data)
 
+        if not connectable and is_connectable_by_any_source:
+            # Since we have a connectable path and our BleakClient will
+            # route any connection attempts to the connectable path, we
+            # mark the service_info as connectable so that the callbacks
+            # will be called and the device can be discovered.
+            service_info = replace(service_info, connectable=True)
+
         matched_domains = self._integration_matcher.match_domains(service_info)
         _LOGGER.debug(
             "%s: %s %s connectable: %s match: %s rssi: %s",
@@ -406,7 +419,7 @@ class BluetoothManager:
             advertisement_data,
             connectable,
             matched_domains,
-            device.rssi,
+            advertisement_data.rssi,
         )
 
         for match in self._callback_index.match_callbacks(service_info):
@@ -518,27 +531,23 @@ class BluetoothManager:
 
     def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]:
         """Return the scanners by type."""
-        return (
-            self._connectable_scanners
-            if connectable
-            else self._non_connectable_scanners
-        )
+        if connectable:
+            return self._connectable_scanners
+        return self._non_connectable_scanners
 
     def _get_unavailable_callbacks_by_type(
         self, connectable: bool
     ) -> dict[str, list[Callable[[BluetoothServiceInfoBleak], None]]]:
         """Return the unavailable callbacks by type."""
-        return (
-            self._connectable_unavailable_callbacks
-            if connectable
-            else self._unavailable_callbacks
-        )
+        if connectable:
+            return self._connectable_unavailable_callbacks
+        return self._unavailable_callbacks
 
     def _get_history_by_type(
         self, connectable: bool
     ) -> dict[str, BluetoothServiceInfoBleak]:
         """Return the history by type."""
-        return self._connectable_history if connectable else self._history
+        return self._connectable_history if connectable else self._all_history
 
     def async_register_scanner(
         self, scanner: BaseHaScanner, connectable: bool
diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json
index 4d237c7e915..1a5871f2e5d 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.18.1",
-    "bleak-retry-connector==2.1.3",
+    "bleak==0.19.0",
+    "bleak-retry-connector==2.2.0",
     "bluetooth-adapters==0.6.0",
     "bluetooth-auto-recovery==0.3.4",
     "dbus-fast==1.45.0"
diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py
index 852ce4e47d3..1256cd7697a 100644
--- a/homeassistant/components/bluetooth/models.py
+++ b/homeassistant/components/bluetooth/models.py
@@ -115,9 +115,12 @@ class BaseHaScanner:
     def discovered_devices(self) -> list[BLEDevice]:
         """Return a list of discovered devices."""
 
+    @property
     @abstractmethod
-    async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
-        """Get a device by address."""
+    def discovered_devices_and_advertisement_data(
+        self,
+    ) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
+        """Return a list of discovered devices and their advertisement data."""
 
     async def async_diagnostics(self) -> dict[str, Any]:
         """Return diagnostic information about the scanner."""
@@ -127,7 +130,6 @@ class BaseHaScanner:
                 {
                     "name": device.name,
                     "address": device.address,
-                    "rssi": device.rssi,
                 }
                 for device in self.discovered_devices
             ],
@@ -285,7 +287,7 @@ class HaBleakClientWrapper(BleakClient):
         """Connect to the specified GATT server."""
         if not self._backend:
             wrapped_backend = (
-                self._async_get_backend() or await self._async_get_fallback_backend()
+                self._async_get_backend() or self._async_get_fallback_backend()
             )
             self._backend = wrapped_backend.client(
                 await freshen_ble_device(wrapped_backend.device)
@@ -329,7 +331,8 @@ class HaBleakClientWrapper(BleakClient):
 
         return None
 
-    async def _async_get_fallback_backend(self) -> _HaWrappedBleakBackend:
+    @hass_callback
+    def _async_get_fallback_backend(self) -> _HaWrappedBleakBackend:
         """Get a fallback backend for the given address."""
         #
         # The preferred backend cannot currently connect the device
@@ -340,13 +343,20 @@ class HaBleakClientWrapper(BleakClient):
         #
         assert MANAGER is not None
         address = self.__address
-        devices = await MANAGER.async_get_devices_by_address(address, True)
-        for ble_device in sorted(
-            devices,
-            key=lambda ble_device: ble_device.rssi or NO_RSSI_VALUE,
+        device_advertisement_datas = (
+            MANAGER.async_get_discovered_devices_and_advertisement_data_by_address(
+                address, True
+            )
+        )
+        for device_advertisement_data in sorted(
+            device_advertisement_datas,
+            key=lambda device_advertisement_data: device_advertisement_data[1].rssi
+            or NO_RSSI_VALUE,
             reverse=True,
         ):
-            if backend := self._async_get_backend_for_ble_device(ble_device):
+            if backend := self._async_get_backend_for_ble_device(
+                device_advertisement_data[0]
+            ):
                 return backend
 
         raise BleakError(
diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py
index 87c6a48380b..642bf13f7cd 100644
--- a/homeassistant/components/bluetooth/scanner.py
+++ b/homeassistant/components/bluetooth/scanner.py
@@ -17,7 +17,6 @@ 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, AdvertisementDataCallback
-from bleak_retry_connector import get_device_by_adapter
 from dbus_fast import InvalidMessageError
 
 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
@@ -144,6 +143,13 @@ class HaScanner(BaseHaScanner):
         """Return a list of discovered devices."""
         return self.scanner.discovered_devices
 
+    @property
+    def discovered_devices_and_advertisement_data(
+        self,
+    ) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
+        """Return a list of discovered devices and advertisement data."""
+        return self.scanner.discovered_devices_and_advertisement_data
+
     @hass_callback
     def async_setup(self) -> None:
         """Set up the scanner."""
@@ -151,16 +157,6 @@ class HaScanner(BaseHaScanner):
             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":
-            return await get_device_by_adapter(address, self.adapter)
-        # We don't have a fast version of this for MacOS yet
-        return next(
-            (device for device in self.discovered_devices if device.address == address),
-            None,
-        )
-
     async def async_diagnostics(self) -> dict[str, Any]:
         """Return diagnostic information about the scanner."""
         base_diag = await super().async_diagnostics()
diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py
index 82a6bdfbece..284e605fdfa 100644
--- a/homeassistant/components/esphome/bluetooth/scanner.py
+++ b/homeassistant/components/esphome/bluetooth/scanner.py
@@ -37,7 +37,9 @@ class ESPHomeScanner(BaseHaScanner):
         """Initialize the scanner."""
         super().__init__(hass, scanner_id)
         self._new_info_callback = new_info_callback
-        self._discovered_devices: dict[str, BLEDevice] = {}
+        self._discovered_device_advertisement_datas: dict[
+            str, tuple[BLEDevice, AdvertisementData]
+        ] = {}
         self._discovered_device_timestamps: dict[str, float] = {}
         self._connector = connector
         self._connectable = connectable
@@ -61,17 +63,23 @@ class ESPHomeScanner(BaseHaScanner):
             if now - timestamp > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
         ]
         for address in expired:
-            del self._discovered_devices[address]
+            del self._discovered_device_advertisement_datas[address]
             del self._discovered_device_timestamps[address]
 
     @property
     def discovered_devices(self) -> list[BLEDevice]:
         """Return a list of discovered devices."""
-        return list(self._discovered_devices.values())
+        return [
+            device_advertisement_data[0]
+            for device_advertisement_data in self._discovered_device_advertisement_datas.values()
+        ]
 
-    async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
-        """Get a device by address."""
-        return self._discovered_devices.get(address)
+    @property
+    def discovered_devices_and_advertisement_data(
+        self,
+    ) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
+        """Return a list of discovered devices and advertisement data."""
+        return self._discovered_device_advertisement_datas
 
     @callback
     def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None:
@@ -79,32 +87,39 @@ class ESPHomeScanner(BaseHaScanner):
         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 prev_discovery := self._discovered_device_advertisement_datas.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
+            prev_device = prev_discovery[0]
+            if len(prev_device.name) > len(adv.name):
+                name = prev_device.name
 
-        advertisement_data = AdvertisementData(  # type: ignore[no-untyped-call]
+        advertisement_data = AdvertisementData(
             local_name=None if name == "" else name,
             manufacturer_data=adv.manufacturer_data,
             service_data=adv.service_data,
             service_uuids=adv.service_uuids,
+            rssi=adv.rssi,
+            tx_power=-127,
+            platform_data=(),
         )
         device = BLEDevice(  # type: ignore[no-untyped-call]
             address=address,
             name=name,
             details=self._details,
-            rssi=adv.rssi,
+            rssi=adv.rssi,  # deprecated, will be removed in newer bleak
+        )
+        self._discovered_device_advertisement_datas[address] = (
+            device,
+            advertisement_data,
         )
-        self._discovered_devices[address] = device
         self._discovered_device_timestamps[address] = now
         self._new_info_callback(
             BluetoothServiceInfoBleak(
                 name=advertisement_data.local_name or device.name or device.address,
                 address=device.address,
-                rssi=device.rssi,
+                rssi=adv.rssi,
                 manufacturer_data=advertisement_data.manufacturer_data,
                 service_data=advertisement_data.service_data,
                 service_uuids=advertisement_data.service_uuids,
diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py
index e9f928dd571..da9305a2ae5 100644
--- a/homeassistant/components/homekit_controller/sensor.py
+++ b/homeassistant/components/homekit_controller/sensor.py
@@ -10,7 +10,10 @@ from aiohomekit.model.characteristics import Characteristic, CharacteristicsType
 from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus
 from aiohomekit.model.services import Service, ServicesTypes
 
-from homeassistant.components.bluetooth import async_ble_device_from_address
+from homeassistant.components.bluetooth import (
+    async_ble_device_from_address,
+    async_last_service_info,
+)
 from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
@@ -571,8 +574,8 @@ class RSSISensor(HomeKitEntity, SensorEntity):
     def native_value(self) -> int | None:
         """Return the current rssi value."""
         address = self._accessory.pairing_data["AccessoryAddress"]
-        ble_device = async_ble_device_from_address(self.hass, address)
-        return ble_device.rssi if ble_device else None
+        last_service_info = async_last_service_info(self.hass, address)
+        return last_service_info.rssi if last_service_info else None
 
 
 async def async_setup_entry(
diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py
index ee93c74af37..7f4f3c77b99 100644
--- a/homeassistant/components/switchbot/coordinator.py
+++ b/homeassistant/components/switchbot/coordinator.py
@@ -60,6 +60,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
         self.device_name = device_name
         self.base_unique_id = base_unique_id
         self.model = model
+        self.service_info: bluetooth.BluetoothServiceInfoBleak | None = None
         self._ready_event = asyncio.Event()
 
     @callback
@@ -70,6 +71,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
     ) -> None:
         """Handle a Bluetooth event."""
         self.ble_device = service_info.device
+        self.service_info = service_info
         if adv := switchbot.parse_advertisement_data(
             service_info.device, service_info.advertisement
         ):
diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py
index 1077fd4fce6..9dd0ef3900b 100644
--- a/homeassistant/components/switchbot/sensor.py
+++ b/homeassistant/components/switchbot/sensor.py
@@ -117,4 +117,5 @@ class SwitchbotRSSISensor(SwitchBotSensor):
     @property
     def native_value(self) -> str | int:
         """Return the state of the sensor."""
-        return self.coordinator.ble_device.rssi
+        assert self.coordinator.service_info is not None
+        return self.coordinator.service_info.rssi
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index fc6840d0b8b..97082d29719 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.1.3
-bleak==0.18.1
+bleak-retry-connector==2.2.0
+bleak==0.19.0
 bluetooth-adapters==0.6.0
 bluetooth-auto-recovery==0.3.4
 certifi>=2021.5.30
@@ -20,7 +20,7 @@ cryptography==38.0.1
 dbus-fast==1.45.0
 fnvhash==0.1.0
 hass-nabucasa==0.56.0
-home-assistant-bluetooth==1.3.0
+home-assistant-bluetooth==1.6.0
 home-assistant-frontend==20221010.0
 httpx==0.23.0
 ifaddr==0.1.7
diff --git a/pyproject.toml b/pyproject.toml
index b5488631eac..09f326bbce5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,7 +36,7 @@ dependencies    = [
     # When bumping httpx, please check the version pins of
     # httpcore, anyio, and h11 in gen_requirements_all
     "httpx==0.23.0",
-    "home-assistant-bluetooth==1.3.0",
+    "home-assistant-bluetooth==1.6.0",
     "ifaddr==0.1.7",
     "jinja2==3.1.2",
     "lru-dict==1.1.8",
diff --git a/requirements.txt b/requirements.txt
index 0dfc353823a..623464874c1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,7 +11,7 @@ bcrypt==3.1.7
 certifi>=2021.5.30
 ciso8601==2.2.0
 httpx==0.23.0
-home-assistant-bluetooth==1.3.0
+home-assistant-bluetooth==1.6.0
 ifaddr==0.1.7
 jinja2==3.1.2
 lru-dict==1.1.8
diff --git a/requirements_all.txt b/requirements_all.txt
index af46d0643d3..74dc226c22e 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.1.3
+bleak-retry-connector==2.2.0
 
 # homeassistant.components.bluetooth
-bleak==0.18.1
+bleak==0.19.0
 
 # homeassistant.components.blebox
 blebox_uniapi==2.1.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 7ecdb7bd6aa..9b4bb8659e3 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.1.3
+bleak-retry-connector==2.2.0
 
 # homeassistant.components.bluetooth
-bleak==0.18.1
+bleak==0.19.0
 
 # homeassistant.components.blebox
 blebox_uniapi==2.1.0
diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py
index d480f44b27e..7f8df35f263 100644
--- a/tests/components/airthings_ble/__init__.py
+++ b/tests/components/airthings_ble/__init__.py
@@ -5,10 +5,11 @@ 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
 
+from tests.components.bluetooth import generate_advertisement_data
+
 
 def patch_async_setup_entry(return_value=True):
     """Patch async setup entry to return True."""
@@ -48,7 +49,7 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak(
         "cc:cc:cc:cc:cc:cc",
         "cc-cc-cc-cc-cc-cc",
     ),
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
         service_uuids=["b42e1c08-ade7-11e4-89d3-123b93f75cba"],
     ),
@@ -68,7 +69,7 @@ UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak(
         "cc:cc:cc:cc:cc:cc",
         "unknown",
     ),
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         manufacturer_data={},
         service_uuids=[],
     ),
diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py
index a836740bb9b..e695f18c42f 100644
--- a/tests/components/bluetooth/__init__.py
+++ b/tests/components/bluetooth/__init__.py
@@ -2,6 +2,7 @@
 
 
 import time
+from typing import Any
 from unittest.mock import patch
 
 from bleak.backends.scanner import AdvertisementData, BLEDevice
@@ -27,8 +28,27 @@ __all__ = (
     "inject_bluetooth_service_info",
     "patch_all_discovered_devices",
     "patch_discovered_devices",
+    "generate_advertisement_data",
 )
 
+ADVERTISEMENT_DATA_DEFAULTS = {
+    "local_name": "",
+    "manufacturer_data": {},
+    "service_data": {},
+    "service_uuids": [],
+    "rssi": -127,
+    "platform_data": ((),),
+    "tx_power": -127,
+}
+
+
+def generate_advertisement_data(**kwargs: Any) -> AdvertisementData:
+    """Generate advertisement data with defaults."""
+    new = kwargs.copy()
+    for key, value in ADVERTISEMENT_DATA_DEFAULTS.items():
+        new.setdefault(key, value)
+    return AdvertisementData(**new)
+
 
 def _get_manager() -> BluetoothManager:
     """Return the bluetooth manager."""
@@ -77,7 +97,7 @@ def inject_advertisement_with_time_and_source_connectable(
         models.BluetoothServiceInfoBleak(
             name=adv.local_name or device.name or device.address,
             address=device.address,
-            rssi=device.rssi,
+            rssi=adv.rssi,
             manufacturer_data=adv.manufacturer_data,
             service_data=adv.service_data,
             service_uuids=adv.service_uuids,
@@ -94,17 +114,17 @@ def inject_bluetooth_service_info_bleak(
     hass: HomeAssistant, info: models.BluetoothServiceInfoBleak
 ) -> None:
     """Inject an advertisement into the manager with connectable status."""
-    advertisement_data = AdvertisementData(  # type: ignore[no-untyped-call]
+    advertisement_data = generate_advertisement_data(
         local_name=None if info.name == "" else info.name,
         manufacturer_data=info.manufacturer_data,
         service_data=info.service_data,
         service_uuids=info.service_uuids,
+        rssi=info.rssi,
     )
     device = BLEDevice(  # type: ignore[no-untyped-call]
         address=info.address,
         name=info.name,
         details={},
-        rssi=info.rssi,
     )
     inject_advertisement_with_time_and_source_connectable(
         hass,
@@ -120,17 +140,17 @@ def inject_bluetooth_service_info(
     hass: HomeAssistant, info: models.BluetoothServiceInfo
 ) -> None:
     """Inject a BluetoothServiceInfo into the manager."""
-    advertisement_data = AdvertisementData(  # type: ignore[no-untyped-call]
+    advertisement_data = generate_advertisement_data(  # type: ignore[no-untyped-call]
         local_name=None if info.name == "" else info.name,
         manufacturer_data=info.manufacturer_data,
         service_data=info.service_data,
         service_uuids=info.service_uuids,
+        rssi=info.rssi,
     )
     device = BLEDevice(  # type: ignore[no-untyped-call]
         address=info.address,
         name=info.name,
         details={},
-        rssi=info.rssi,
     )
     inject_advertisement(hass, device, advertisement_data)
 
@@ -138,7 +158,9 @@ def inject_bluetooth_service_info(
 def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None:
     """Mock all the discovered devices from all the scanners."""
     return patch.object(
-        _get_manager(), "async_all_discovered_devices", return_value=mock_discovered
+        _get_manager(),
+        "_async_all_discovered_addresses",
+        return_value={ble_device.address for ble_device in mock_discovered},
     )
 
 
diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py
index 6e9671cfe4e..6eb2b5a968e 100644
--- a/tests/components/bluetooth/test_advertisement_tracker.py
+++ b/tests/components/bluetooth/test_advertisement_tracker.py
@@ -21,7 +21,11 @@ from homeassistant.components.bluetooth.models import BaseHaScanner
 from homeassistant.core import callback
 from homeassistant.util import dt as dt_util
 
-from . import inject_advertisement_with_time_and_source
+from . import (
+    generate_advertisement_data,
+    inject_advertisement_with_time_and_source,
+    inject_advertisement_with_time_and_source_connectable,
+)
 
 from tests.common import async_fire_time_changed
 
@@ -33,8 +37,8 @@ async def test_advertisment_interval_shorter_than_adapter_stack_timeout(
 ):
     """Test we can determine the advertisement interval."""
     start_monotonic_time = time.monotonic()
-    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
+    switchbot_device = BLEDevice("44:44:33:11:23:12", "wohand")
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
     )
     switchbot_device_went_unavailable = False
@@ -77,8 +81,8 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_connectab
 ):
     """Test device with a long advertisement interval."""
     start_monotonic_time = time.monotonic()
-    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
+    switchbot_device = BLEDevice("44:44:33:11:23:18", "wohand")
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
     )
     switchbot_device_went_unavailable = False
@@ -124,7 +128,7 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c
     """Test device with a long advertisement interval with an adapter change."""
     start_monotonic_time = time.monotonic()
     switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
     )
     switchbot_device_went_unavailable = False
@@ -179,7 +183,7 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_not_conne
     """Test device with a long advertisement interval that is not connectable not reaching the advertising interval."""
     start_monotonic_time = time.monotonic()
     switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
     )
     switchbot_device_went_unavailable = False
@@ -227,9 +231,11 @@ async def test_advertisment_interval_shorter_than_adapter_stack_timeout_adapter_
 ):
     """Test device with a short advertisement interval with an adapter change that is not connectable."""
     start_monotonic_time = time.monotonic()
-    switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
-        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    switchbot_device = BLEDevice("44:44:33:11:23:5C", "wohand")
+    switchbot_adv = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+        rssi=-100,
     )
     switchbot_device_went_unavailable = False
 
@@ -248,9 +254,18 @@ async def test_advertisment_interval_shorter_than_adapter_stack_timeout_adapter_
             "original",
         )
 
+    switchbot_adv_better_rssi = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+        rssi=-30,
+    )
     for i in range(ADVERTISING_TIMES_NEEDED):
         inject_advertisement_with_time_and_source(
-            hass, switchbot_device, switchbot_adv, start_monotonic_time + (i * 2), "new"
+            hass,
+            switchbot_device,
+            switchbot_adv_better_rssi,
+            start_monotonic_time + (i * 2),
+            "new",
         )
 
     switchbot_device_unavailable_cancel = async_track_unavailable(
@@ -282,8 +297,10 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c
     """Test device with a long advertisement interval with an adapter change that is not connectable."""
     start_monotonic_time = time.monotonic()
     switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
-        local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
+    switchbot_adv = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+        rssi=-100,
     )
     switchbot_device_went_unavailable = False
 
@@ -291,8 +308,11 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c
         """Fake scanner."""
 
         @property
-        def discovered_devices(self) -> list[BLEDevice]:
-            return []
+        def discovered_devices_and_advertisement_data(
+            self,
+        ) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
+            """Return a list of discovered devices."""
+            return {}
 
     scanner = FakeScanner(hass, "new")
     cancel_scanner = async_register_scanner(hass, scanner, False)
@@ -304,21 +324,28 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c
         switchbot_device_went_unavailable = True
 
     for i in range(ADVERTISING_TIMES_NEEDED):
-        inject_advertisement_with_time_and_source(
+        inject_advertisement_with_time_and_source_connectable(
             hass,
             switchbot_device,
             switchbot_adv,
             start_monotonic_time + (i * 2),
             "original",
+            connectable=False,
         )
 
+    switchbot_better_rssi_adv = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+        rssi=-30,
+    )
     for i in range(ADVERTISING_TIMES_NEEDED):
-        inject_advertisement_with_time_and_source(
+        inject_advertisement_with_time_and_source_connectable(
             hass,
             switchbot_device,
-            switchbot_adv,
+            switchbot_better_rssi_adv,
             start_monotonic_time + (i * ONE_HOUR_SECONDS),
             "new",
+            connectable=False,
         )
 
     switchbot_device_unavailable_cancel = async_track_unavailable(
@@ -364,7 +391,7 @@ async def test_advertisment_interval_longer_increasing_than_adapter_stack_timeou
     """Test device with a increasing advertisement interval with an adapter change that is not connectable."""
     start_monotonic_time = time.monotonic()
     switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
     )
     switchbot_device_went_unavailable = False
diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py
index 7e2f15a984f..b053a439e00 100644
--- a/tests/components/bluetooth/test_diagnostics.py
+++ b/tests/components/bluetooth/test_diagnostics.py
@@ -3,12 +3,12 @@
 
 from unittest.mock import ANY, patch
 
-from bleak.backends.scanner import AdvertisementData, BLEDevice
+from bleak.backends.scanner import BLEDevice
 
 from homeassistant.components import bluetooth
 from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS
 
-from . import inject_advertisement
+from . import generate_advertisement_data, inject_advertisement
 
 from tests.common import MockConfigEntry
 from tests.components.diagnostics import get_diagnostics_for_config_entry
@@ -96,11 +96,6 @@ async def test_diagnostics(
                 }
             },
             "manager": {
-                "advertisement_tracker": {
-                    "intervals": {},
-                    "sources": {},
-                    "timings": {},
-                },
                 "adapters": {
                     "hci0": {
                         "address": "00:00:00:00:00:01",
@@ -115,13 +110,18 @@ async def test_diagnostics(
                         "sw_version": "BlueZ 4.63",
                     },
                 },
+                "advertisement_tracker": {
+                    "intervals": {},
+                    "sources": {},
+                    "timings": {},
+                },
                 "connectable_history": [],
-                "history": [],
+                "non_connectable_history": [],
                 "scanners": [
                     {
                         "adapter": "hci0",
                         "discovered_devices": [
-                            {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60}
+                            {"address": "44:44:33:11:23:45", "name": "x"}
                         ],
                         "last_detection": ANY,
                         "name": "hci0 (00:00:00:00:00:01)",
@@ -132,7 +132,7 @@ async def test_diagnostics(
                     {
                         "adapter": "hci0",
                         "discovered_devices": [
-                            {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60}
+                            {"address": "44:44:33:11:23:45", "name": "x"}
                         ],
                         "last_detection": ANY,
                         "name": "hci0 (00:00:00:00:00:01)",
@@ -143,7 +143,7 @@ async def test_diagnostics(
                     {
                         "adapter": "hci1",
                         "discovered_devices": [
-                            {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60}
+                            {"address": "44:44:33:11:23:45", "name": "x"}
                         ],
                         "last_detection": ANY,
                         "name": "hci1 (00:00:00:00:00:02)",
@@ -166,7 +166,7 @@ async def test_diagnostics_macos(
     # 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(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
     )
 
@@ -203,11 +203,6 @@ async def test_diagnostics_macos(
                 }
             },
             "manager": {
-                "advertisement_tracker": {
-                    "intervals": {},
-                    "sources": {"44:44:33:11:23:45": "local"},
-                    "timings": {"44:44:33:11:23:45": [ANY]},
-                },
                 "adapters": {
                     "Core Bluetooth": {
                         "address": "00:00:00:00:00:00",
@@ -215,39 +210,41 @@ async def test_diagnostics_macos(
                         "sw_version": ANY,
                     }
                 },
+                "advertisement_tracker": {
+                    "intervals": {},
+                    "sources": {"44:44:33:11:23:45": "local"},
+                    "timings": {"44:44:33:11:23:45": [ANY]},
+                },
                 "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,
+                        "advertisement": [
+                            "wohand",
+                            {"1": {"__type": "<class " "'bytes'>", "repr": "b'\\x01'"}},
+                            {},
+                            [],
+                            -127,
+                            -127,
+                            [[]],
+                        ],
                         "connectable": True,
-                        "manufacturer_data": ANY,
+                        "manufacturer_data": {
+                            "1": {"__type": "<class " "'bytes'>", "repr": "b'\\x01'"}
+                        },
                         "name": "wohand",
-                        "rssi": 0,
+                        "rssi": -127,
                         "service_data": {},
                         "service_uuids": [],
                         "source": "local",
                         "time": ANY,
                     }
                 ],
+                "non_connectable_history": [],
                 "scanners": [
                     {
                         "adapter": "Core Bluetooth",
                         "discovered_devices": [
-                            {"address": "44:44:33:11:23:45", "name": "x", "rssi": -60}
+                            {"address": "44:44:33:11:23:45", "name": "x"}
                         ],
                         "last_detection": ANY,
                         "name": "Core Bluetooth",
diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py
index 746f3004c30..495877118e7 100644
--- a/tests/components/bluetooth/test_init.py
+++ b/tests/components/bluetooth/test_init.py
@@ -44,6 +44,7 @@ from homeassistant.util import dt as dt_util
 from . import (
     _get_manager,
     async_setup_with_default_adapter,
+    generate_advertisement_data,
     inject_advertisement,
     inject_advertisement_with_time_and_source_connectable,
     patch_discovered_devices,
@@ -334,7 +335,9 @@ async def test_discovery_match_by_service_uuid(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
-        wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
+        wrong_adv = generate_advertisement_data(
+            local_name="wrong_name", service_uuids=[]
+        )
 
         inject_advertisement(hass, wrong_device, wrong_adv)
         await hass.async_block_till_done()
@@ -342,7 +345,7 @@ async def test_discovery_match_by_service_uuid(
         assert len(mock_config_flow.mock_calls) == 0
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
         )
 
@@ -379,7 +382,9 @@ async def test_discovery_match_by_service_uuid_connectable(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
-        wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
+        wrong_adv = generate_advertisement_data(
+            local_name="wrong_name", service_uuids=[]
+        )
 
         inject_advertisement_with_time_and_source_connectable(
             hass, wrong_device, wrong_adv, time.monotonic(), "any", True
@@ -389,7 +394,7 @@ async def test_discovery_match_by_service_uuid_connectable(
         assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
         )
 
@@ -424,7 +429,9 @@ async def test_discovery_match_by_service_uuid_not_connectable(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
-        wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
+        wrong_adv = generate_advertisement_data(
+            local_name="wrong_name", service_uuids=[]
+        )
 
         inject_advertisement_with_time_and_source_connectable(
             hass, wrong_device, wrong_adv, time.monotonic(), "any", False
@@ -434,7 +441,7 @@ async def test_discovery_match_by_service_uuid_not_connectable(
         assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
         )
 
@@ -467,7 +474,9 @@ async def test_discovery_match_by_name_connectable_false(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
-        wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
+        wrong_adv = generate_advertisement_data(
+            local_name="wrong_name", service_uuids=[]
+        )
 
         inject_advertisement_with_time_and_source_connectable(
             hass, wrong_device, wrong_adv, time.monotonic(), "any", False
@@ -477,7 +486,7 @@ async def test_discovery_match_by_name_connectable_false(
         assert len(_domains_from_mock_config_flow(mock_config_flow)) == 0
 
         qingping_device = BLEDevice("44:44:33:11:23:45", "Qingping Motion & Light")
-        qingping_adv = AdvertisementData(
+        qingping_adv = generate_advertisement_data(
             local_name="Qingping Motion & Light",
             service_data={
                 "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x01{"
@@ -493,8 +502,20 @@ async def test_discovery_match_by_name_connectable_false(
 
         mock_config_flow.reset_mock()
         # Make sure it will also take a connectable device
+        qingping_adv_with_better_rssi = generate_advertisement_data(
+            local_name="Qingping Motion & Light",
+            service_data={
+                "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x02{"
+            },
+            rssi=-30,
+        )
         inject_advertisement_with_time_and_source_connectable(
-            hass, qingping_device, qingping_adv, time.monotonic(), "any", True
+            hass,
+            qingping_device,
+            qingping_adv_with_better_rssi,
+            time.monotonic(),
+            "any",
+            True,
         )
         await hass.async_block_till_done()
         assert _domains_from_mock_config_flow(mock_config_flow) == ["qingping"]
@@ -517,7 +538,9 @@ async def test_discovery_match_by_local_name(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name")
-        wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
+        wrong_adv = generate_advertisement_data(
+            local_name="wrong_name", service_uuids=[]
+        )
 
         inject_advertisement(hass, wrong_device, wrong_adv)
         await hass.async_block_till_done()
@@ -525,7 +548,7 @@ async def test_discovery_match_by_local_name(
         assert len(mock_config_flow.mock_calls) == 0
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
         )
 
@@ -559,12 +582,12 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         hkc_device = BLEDevice("44:44:33:11:23:45", "lock")
-        hkc_adv_no_mfr_data = AdvertisementData(
+        hkc_adv_no_mfr_data = generate_advertisement_data(
             local_name="lock",
             service_uuids=[],
             manufacturer_data={},
         )
-        hkc_adv = AdvertisementData(
+        hkc_adv = generate_advertisement_data(
             local_name="lock",
             service_uuids=[],
             manufacturer_data={76: b"\x06\x02\x03\x99"},
@@ -593,7 +616,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start(
 
         mock_config_flow.reset_mock()
         not_hkc_device = BLEDevice("44:44:33:11:23:21", "lock")
-        not_hkc_adv = AdvertisementData(
+        not_hkc_adv = generate_advertisement_data(
             local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"}
         )
 
@@ -602,7 +625,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start(
 
         assert len(mock_config_flow.mock_calls) == 0
         not_apple_device = BLEDevice("44:44:33:11:23:23", "lock")
-        not_apple_adv = AdvertisementData(
+        not_apple_adv = generate_advertisement_data(
             local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"}
         )
 
@@ -642,36 +665,38 @@ async def test_discovery_match_by_service_data_uuid_then_others(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         device = BLEDevice("44:44:33:11:23:45", "lock")
-        adv_without_service_data_uuid = AdvertisementData(
+        adv_without_service_data_uuid = generate_advertisement_data(
             local_name="lock",
             service_uuids=[],
             manufacturer_data={},
         )
-        adv_with_mfr_data = AdvertisementData(
+        adv_with_mfr_data = generate_advertisement_data(
             local_name="lock",
             service_uuids=[],
             manufacturer_data={323: b"\x01\x02\x03"},
             service_data={},
         )
-        adv_with_service_data_uuid = AdvertisementData(
+        adv_with_service_data_uuid = generate_advertisement_data(
             local_name="lock",
             service_uuids=[],
             manufacturer_data={},
             service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
         )
-        adv_with_service_data_uuid_and_mfr_data = AdvertisementData(
+        adv_with_service_data_uuid_and_mfr_data = generate_advertisement_data(
             local_name="lock",
             service_uuids=[],
             manufacturer_data={323: b"\x01\x02\x03"},
             service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
         )
-        adv_with_service_data_uuid_and_mfr_data_and_service_uuid = AdvertisementData(
-            local_name="lock",
-            manufacturer_data={323: b"\x01\x02\x03"},
-            service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
-            service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"],
+        adv_with_service_data_uuid_and_mfr_data_and_service_uuid = (
+            generate_advertisement_data(
+                local_name="lock",
+                manufacturer_data={323: b"\x01\x02\x03"},
+                service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"},
+                service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"],
+            )
         )
-        adv_with_service_uuid = AdvertisementData(
+        adv_with_service_uuid = generate_advertisement_data(
             local_name="lock",
             manufacturer_data={},
             service_data={},
@@ -790,18 +815,18 @@ async def test_discovery_match_by_service_data_uuid_when_format_changes(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         device = BLEDevice("44:44:33:11:23:45", "lock")
-        adv_without_service_data_uuid = AdvertisementData(
+        adv_without_service_data_uuid = generate_advertisement_data(
             local_name="Qingping Temp RH M",
             service_uuids=[],
             manufacturer_data={},
         )
-        xiaomi_format_adv = AdvertisementData(
+        xiaomi_format_adv = generate_advertisement_data(
             local_name="Qingping Temp RH M",
             service_data={
                 "0000fe95-0000-1000-8000-00805f9b34fb": b"0XH\x0b\x06\xa7%\x144-X\x08"
             },
         )
-        qingping_format_adv = AdvertisementData(
+        qingping_format_adv = generate_advertisement_data(
             local_name="Qingping Temp RH M",
             service_data={
                 "0000fdcd-0000-1000-8000-00805f9b34fb": b"\x08\x16\xa7%\x144-X\x01\x04\xdb\x00\xa6\x01\x02\x01d"
@@ -871,12 +896,12 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         device = BLEDevice("44:44:33:11:23:45", "lock")
-        adv_service_uuids = AdvertisementData(
+        adv_service_uuids = generate_advertisement_data(
             local_name="lock",
             service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fc"],
             manufacturer_data={},
         )
-        adv_manufacturer_data = AdvertisementData(
+        adv_manufacturer_data = generate_advertisement_data(
             local_name="lock",
             service_uuids=[],
             manufacturer_data={76: b"\x06\x02\x03\x99"},
@@ -924,10 +949,10 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth):
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]
         )
-        switchbot_adv_2 = AdvertisementData(
+        switchbot_adv_2 = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={1: b"\x01"},
@@ -958,8 +983,8 @@ async def test_async_discovered_device_api(
     with patch(
         "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
     ), patch(
-        "bleak.BleakScanner.discovered_devices",  # Must patch before we setup
-        [MagicMock(address="44:44:33:11:23:45")],
+        "bleak.BleakScanner.discovered_devices_and_advertisement_data",  # Must patch before we setup
+        {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())},
     ):
         assert not bluetooth.async_discovered_service_info(hass)
         assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
@@ -974,10 +999,14 @@ async def test_async_discovered_device_api(
             assert not bluetooth.async_discovered_service_info(hass)
 
             wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name")
-            wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[])
+            wrong_adv = generate_advertisement_data(
+                local_name="wrong_name", service_uuids=[]
+            )
             inject_advertisement(hass, wrong_device, wrong_adv)
             switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-            switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
+            switchbot_adv = generate_advertisement_data(
+                local_name="wohand", service_uuids=[]
+            )
             inject_advertisement(hass, switchbot_device, switchbot_adv)
             wrong_device_went_unavailable = False
             switchbot_device_went_unavailable = False
@@ -1070,7 +1099,7 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
@@ -1080,13 +1109,13 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo
         inject_advertisement(hass, switchbot_device, switchbot_adv)
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
@@ -1138,7 +1167,7 @@ async def test_register_callbacks_raises_exception(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
@@ -1197,7 +1226,7 @@ async def test_register_callback_by_address(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
@@ -1207,13 +1236,13 @@ async def test_register_callback_by_address(
         inject_advertisement(hass, switchbot_device, switchbot_adv)
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         # 3rd callback raises ValueError but is still tracked
         inject_advertisement(hass, empty_device, empty_adv)
@@ -1299,18 +1328,29 @@ async def test_register_callback_by_address_connectable_only(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
-
+        switchbot_adv_better_rssi = generate_advertisement_data(
+            local_name="wohand",
+            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+            manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
+            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
+            rssi=-30,
+        )
         inject_advertisement_with_time_and_source_connectable(
             hass, switchbot_device, switchbot_adv, time.monotonic(), "test", False
         )
         inject_advertisement_with_time_and_source_connectable(
-            hass, switchbot_device, switchbot_adv, time.monotonic(), "test", True
+            hass,
+            switchbot_device,
+            switchbot_adv_better_rssi,
+            time.monotonic(),
+            "test",
+            True,
         )
 
         cancel()
@@ -1354,7 +1394,7 @@ async def test_register_callback_by_manufacturer_id(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         apple_device = BLEDevice("44:44:33:11:23:45", "rtx")
-        apple_adv = AdvertisementData(
+        apple_adv = generate_advertisement_data(
             local_name="rtx",
             manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1362,7 +1402,7 @@ async def test_register_callback_by_manufacturer_id(
         inject_advertisement(hass, apple_device, apple_adv)
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
@@ -1409,7 +1449,7 @@ async def test_register_callback_by_connectable(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         apple_device = BLEDevice("44:44:33:11:23:45", "rtx")
-        apple_adv = AdvertisementData(
+        apple_adv = generate_advertisement_data(
             local_name="rtx",
             manufacturer_data={7676: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1417,7 +1457,7 @@ async def test_register_callback_by_connectable(
         inject_advertisement(hass, apple_device, apple_adv)
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
@@ -1464,7 +1504,7 @@ async def test_not_filtering_wanted_apple_devices(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         ibeacon_device = BLEDevice("44:44:33:11:23:45", "rtx")
-        ibeacon_adv = AdvertisementData(
+        ibeacon_adv = generate_advertisement_data(
             local_name="ibeacon",
             manufacturer_data={76: b"\x02\x00\x00\x00"},
         )
@@ -1472,7 +1512,7 @@ async def test_not_filtering_wanted_apple_devices(
         inject_advertisement(hass, ibeacon_device, ibeacon_adv)
 
         homekit_device = BLEDevice("44:44:33:11:23:46", "rtx")
-        homekit_adv = AdvertisementData(
+        homekit_adv = generate_advertisement_data(
             local_name="homekit",
             manufacturer_data={76: b"\x06\x00\x00\x00"},
         )
@@ -1480,7 +1520,7 @@ async def test_not_filtering_wanted_apple_devices(
         inject_advertisement(hass, homekit_device, homekit_adv)
 
         apple_device = BLEDevice("44:44:33:11:23:47", "rtx")
-        apple_adv = AdvertisementData(
+        apple_adv = generate_advertisement_data(
             local_name="apple",
             manufacturer_data={76: b"\x10\x00\x00\x00"},
         )
@@ -1524,7 +1564,7 @@ async def test_filtering_noisy_apple_devices(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         apple_device = BLEDevice("44:44:33:11:23:45", "rtx")
-        apple_adv = AdvertisementData(
+        apple_adv = generate_advertisement_data(
             local_name="noisy",
             manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1532,7 +1572,7 @@ async def test_filtering_noisy_apple_devices(
         inject_advertisement(hass, apple_device, apple_adv)
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
@@ -1574,7 +1614,7 @@ async def test_register_callback_by_address_connectable_manufacturer_id(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         apple_device = BLEDevice("44:44:33:11:23:45", "rtx")
-        apple_adv = AdvertisementData(
+        apple_adv = generate_advertisement_data(
             local_name="rtx",
             manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1628,7 +1668,7 @@ async def test_register_callback_by_manufacturer_id_and_address(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         rtx_device = BLEDevice("44:44:33:11:23:45", "rtx")
-        rtx_adv = AdvertisementData(
+        rtx_adv = generate_advertisement_data(
             local_name="rtx",
             manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1636,7 +1676,7 @@ async def test_register_callback_by_manufacturer_id_and_address(
         inject_advertisement(hass, rtx_device, rtx_adv)
 
         yale_device = BLEDevice("44:44:33:11:23:45", "apple")
-        yale_adv = AdvertisementData(
+        yale_adv = generate_advertisement_data(
             local_name="yale",
             manufacturer_data={465: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1645,7 +1685,7 @@ async def test_register_callback_by_manufacturer_id_and_address(
         await hass.async_block_till_done()
 
         other_apple_device = BLEDevice("44:44:33:11:23:22", "apple")
-        other_apple_adv = AdvertisementData(
+        other_apple_adv = generate_advertisement_data(
             local_name="apple",
             manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1696,7 +1736,7 @@ async def test_register_callback_by_service_uuid_and_address(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         switchbot_dev = BLEDevice("44:44:33:11:23:45", "switchbot")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="switchbot",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
         )
@@ -1704,7 +1744,7 @@ async def test_register_callback_by_service_uuid_and_address(
         inject_advertisement(hass, switchbot_dev, switchbot_adv)
 
         switchbot_missing_service_uuid_dev = BLEDevice("44:44:33:11:23:45", "switchbot")
-        switchbot_missing_service_uuid_adv = AdvertisementData(
+        switchbot_missing_service_uuid_adv = generate_advertisement_data(
             local_name="switchbot",
         )
 
@@ -1714,7 +1754,7 @@ async def test_register_callback_by_service_uuid_and_address(
         await hass.async_block_till_done()
 
         service_uuid_wrong_address_dev = BLEDevice("44:44:33:11:23:22", "switchbot2")
-        service_uuid_wrong_address_adv = AdvertisementData(
+        service_uuid_wrong_address_adv = generate_advertisement_data(
             local_name="switchbot2",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
         )
@@ -1765,7 +1805,7 @@ async def test_register_callback_by_service_data_uuid_and_address(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         switchbot_dev = BLEDevice("44:44:33:11:23:45", "switchbot")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="switchbot",
             service_data={"cba20d00-224d-11e6-9fb8-0002a5d5c51b": b"x"},
         )
@@ -1773,7 +1813,7 @@ async def test_register_callback_by_service_data_uuid_and_address(
         inject_advertisement(hass, switchbot_dev, switchbot_adv)
 
         switchbot_missing_service_uuid_dev = BLEDevice("44:44:33:11:23:45", "switchbot")
-        switchbot_missing_service_uuid_adv = AdvertisementData(
+        switchbot_missing_service_uuid_adv = generate_advertisement_data(
             local_name="switchbot",
         )
 
@@ -1783,7 +1823,7 @@ async def test_register_callback_by_service_data_uuid_and_address(
         await hass.async_block_till_done()
 
         service_uuid_wrong_address_dev = BLEDevice("44:44:33:11:23:22", "switchbot2")
-        service_uuid_wrong_address_adv = AdvertisementData(
+        service_uuid_wrong_address_adv = generate_advertisement_data(
             local_name="switchbot2",
             service_data={"cba20d00-224d-11e6-9fb8-0002a5d5c51b": b"x"},
         )
@@ -1831,7 +1871,7 @@ async def test_register_callback_by_local_name(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         rtx_device = BLEDevice("44:44:33:11:23:45", "rtx")
-        rtx_adv = AdvertisementData(
+        rtx_adv = generate_advertisement_data(
             local_name="rtx",
             manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1839,12 +1879,12 @@ async def test_register_callback_by_local_name(
         inject_advertisement(hass, rtx_device, rtx_adv)
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
 
         rtx_device_2 = BLEDevice("44:44:33:11:23:45", "rtx")
-        rtx_adv_2 = AdvertisementData(
+        rtx_adv_2 = generate_advertisement_data(
             local_name="rtx2",
             manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"},
         )
@@ -1927,7 +1967,7 @@ async def test_register_callback_by_service_data_uuid(
         assert len(mock_bleak_scanner_start.mock_calls) == 1
 
         apple_device = BLEDevice("44:44:33:11:23:45", "xiaomi")
-        apple_adv = AdvertisementData(
+        apple_adv = generate_advertisement_data(
             local_name="xiaomi",
             service_data={
                 "0000fe95-0000-1000-8000-00805f9b34fb": b"\xd8.\xad\xcd\r\x85"
@@ -1937,7 +1977,7 @@ async def test_register_callback_by_service_data_uuid(
         inject_advertisement(hass, apple_device, apple_adv)
 
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
@@ -1981,13 +2021,13 @@ async def test_register_callback_survives_reload(
     assert len(mock_bleak_scanner_start.mock_calls) == 1
 
     switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand",
         service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"],
         manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
         service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
     )
-    switchbot_adv_2 = AdvertisementData(
+    switchbot_adv_2 = generate_advertisement_data(
         local_name="wohand",
         service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"],
         manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
@@ -2035,7 +2075,7 @@ async def test_process_advertisements_bail_on_good_advertisement(
 
     while not done.done():
         device = BLEDevice("aa:44:33:11:23:45", "wohand")
-        adv = AdvertisementData(
+        adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
@@ -2060,13 +2100,13 @@ async def test_process_advertisements_ignore_bad_advertisement(
     return_value = asyncio.Event()
 
     device = BLEDevice("aa:44:33:11:23:45", "wohand")
-    adv = AdvertisementData(
+    adv = generate_advertisement_data(
         local_name="wohand",
         service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"],
         manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
         service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""},
     )
-    adv2 = AdvertisementData(
+    adv2 = generate_advertisement_data(
         local_name="wohand",
         service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"],
         manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
@@ -2142,20 +2182,20 @@ async def test_wrapped_instance_with_filter(
             detected.append((device, advertisement_data))
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
-        switchbot_adv_2 = AdvertisementData(
+        switchbot_adv_2 = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         assert _get_manager() is not None
         scanner = models.HaBleakScannerWrapper(
@@ -2214,20 +2254,20 @@ async def test_wrapped_instance_with_service_uuids(
             detected.append((device, advertisement_data))
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
-        switchbot_adv_2 = AdvertisementData(
+        switchbot_adv_2 = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         assert _get_manager() is not None
         scanner = models.HaBleakScannerWrapper(
@@ -2272,7 +2312,7 @@ async def test_wrapped_instance_with_broken_callbacks(
             detected.append((device, advertisement_data))
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
@@ -2313,20 +2353,20 @@ async def test_wrapped_instance_changes_uuids(
             detected.append((device, advertisement_data))
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
-        switchbot_adv_2 = AdvertisementData(
+        switchbot_adv_2 = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
         empty_device = BLEDevice("11:22:33:44:55:66", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         assert _get_manager() is not None
         scanner = models.HaBleakScannerWrapper()
@@ -2368,20 +2408,20 @@ async def test_wrapped_instance_changes_filters(
             detected.append((device, advertisement_data))
 
         switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand")
-        switchbot_adv = AdvertisementData(
+        switchbot_adv = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
-        switchbot_adv_2 = AdvertisementData(
+        switchbot_adv_2 = generate_advertisement_data(
             local_name="wohand",
             service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
             manufacturer_data={89: b"\xd8.\xad\xcd\r\x84"},
             service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
         )
         empty_device = BLEDevice("11:22:33:44:55:62", "empty")
-        empty_adv = AdvertisementData(local_name="empty")
+        empty_adv = generate_advertisement_data(local_name="empty")
 
         assert _get_manager() is not None
         scanner = models.HaBleakScannerWrapper()
@@ -2434,8 +2474,8 @@ async def test_async_ble_device_from_address(
     with patch(
         "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt
     ), patch(
-        "bleak.BleakScanner.discovered_devices",  # Must patch before we setup
-        [MagicMock(address="44:44:33:11:23:45")],
+        "bleak.BleakScanner.discovered_devices_and_advertisement_data",  # Must patch before we setup
+        {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())},
     ):
         assert not bluetooth.async_discovered_service_info(hass)
         assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
@@ -2453,7 +2493,9 @@ async def test_async_ble_device_from_address(
         assert not bluetooth.async_discovered_service_info(hass)
 
         switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-        switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[])
+        switchbot_adv = generate_advertisement_data(
+            local_name="wohand", service_uuids=[]
+        )
         inject_advertisement(hass, switchbot_device, switchbot_adv)
         await hass.async_block_till_done()
 
diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py
index 4e5ab24b80f..3f5fb56539c 100644
--- a/tests/components/bluetooth/test_manager.py
+++ b/tests/components/bluetooth/test_manager.py
@@ -1,8 +1,9 @@
 """Tests for the Bluetooth integration manager."""
 
+import time
 from unittest.mock import AsyncMock, MagicMock, patch
 
-from bleak.backends.scanner import AdvertisementData, BLEDevice
+from bleak.backends.scanner import BLEDevice
 from bluetooth_adapters import AdvertisementHistory
 
 from homeassistant.components import bluetooth
@@ -12,8 +13,10 @@ from homeassistant.components.bluetooth.manager import (
 from homeassistant.setup import async_setup_component
 
 from . import (
+    generate_advertisement_data,
     inject_advertisement_with_source,
     inject_advertisement_with_time_and_source,
+    inject_advertisement_with_time_and_source_connectable,
 )
 
 
@@ -25,7 +28,7 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason(
     address = "44:44:33:11:23:12"
 
     switchbot_device_signal_100 = BLEDevice(address, "wohand_signal_100", rssi=-100)
-    switchbot_adv_signal_100 = AdvertisementData(
+    switchbot_adv_signal_100 = generate_advertisement_data(
         local_name="wohand_signal_100", service_uuids=[]
     )
     inject_advertisement_with_source(
@@ -38,7 +41,7 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason(
     )
 
     switchbot_device_signal_99 = BLEDevice(address, "wohand_signal_99", rssi=-99)
-    switchbot_adv_signal_99 = AdvertisementData(
+    switchbot_adv_signal_99 = generate_advertisement_data(
         local_name="wohand_signal_99", service_uuids=[]
     )
     inject_advertisement_with_source(
@@ -51,7 +54,7 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason(
     )
 
     switchbot_device_signal_98 = BLEDevice(address, "wohand_good_signal", rssi=-98)
-    switchbot_adv_signal_98 = AdvertisementData(
+    switchbot_adv_signal_98 = generate_advertisement_data(
         local_name="wohand_good_signal", service_uuids=[]
     )
     inject_advertisement_with_source(
@@ -70,9 +73,9 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth):
 
     address = "44:44:33:11:23:45"
 
-    switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal", rssi=-100)
-    switchbot_adv_poor_signal = AdvertisementData(
-        local_name="wohand_poor_signal", service_uuids=[]
+    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"
@@ -83,9 +86,9 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth):
         is switchbot_device_poor_signal
     )
 
-    switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal", rssi=-60)
-    switchbot_adv_good_signal = AdvertisementData(
-        local_name="wohand_good_signal", service_uuids=[]
+    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, "hci1"
@@ -105,11 +108,9 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth):
     )
 
     # We should not switch adapters unless the signal hits the threshold
-    switchbot_device_similar_signal = BLEDevice(
-        address, "wohand_similar_signal", rssi=-62
-    )
-    switchbot_adv_similar_signal = AdvertisementData(
-        local_name="wohand_similar_signal", service_uuids=[]
+    switchbot_device_similar_signal = BLEDevice(address, "wohand_similar_signal")
+    switchbot_adv_similar_signal = generate_advertisement_data(
+        local_name="wohand_similar_signal", service_uuids=[], rssi=-62
     )
 
     inject_advertisement_with_source(
@@ -126,9 +127,9 @@ async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth):
 
     address = "44:44:33:11:23:45"
 
-    switchbot_device_no_rssi = BLEDevice(address, "wohand_poor_signal", rssi=0)
-    switchbot_adv_no_rssi = AdvertisementData(
-        local_name="wohand_no_rssi", service_uuids=[]
+    switchbot_device_no_rssi = BLEDevice(address, "wohand_poor_signal")
+    switchbot_adv_no_rssi = generate_advertisement_data(
+        local_name="wohand_no_rssi", service_uuids=[], rssi=0
     )
     inject_advertisement_with_source(
         hass, switchbot_device_no_rssi, switchbot_adv_no_rssi, "hci0"
@@ -139,9 +140,9 @@ async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth):
         is switchbot_device_no_rssi
     )
 
-    switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal", rssi=-60)
-    switchbot_adv_good_signal = AdvertisementData(
-        local_name="wohand_good_signal", service_uuids=[]
+    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, "hci1"
@@ -161,11 +162,9 @@ async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth):
     )
 
     # We should not switch adapters unless the signal hits the threshold
-    switchbot_device_similar_signal = BLEDevice(
-        address, "wohand_similar_signal", rssi=-62
-    )
-    switchbot_adv_similar_signal = AdvertisementData(
-        local_name="wohand_similar_signal", service_uuids=[]
+    switchbot_device_similar_signal = BLEDevice(address, "wohand_similar_signal")
+    switchbot_adv_similar_signal = generate_advertisement_data(
+        local_name="wohand_similar_signal", service_uuids=[], rssi=-62
     )
 
     inject_advertisement_with_source(
@@ -183,11 +182,9 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth):
     address = "44:44:33:11:23:41"
     start_time_monotonic = 50.0
 
-    switchbot_device_poor_signal_hci0 = BLEDevice(
-        address, "wohand_poor_signal_hci0", rssi=-100
-    )
-    switchbot_adv_poor_signal_hci0 = AdvertisementData(
-        local_name="wohand_poor_signal_hci0", service_uuids=[]
+    switchbot_device_poor_signal_hci0 = BLEDevice(address, "wohand_poor_signal_hci0")
+    switchbot_adv_poor_signal_hci0 = generate_advertisement_data(
+        local_name="wohand_poor_signal_hci0", service_uuids=[], rssi=-100
     )
     inject_advertisement_with_time_and_source(
         hass,
@@ -202,11 +199,9 @@ async def test_switching_adapters_based_on_stale(hass, enable_bluetooth):
         is switchbot_device_poor_signal_hci0
     )
 
-    switchbot_device_poor_signal_hci1 = BLEDevice(
-        address, "wohand_poor_signal_hci1", rssi=-99
-    )
-    switchbot_adv_poor_signal_hci1 = AdvertisementData(
-        local_name="wohand_poor_signal_hci1", service_uuids=[]
+    switchbot_device_poor_signal_hci1 = BLEDevice(address, "wohand_poor_signal_hci1")
+    switchbot_adv_poor_signal_hci1 = generate_advertisement_data(
+        local_name="wohand_poor_signal_hci1", service_uuids=[], rssi=-99
     )
     inject_advertisement_with_time_and_source(
         hass,
@@ -246,7 +241,7 @@ async def test_restore_history_from_dbus(hass, one_adapter):
     ble_device = BLEDevice(address, "name")
     history = {
         address: AdvertisementHistory(
-            ble_device, AdvertisementData(local_name="name"), "hci0"
+            ble_device, generate_advertisement_data(local_name="name"), "hci0"
         )
     }
 
@@ -258,3 +253,86 @@ async def test_restore_history_from_dbus(hass, one_adapter):
         await hass.async_block_till_done()
 
     assert bluetooth.async_ble_device_from_address(hass, address) is ble_device
+
+
+async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable(
+    hass, enable_bluetooth
+):
+    """Test switching adapters based on rssi from connectable to non connectable."""
+
+    address = "44:44:33:11:23:45"
+    now = time.monotonic()
+    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_time_and_source_connectable(
+        hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, now, "hci0", True
+    )
+
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, False)
+        is switchbot_device_poor_signal
+    )
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, True)
+        is switchbot_device_poor_signal
+    )
+    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_time_and_source_connectable(
+        hass,
+        switchbot_device_good_signal,
+        switchbot_adv_good_signal,
+        now,
+        "hci1",
+        False,
+    )
+
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, False)
+        is switchbot_device_good_signal
+    )
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, True)
+        is switchbot_device_poor_signal
+    )
+    inject_advertisement_with_time_and_source_connectable(
+        hass,
+        switchbot_device_good_signal,
+        switchbot_adv_poor_signal,
+        now,
+        "hci0",
+        False,
+    )
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, False)
+        is switchbot_device_good_signal
+    )
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, True)
+        is switchbot_device_poor_signal
+    )
+    switchbot_device_excellent_signal = BLEDevice(address, "wohand_excellent_signal")
+    switchbot_adv_excellent_signal = generate_advertisement_data(
+        local_name="wohand_excellent_signal", service_uuids=[], rssi=-25
+    )
+
+    inject_advertisement_with_time_and_source_connectable(
+        hass,
+        switchbot_device_excellent_signal,
+        switchbot_adv_excellent_signal,
+        now,
+        "hci2",
+        False,
+    )
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, False)
+        is switchbot_device_excellent_signal
+    )
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, True)
+        is switchbot_device_poor_signal
+    )
diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py
index e0e782dff84..adb953b2af2 100644
--- a/tests/components/bluetooth/test_models.py
+++ b/tests/components/bluetooth/test_models.py
@@ -16,7 +16,12 @@ from homeassistant.components.bluetooth.models import (
     HaBluetoothConnector,
 )
 
-from . import _get_manager, inject_advertisement, inject_advertisement_with_source
+from . import (
+    _get_manager,
+    generate_advertisement_data,
+    inject_advertisement,
+    inject_advertisement_with_source,
+)
 
 
 class MockBleakClient(BleakClient):
@@ -49,7 +54,7 @@ async def test_wrapped_bleak_scanner(hass, enable_bluetooth):
     """Test wrapped bleak scanner dispatches calls as expected."""
     scanner = HaBleakScannerWrapper()
     switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand")
-    switchbot_adv = AdvertisementData(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
     )
     inject_advertisement(hass, switchbot_device, switchbot_adv)
@@ -84,7 +89,7 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected(
     switchbot_device = BLEDevice(
         "44:44:33:11:23:45", "wohand", {"path": "/org/bluez/hci0/dev_44_44_33_11_23_45"}
     )
-    switchbot_adv = AdvertisementData(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
     )
     inject_advertisement(hass, switchbot_device, switchbot_adv)
@@ -116,7 +121,7 @@ async def test_ble_device_with_proxy_client_out_of_connections(
         },
         rssi=-30,
     )
-    switchbot_adv = AdvertisementData(
+    switchbot_adv = generate_advertisement_data(
         local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
     )
 
@@ -153,6 +158,11 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
             ),
             "path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
         },
+    )
+    switchbot_proxy_device_adv_no_connection_slot = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=[],
+        manufacturer_data={1: b"\x01"},
         rssi=-30,
     )
     switchbot_proxy_device_has_connection_slot = BLEDevice(
@@ -166,14 +176,19 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
         },
         rssi=-40,
     )
+    switchbot_proxy_device_adv_has_connection_slot = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=[],
+        manufacturer_data={1: b"\x01"},
+        rssi=-40,
+    )
     switchbot_device = BLEDevice(
         "44:44:33:11:23:45",
         "wohand",
         {"path": "/org/bluez/hci0/dev_44_44_33_11_23_45"},
-        rssi=-100,
     )
-    switchbot_adv = AdvertisementData(
-        local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
+    switchbot_adv = generate_advertisement_data(
+        local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}, rssi=-100
     )
 
     inject_advertisement_with_source(
@@ -182,21 +197,28 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
     inject_advertisement_with_source(
         hass,
         switchbot_proxy_device_has_connection_slot,
-        switchbot_adv,
+        switchbot_proxy_device_adv_has_connection_slot,
         "esp32_has_connection_slot",
     )
     inject_advertisement_with_source(
         hass,
         switchbot_proxy_device_no_connection_slot,
-        switchbot_adv,
+        switchbot_proxy_device_adv_no_connection_slot,
         "esp32_no_connection_slot",
     )
 
     class FakeScanner(BaseHaScanner):
         @property
-        def discovered_devices(self) -> list[BLEDevice]:
+        def discovered_devices_and_advertisement_data(
+            self,
+        ) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
             """Return a list of discovered devices."""
-            return [switchbot_proxy_device_has_connection_slot]
+            return {
+                switchbot_proxy_device_has_connection_slot.address: (
+                    switchbot_proxy_device_has_connection_slot,
+                    switchbot_proxy_device_adv_has_connection_slot,
+                )
+            }
 
         async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
             """Return a list of discovered devices."""
@@ -237,7 +259,12 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
         rssi=-30,
     )
     switchbot_proxy_device_no_connection_slot.metadata["delegate"] = 0
-
+    switchbot_proxy_device_no_connection_slot_adv = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=[],
+        manufacturer_data={1: b"\x01"},
+        rssi=-30,
+    )
     switchbot_proxy_device_has_connection_slot = BLEDevice(
         "44:44:33:11:23:45",
         "wohand_has_connection_slot",
@@ -247,9 +274,14 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
             ),
             "path": "/org/bluez/hci0/dev_44_44_33_11_23_45",
         },
-        rssi=-40,
     )
     switchbot_proxy_device_has_connection_slot.metadata["delegate"] = 0
+    switchbot_proxy_device_has_connection_slot_adv = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=[],
+        manufacturer_data={1: b"\x01"},
+        rssi=-40,
+    )
 
     switchbot_device = BLEDevice(
         "44:44:33:11:23:45",
@@ -258,31 +290,41 @@ async def test_ble_device_with_proxy_client_out_of_connections_uses_best_availab
         rssi=-100,
     )
     switchbot_device.metadata["delegate"] = 0
-    switchbot_adv = AdvertisementData(
-        local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"}
+    switchbot_device_adv = generate_advertisement_data(
+        local_name="wohand",
+        service_uuids=[],
+        manufacturer_data={1: b"\x01"},
+        rssi=-100,
     )
 
     inject_advertisement_with_source(
-        hass, switchbot_device, switchbot_adv, "00:00:00:00:00:01"
+        hass, switchbot_device, switchbot_device_adv, "00:00:00:00:00:01"
     )
     inject_advertisement_with_source(
         hass,
         switchbot_proxy_device_has_connection_slot,
-        switchbot_adv,
+        switchbot_proxy_device_has_connection_slot_adv,
         "esp32_has_connection_slot",
     )
     inject_advertisement_with_source(
         hass,
         switchbot_proxy_device_no_connection_slot,
-        switchbot_adv,
+        switchbot_proxy_device_no_connection_slot_adv,
         "esp32_no_connection_slot",
     )
 
     class FakeScanner(BaseHaScanner):
         @property
-        def discovered_devices(self) -> list[BLEDevice]:
+        def discovered_devices_and_advertisement_data(
+            self,
+        ) -> dict[str, tuple[BLEDevice, AdvertisementData]]:
             """Return a list of discovered devices."""
-            return [switchbot_proxy_device_has_connection_slot]
+            return {
+                switchbot_proxy_device_has_connection_slot.address: (
+                    switchbot_proxy_device_has_connection_slot,
+                    switchbot_proxy_device_has_connection_slot_adv,
+                )
+            }
 
         async def async_get_device_by_address(self, address: str) -> BLEDevice | None:
             """Return a list of discovered devices."""
diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py
index b8ade8c39f9..fb80bb7cec4 100644
--- a/tests/components/bluetooth/test_passive_update_coordinator.py
+++ b/tests/components/bluetooth/test_passive_update_coordinator.py
@@ -127,8 +127,8 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable(
 ):
     """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device."""
     with patch(
-        "bleak.BleakScanner.discovered_devices",  # Must patch before we setup
-        [MagicMock(address="44:44:33:11:23:45")],
+        "bleak.BleakScanner.discovered_devices_and_advertisement_data",  # Must patch before we setup
+        {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())},
     ):
         await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
         await hass.async_block_till_done()
diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py
index 0ca5f299a50..e72efd565de 100644
--- a/tests/components/bluetooth/test_passive_update_processor.py
+++ b/tests/components/bluetooth/test_passive_update_processor.py
@@ -201,8 +201,8 @@ async def test_unavailable_after_no_data(
 ):
     """Test that the coordinator is unavailable after no data for a while."""
     with patch(
-        "bleak.BleakScanner.discovered_devices",  # Must patch before we setup
-        [MagicMock(address="44:44:33:11:23:45")],
+        "bleak.BleakScanner.discovered_devices_and_advertisement_data",  # Must patch before we setup
+        {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())},
     ):
         await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
         await hass.async_block_till_done()
diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py
index a4666352479..512815b1239 100644
--- a/tests/components/bluetooth/test_scanner.py
+++ b/tests/components/bluetooth/test_scanner.py
@@ -4,11 +4,7 @@ import time
 from unittest.mock import MagicMock, patch
 
 from bleak import BleakError
-from bleak.backends.scanner import (
-    AdvertisementData,
-    AdvertisementDataCallback,
-    BLEDevice,
-)
+from bleak.backends.scanner import AdvertisementDataCallback, BLEDevice
 from dbus_fast import InvalidMessageError
 import pytest
 
@@ -22,7 +18,7 @@ from homeassistant.config_entries import ConfigEntryState
 from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
 from homeassistant.util import dt as dt_util
 
-from . import _get_manager, async_setup_with_one_adapter
+from . import _get_manager, async_setup_with_one_adapter, generate_advertisement_data
 
 from tests.common import async_fire_time_changed
 
@@ -222,7 +218,7 @@ async def test_recovery_from_dbus_restart(hass, one_adapter):
     ):
         _callback(
             BLEDevice("44:44:33:11:23:42", "any_name"),
-            AdvertisementData(local_name="any_name"),
+            generate_advertisement_data(local_name="any_name"),
         )
 
     # Ensure we don't restart the scanner if we don't need to
diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py
index 36ed6abdde5..585c83f20a7 100644
--- a/tests/components/bluetooth_le_tracker/test_device_tracker.py
+++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py
@@ -5,7 +5,7 @@ from datetime import timedelta
 from unittest.mock import patch
 
 from bleak import BleakError
-from bleak.backends.scanner import AdvertisementData, BLEDevice
+from bleak.backends.scanner import BLEDevice
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 from homeassistant.components.bluetooth_le_tracker import device_tracker
@@ -23,6 +23,7 @@ from homeassistant.setup import async_setup_component
 from homeassistant.util import dt as dt_util, slugify
 
 from tests.common import async_fire_time_changed
+from tests.components.bluetooth import generate_advertisement_data
 
 
 class MockBleakClient:
@@ -89,7 +90,7 @@ async def test_preserve_new_tracked_device_name(
             service_uuids=[],
             source="local",
             device=BLEDevice(address, None),
-            advertisement=AdvertisementData(local_name="empty"),
+            advertisement=generate_advertisement_data(local_name="empty"),
             time=0,
             connectable=False,
         )
@@ -114,7 +115,7 @@ async def test_preserve_new_tracked_device_name(
             service_uuids=[],
             source="local",
             device=BLEDevice(address, None),
-            advertisement=AdvertisementData(local_name="empty"),
+            advertisement=generate_advertisement_data(local_name="empty"),
             time=0,
             connectable=False,
         )
@@ -158,7 +159,7 @@ async def test_tracking_battery_times_out(
             service_uuids=[],
             source="local",
             device=BLEDevice(address, None),
-            advertisement=AdvertisementData(local_name="empty"),
+            advertisement=generate_advertisement_data(local_name="empty"),
             time=0,
             connectable=False,
         )
@@ -224,7 +225,7 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_
             service_uuids=[],
             source="local",
             device=BLEDevice(address, None),
-            advertisement=AdvertisementData(local_name="empty"),
+            advertisement=generate_advertisement_data(local_name="empty"),
             time=0,
             connectable=False,
         )
@@ -292,7 +293,7 @@ async def test_tracking_battery_successful(
             service_uuids=[],
             source="local",
             device=BLEDevice(address, None),
-            advertisement=AdvertisementData(local_name="empty"),
+            advertisement=generate_advertisement_data(local_name="empty"),
             time=0,
             connectable=True,
         )
diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py
index e480c0a3810..25ccb72edfa 100644
--- a/tests/components/bthome/__init__.py
+++ b/tests/components/bthome/__init__.py
@@ -1,10 +1,11 @@
 """Tests for the BTHome integration."""
 
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 
+from tests.components.bluetooth import generate_advertisement_data
+
 TEMP_HUMI_SERVICE_INFO = BluetoothServiceInfoBleak(
     name="ATC 8D18B2",
     address="A4:C1:38:8D:18:B2",
@@ -16,7 +17,7 @@ TEMP_HUMI_SERVICE_INFO = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -32,7 +33,7 @@ TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000181e-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -48,7 +49,7 @@ PRST_SERVICE_INFO = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="prst"),
+    advertisement=generate_advertisement_data(local_name="prst"),
     time=0,
     connectable=False,
 )
@@ -64,7 +65,7 @@ INVALID_PAYLOAD = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -78,7 +79,7 @@ NOT_BTHOME_SERVICE_INFO = BluetoothServiceInfoBleak(
     service_data={},
     service_uuids=[],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -97,7 +98,7 @@ def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBlea
         },
         service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"],
         source="local",
-        advertisement=AdvertisementData(local_name="Test Device"),
+        advertisement=generate_advertisement_data(local_name="Test Device"),
         time=0,
         connectable=False,
     )
@@ -118,7 +119,7 @@ def make_encrypted_advertisement(
         },
         service_uuids=["0000181e-0000-1000-8000-00805f9b34fb"],
         source="local",
-        advertisement=AdvertisementData(local_name="ATC 8F80A5"),
+        advertisement=generate_advertisement_data(local_name="ATC 8F80A5"),
         time=0,
         connectable=False,
     )
diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py
index 94acad4df5a..d4014ea8657 100644
--- a/tests/components/fjaraskupan/__init__.py
+++ b/tests/components/fjaraskupan/__init__.py
@@ -2,10 +2,11 @@
 
 
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 
+from tests.components.bluetooth import generate_advertisement_data
+
 COOKER_SERVICE_INFO = BluetoothServiceInfoBleak(
     name="COOKERHOOD_FJAR",
     address="AA:BB:CC:DD:EE:FF",
@@ -15,7 +16,7 @@ COOKER_SERVICE_INFO = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="COOKERHOOD_FJAR"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py
index 7ae4c20c406..136ca99c56d 100644
--- a/tests/components/keymitt_ble/__init__.py
+++ b/tests/components/keymitt_ble/__init__.py
@@ -2,11 +2,12 @@
 from unittest.mock import patch
 
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 from homeassistant.const import CONF_ADDRESS
 
+from tests.components.bluetooth import generate_advertisement_data
+
 DOMAIN = "keymitt_ble"
 
 ENTRY_CONFIG = {
@@ -38,7 +39,7 @@ SERVICE_INFO = BluetoothServiceInfoBleak(
     service_data={},
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         local_name="mibp",
         manufacturer_data={},
         service_uuids=["0000abcd-0000-1000-8000-00805f9b34fb"],
diff --git a/tests/components/led_ble/__init__.py b/tests/components/led_ble/__init__.py
index 702b793f57a..7f48ff7a087 100644
--- a/tests/components/led_ble/__init__.py
+++ b/tests/components/led_ble/__init__.py
@@ -1,9 +1,10 @@
 """Tests for the LED BLE Bluetooth integration."""
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 
+from tests.components.bluetooth import generate_advertisement_data
+
 LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     name="Triones:F30200000152C",
     address="AA:BB:CC:DD:EE:FF",
@@ -13,7 +14,7 @@ LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Triones:F30200000152C"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
@@ -27,7 +28,7 @@ UNSUPPORTED_LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="LEDnetWFF30200000152C"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
@@ -45,7 +46,7 @@ NOT_LED_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py
index 1b5af1f8abf..c06933b7404 100644
--- a/tests/components/melnor/conftest.py
+++ b/tests/components/melnor/conftest.py
@@ -5,7 +5,6 @@ from __future__ import annotations
 from unittest.mock import AsyncMock, patch
 
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 from melnor_bluetooth.device import Device
 
 from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
@@ -14,6 +13,7 @@ from homeassistant.const import CONF_ADDRESS
 from homeassistant.core import HomeAssistant
 
 from tests.common import MockConfigEntry
+from tests.components.bluetooth import generate_advertisement_data
 
 FAKE_ADDRESS_1 = "FAKE-ADDRESS-1"
 FAKE_ADDRESS_2 = "FAKE-ADDRESS-2"
@@ -30,7 +30,7 @@ FAKE_SERVICE_INFO_1 = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(FAKE_ADDRESS_1, None),
-    advertisement=AdvertisementData(local_name=""),
+    advertisement=generate_advertisement_data(local_name=""),
     time=0,
     connectable=True,
 )
@@ -46,7 +46,7 @@ FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(FAKE_ADDRESS_2, None),
-    advertisement=AdvertisementData(local_name=""),
+    advertisement=generate_advertisement_data(local_name=""),
     time=0,
     connectable=True,
 )
diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py
index f30f72892ba..d5216cf6262 100644
--- a/tests/components/switchbot/__init__.py
+++ b/tests/components/switchbot/__init__.py
@@ -2,13 +2,13 @@
 from unittest.mock import patch
 
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 from homeassistant.const import CONF_ADDRESS
 from homeassistant.core import HomeAssistant
 
 from tests.common import MockConfigEntry
+from tests.components.bluetooth import generate_advertisement_data
 
 DOMAIN = "switchbot"
 
@@ -62,7 +62,7 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak(
     address="AA:BB:CC:DD:EE:FF",
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         local_name="WoHand",
         manufacturer_data={89: b"\xfd`0U\x92W"},
         service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"},
@@ -82,7 +82,7 @@ WOHAND_SERVICE_INFO_NOT_CONNECTABLE = BluetoothServiceInfoBleak(
     address="aa:bb:cc:dd:ee:ff",
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         local_name="WoHand",
         manufacturer_data={89: b"\xfd`0U\x92W"},
         service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"},
@@ -102,7 +102,7 @@ WOHAND_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak(
     address="798A8547-2A3D-C609-55FF-73FA824B923B",
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         local_name="WoHand",
         manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
         service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"\xc8\x10\xcf"},
@@ -122,7 +122,7 @@ WOHAND_SERVICE_ALT_ADDRESS_INFO = BluetoothServiceInfoBleak(
     address="cc:cc:cc:cc:cc:cc",
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         local_name="WoHand",
         manufacturer_data={89: b"\xfd`0U\x92W"},
         service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"},
@@ -140,7 +140,7 @@ WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak(
     service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         local_name="WoCurtain",
         manufacturer_data={89: b"\xc1\xc7'}U\xab"},
         service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Y\x00\x11\x04"},
@@ -159,7 +159,7 @@ WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak(
     service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"},
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         manufacturer_data={2409: b"\xda,\x1e\xb1\x86Au\x03\x00\x96\xac"},
         service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"},
     ),
@@ -176,7 +176,7 @@ NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak(
     service_data={},
     rssi=-60,
     source="local",
-    advertisement=AdvertisementData(
+    advertisement=generate_advertisement_data(
         manufacturer_data={},
         service_data={},
     ),
diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py
index 4593e5c01f3..ab88cc559b7 100644
--- a/tests/components/xiaomi_ble/__init__.py
+++ b/tests/components/xiaomi_ble/__init__.py
@@ -1,10 +1,11 @@
 """Tests for the SensorPush integration."""
 
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 
+from tests.components.bluetooth import generate_advertisement_data
+
 NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfoBleak(
     name="Not it",
     address="00:00:00:00:00:00",
@@ -14,7 +15,7 @@ NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfoBleak(
     service_data={},
     service_uuids=[],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -30,7 +31,7 @@ LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -46,7 +47,7 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -62,7 +63,7 @@ JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -78,7 +79,7 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -94,7 +95,7 @@ MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak(
     },
     service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"],
     source="local",
-    advertisement=AdvertisementData(local_name="Not it"),
+    advertisement=generate_advertisement_data(local_name="Not it"),
     time=0,
     connectable=False,
 )
@@ -115,7 +116,7 @@ def make_advertisement(
         },
         service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"],
         source="local",
-        advertisement=AdvertisementData(local_name="Test Device"),
+        advertisement=generate_advertisement_data(local_name="Test Device"),
         time=0,
         connectable=connectable,
     )
diff --git a/tests/components/yalexs_ble/__init__.py b/tests/components/yalexs_ble/__init__.py
index 36002a49f3e..200200c0a0b 100644
--- a/tests/components/yalexs_ble/__init__.py
+++ b/tests/components/yalexs_ble/__init__.py
@@ -1,9 +1,10 @@
 """Tests for the Yale Access Bluetooth integration."""
 from bleak.backends.device import BLEDevice
-from bleak.backends.scanner import AdvertisementData
 
 from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
 
+from tests.components.bluetooth import generate_advertisement_data
+
 YALE_ACCESS_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     name="M1012LU",
     address="AA:BB:CC:DD:EE:FF",
@@ -16,7 +17,7 @@ YALE_ACCESS_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
@@ -34,7 +35,7 @@ LOCK_DISCOVERY_INFO_UUID_ADDRESS = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
@@ -51,7 +52,7 @@ OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
@@ -69,7 +70,7 @@ NOT_YALE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
     service_data={},
     source="local",
     device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"),
-    advertisement=AdvertisementData(),
+    advertisement=generate_advertisement_data(),
     time=0,
     connectable=True,
 )
-- 
GitLab


From 731f6180289c191112ea49cc2d6b2c5321edcfcf Mon Sep 17 00:00:00 2001
From: Steven Looman <steven.looman@gmail.com>
Date: Sat, 15 Oct 2022 20:00:46 +0200
Subject: [PATCH 501/985] Make home assistant discoverable via UPnP/SSDP
 (#79820)

---
 .../components/dlna_dmr/manifest.json         |   2 +-
 .../components/dlna_dms/manifest.json         |   2 +-
 .../components/samsungtv/manifest.json        |   2 +-
 homeassistant/components/ssdp/__init__.py     | 236 ++++++++++++++++--
 homeassistant/components/ssdp/manifest.json   |   2 +-
 homeassistant/components/upnp/manifest.json   |   2 +-
 .../components/yeelight/manifest.json         |   2 +-
 homeassistant/components/yeelight/scanner.py  |   2 +-
 homeassistant/package_constraints.txt         |   2 +-
 requirements_all.txt                          |   2 +-
 requirements_test_all.txt                     |   2 +-
 tests/components/default_config/test_init.py  |   4 +-
 tests/components/dlna_dmr/conftest.py         |   9 +-
 tests/components/dlna_dms/conftest.py         |   9 +-
 tests/components/samsungtv/conftest.py        |   4 +
 tests/components/sonos/conftest.py            |   4 +
 tests/components/ssdp/conftest.py             |  10 +
 tests/components/ssdp/test_init.py            |  20 +-
 tests/components/upnp/conftest.py             |   4 +
 .../yamaha_musiccast/test_config_flow.py      |   4 +
 20 files changed, 290 insertions(+), 34 deletions(-)

diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json
index 7e03d34e900..582e48de839 100644
--- a/homeassistant/components/dlna_dmr/manifest.json
+++ b/homeassistant/components/dlna_dmr/manifest.json
@@ -3,7 +3,7 @@
   "name": "DLNA Digital Media Renderer",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
-  "requirements": ["async-upnp-client==0.31.2"],
+  "requirements": ["async-upnp-client==0.32.0"],
   "dependencies": ["ssdp"],
   "after_dependencies": ["media_source"],
   "ssdp": [
diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json
index a07f33d09dd..569b2cc635e 100644
--- a/homeassistant/components/dlna_dms/manifest.json
+++ b/homeassistant/components/dlna_dms/manifest.json
@@ -3,7 +3,7 @@
   "name": "DLNA Digital Media Server",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/dlna_dms",
-  "requirements": ["async-upnp-client==0.31.2"],
+  "requirements": ["async-upnp-client==0.32.0"],
   "dependencies": ["ssdp"],
   "after_dependencies": ["media_source"],
   "ssdp": [
diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json
index 8523231e084..0fc989c57c6 100644
--- a/homeassistant/components/samsungtv/manifest.json
+++ b/homeassistant/components/samsungtv/manifest.json
@@ -7,7 +7,7 @@
     "samsungctl[websocket]==0.7.1",
     "samsungtvws[async,encrypted]==2.5.0",
     "wakeonlan==2.1.0",
-    "async-upnp-client==0.31.2"
+    "async-upnp-client==0.32.0"
   ],
   "ssdp": [
     {
diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py
index d221cb162f4..8783f5eeb34 100644
--- a/homeassistant/components/ssdp/__init__.py
+++ b/homeassistant/components/ssdp/__init__.py
@@ -8,27 +8,54 @@ from datetime import timedelta
 from enum import Enum
 from ipaddress import IPv4Address, IPv6Address
 import logging
+import socket
 from typing import Any
+from urllib.parse import urljoin
+import xml.etree.ElementTree as ET
 
 from async_upnp_client.aiohttp import AiohttpSessionRequester
-from async_upnp_client.const import AddressTupleVXType, DeviceOrServiceType, SsdpSource
+from async_upnp_client.const import (
+    AddressTupleVXType,
+    DeviceIcon,
+    DeviceInfo,
+    DeviceOrServiceType,
+    SsdpSource,
+)
 from async_upnp_client.description_cache import DescriptionCache
+from async_upnp_client.server import (
+    SSDP_SEARCH_RESPONDER_OPTION_ALWAYS_REPLY_WITH_ROOT_DEVICE,
+    SSDP_SEARCH_RESPONDER_OPTIONS,
+    UpnpServer,
+    UpnpServerDevice,
+    UpnpServerService,
+)
 from async_upnp_client.ssdp import SSDP_PORT, determine_source_target, is_ipv4_address
 from async_upnp_client.ssdp_listener import SsdpDevice, SsdpDeviceTracker, SsdpListener
 from async_upnp_client.utils import CaseInsensitiveDict
 
 from homeassistant import config_entries
 from homeassistant.components import network
-from homeassistant.const import EVENT_HOMEASSISTANT_STOP, MATCH_ALL
+from homeassistant.const import (
+    EVENT_HOMEASSISTANT_STOP,
+    MATCH_ALL,
+    __version__ as current_version,
+)
 from homeassistant.core import HomeAssistant, callback as core_callback
 from homeassistant.data_entry_flow import BaseServiceInfo
 from homeassistant.helpers import discovery_flow
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.instance_id import async_get as async_get_instance_id
+from homeassistant.helpers.network import get_url
+from homeassistant.helpers.system_info import async_get_system_info
 from homeassistant.helpers.typing import ConfigType
 from homeassistant.loader import async_get_ssdp, bind_hass
 
 DOMAIN = "ssdp"
+SSDP_SCANNER = "scanner"
+UPNP_SERVER = "server"
+UPNP_SERVER_MIN_PORT = 40000
+UPNP_SERVER_MAX_PORT = 40100
 SCAN_INTERVAL = timedelta(minutes=2)
 
 IPV4_BROADCAST = IPv4Address("255.255.255.255")
@@ -133,7 +160,7 @@ async def async_register_callback(
 
     Returns a callback that can be used to cancel the registration.
     """
-    scanner: Scanner = hass.data[DOMAIN]
+    scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
     return await scanner.async_register_callback(callback, match_dict)
 
 
@@ -142,7 +169,7 @@ async def async_get_discovery_info_by_udn_st(  # pylint: disable=invalid-name
     hass: HomeAssistant, udn: str, st: str
 ) -> SsdpServiceInfo | None:
     """Fetch the discovery info cache."""
-    scanner: Scanner = hass.data[DOMAIN]
+    scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
     return await scanner.async_get_discovery_info_by_udn_st(udn, st)
 
 
@@ -151,7 +178,7 @@ async def async_get_discovery_info_by_st(  # pylint: disable=invalid-name
     hass: HomeAssistant, st: str
 ) -> list[SsdpServiceInfo]:
     """Fetch all the entries matching the st."""
-    scanner: Scanner = hass.data[DOMAIN]
+    scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
     return await scanner.async_get_discovery_info_by_st(st)
 
 
@@ -160,19 +187,34 @@ async def async_get_discovery_info_by_udn(
     hass: HomeAssistant, udn: str
 ) -> list[SsdpServiceInfo]:
     """Fetch all the entries matching the udn."""
-    scanner: Scanner = hass.data[DOMAIN]
+    scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
     return await scanner.async_get_discovery_info_by_udn(udn)
 
 
+async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6Address]:
+    """Build the list of ssdp sources."""
+    return {
+        source_ip
+        for source_ip in await network.async_get_enabled_source_ips(hass)
+        if not source_ip.is_loopback and not source_ip.is_global
+    }
+
+
 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     """Set up the SSDP integration."""
 
     integration_matchers = IntegrationMatchers()
     integration_matchers.async_setup(await async_get_ssdp(hass))
 
-    scanner = hass.data[DOMAIN] = Scanner(hass, integration_matchers)
+    scanner = Scanner(hass, integration_matchers)
+    server = Server(hass)
+    hass.data[DOMAIN] = {
+        SSDP_SCANNER: scanner,
+        UPNP_SERVER: server,
+    }
 
-    asyncio.create_task(scanner.async_start())
+    hass.create_task(scanner.async_start())
+    hass.create_task(server.async_start())
 
     return True
 
@@ -322,14 +364,6 @@ class Scanner:
             return_exceptions=True,
         )
 
-    async def _async_build_source_set(self) -> set[IPv4Address | IPv6Address]:
-        """Build the list of ssdp sources."""
-        return {
-            source_ip
-            for source_ip in await network.async_get_enabled_source_ips(self.hass)
-            if not source_ip.is_loopback and not source_ip.is_global
-        }
-
     async def async_scan(self, *_: Any) -> None:
         """Scan for new entries using ssdp listeners."""
         await self.async_scan_multicast()
@@ -369,7 +403,7 @@ class Scanner:
         """Start the SSDP Listeners."""
         # Devices are shared between all sources.
         device_tracker = SsdpDeviceTracker()
-        for source_ip in await self._async_build_source_set():
+        for source_ip in await async_build_source_set(self.hass):
             source_ip_str = str(source_ip)
             if source_ip.version == 6:
                 source_tuple: AddressTupleVXType = (
@@ -559,3 +593,171 @@ def _udn_from_usn(usn: str | None) -> str | None:
     if usn.startswith("uuid:"):
         return usn.split("::")[0]
     return None
+
+
+class HassUpnpServiceDevice(UpnpServerDevice):
+    """Hass Device."""
+
+    DEVICE_DEFINITION = DeviceInfo(
+        device_type="urn:home-assistant.io:device:HomeAssistant:1",
+        friendly_name="filled_later_on",
+        manufacturer="Home Assistant",
+        manufacturer_url="https://www.home-assistant.io",
+        model_description=None,
+        model_name="filled_later_on",
+        model_number=current_version,
+        model_url="https://www.home-assistant.io",
+        serial_number="filled_later_on",
+        udn="filled_later_on",
+        upc=None,
+        presentation_url="https://my.home-assistant.io/",
+        url="/device.xml",
+        icons=[
+            DeviceIcon(
+                mimetype="image/png",
+                width=1024,
+                height=1024,
+                depth=24,
+                url="/static/icons/favicon-1024x1024.png",
+            ),
+            DeviceIcon(
+                mimetype="image/png",
+                width=512,
+                height=512,
+                depth=24,
+                url="/static/icons/favicon-512x512.png",
+            ),
+            DeviceIcon(
+                mimetype="image/png",
+                width=384,
+                height=384,
+                depth=24,
+                url="/static/icons/favicon-384x384.png",
+            ),
+            DeviceIcon(
+                mimetype="image/png",
+                width=192,
+                height=192,
+                depth=24,
+                url="/static/icons/favicon-192x192.png",
+            ),
+        ],
+        xml=ET.Element("server_device"),
+    )
+    EMBEDDED_DEVICES: list[type[UpnpServerDevice]] = []
+    SERVICES: list[type[UpnpServerService]] = []
+
+
+async def _async_find_next_available_port(source: AddressTupleVXType) -> int:
+    """Get a free TCP port."""
+    family = socket.AF_INET if is_ipv4_address(source) else socket.AF_INET6
+    test_socket = socket.socket(family, socket.SOCK_STREAM)
+    test_socket.setblocking(False)
+    test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+    for port in range(UPNP_SERVER_MIN_PORT, UPNP_SERVER_MAX_PORT):
+        try:
+            test_socket.bind(source)
+            return port
+        except OSError:
+            if port == UPNP_SERVER_MAX_PORT:
+                raise
+
+    raise RuntimeError("unreachable")
+
+
+class Server:
+    """Class to be visible via SSDP searching and advertisements."""
+
+    def __init__(self, hass: HomeAssistant) -> None:
+        """Initialize class."""
+        self.hass = hass
+        self._upnp_servers: list[UpnpServer] = []
+
+    async def async_start(self) -> None:
+        """Start the server."""
+        self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
+        await self._async_start_upnp_servers()
+
+    async def _async_get_instance_udn(self) -> str:
+        """Get Unique Device Name for this instance."""
+        instance_id = await async_get_instance_id(self.hass)
+        return f"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper()
+
+    async def _async_start_upnp_servers(self) -> None:
+        """Start the UPnP/SSDP servers."""
+        # Update UDN with our instance UDN.
+        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)
+        serial_number = await async_get_instance_id(self.hass)
+        HassUpnpServiceDevice.DEVICE_DEFINITION = (
+            HassUpnpServiceDevice.DEVICE_DEFINITION._replace(
+                udn=udn,
+                friendly_name=f"{self.hass.config.location_name} (Home Assistant)",
+                model_name=model_name,
+                presentation_url=presentation_url,
+                serial_number=serial_number,
+            )
+        )
+
+        # Update icon URLs.
+        for index, icon in enumerate(HassUpnpServiceDevice.DEVICE_DEFINITION.icons):
+            new_url = urljoin(presentation_url, icon.url)
+            HassUpnpServiceDevice.DEVICE_DEFINITION.icons[index] = icon._replace(
+                url=new_url
+            )
+
+        # Start a server on all source IPs.
+        for source_ip in await async_build_source_set(self.hass):
+            source_ip_str = str(source_ip)
+            if source_ip.version == 6:
+                source_tuple: AddressTupleVXType = (
+                    source_ip_str,
+                    0,
+                    0,
+                    int(getattr(source_ip, "scope_id")),
+                )
+            else:
+                source_tuple = (source_ip_str, 0)
+            source, target = determine_source_target(source_tuple)
+            http_port = await _async_find_next_available_port(source)
+            _LOGGER.debug("Binding UPnP HTTP server to: %s:%s", source_ip, http_port)
+            self._upnp_servers.append(
+                UpnpServer(
+                    source=source,
+                    target=target,
+                    http_port=http_port,
+                    server_device=HassUpnpServiceDevice,
+                    options={
+                        SSDP_SEARCH_RESPONDER_OPTIONS: {
+                            SSDP_SEARCH_RESPONDER_OPTION_ALWAYS_REPLY_WITH_ROOT_DEVICE: True
+                        }
+                    },
+                )
+            )
+        results = await asyncio.gather(
+            *(upnp_server.async_start() for upnp_server in self._upnp_servers),
+            return_exceptions=True,
+        )
+        failed_servers = []
+        for idx, result in enumerate(results):
+            if isinstance(result, Exception):
+                _LOGGER.debug(
+                    "Failed to setup server for %s: %s",
+                    self._upnp_servers[idx].source,
+                    result,
+                )
+                failed_servers.append(self._upnp_servers[idx])
+        for server in failed_servers:
+            self._upnp_servers.remove(server)
+
+    async def async_stop(self, *_: Any) -> None:
+        """Stop the server."""
+        await self._async_stop_upnp_servers()
+
+    async def _async_stop_upnp_servers(self) -> None:
+        """Stop UPnP/SSDP servers."""
+        for server in self._upnp_servers:
+            await server.async_stop()
diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json
index e403867226a..b74883e2008 100644
--- a/homeassistant/components/ssdp/manifest.json
+++ b/homeassistant/components/ssdp/manifest.json
@@ -2,7 +2,7 @@
   "domain": "ssdp",
   "name": "Simple Service Discovery Protocol (SSDP)",
   "documentation": "https://www.home-assistant.io/integrations/ssdp",
-  "requirements": ["async-upnp-client==0.31.2"],
+  "requirements": ["async-upnp-client==0.32.0"],
   "dependencies": ["network"],
   "after_dependencies": ["zeroconf"],
   "codeowners": [],
diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json
index 8d1912f2fc4..b81b0ceb777 100644
--- a/homeassistant/components/upnp/manifest.json
+++ b/homeassistant/components/upnp/manifest.json
@@ -3,7 +3,7 @@
   "name": "UPnP/IGD",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/upnp",
-  "requirements": ["async-upnp-client==0.31.2", "getmac==0.8.2"],
+  "requirements": ["async-upnp-client==0.32.0", "getmac==0.8.2"],
   "dependencies": ["network", "ssdp"],
   "codeowners": ["@StevenLooman"],
   "ssdp": [
diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json
index 1032ef0d2e5..ac688719fea 100644
--- a/homeassistant/components/yeelight/manifest.json
+++ b/homeassistant/components/yeelight/manifest.json
@@ -2,7 +2,7 @@
   "domain": "yeelight",
   "name": "Yeelight",
   "documentation": "https://www.home-assistant.io/integrations/yeelight",
-  "requirements": ["yeelight==0.7.10", "async-upnp-client==0.31.2"],
+  "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.0"],
   "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
   "config_flow": true,
   "dependencies": ["network"],
diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py
index 4c0b0f69310..7a0d409b434 100644
--- a/homeassistant/components/yeelight/scanner.py
+++ b/homeassistant/components/yeelight/scanner.py
@@ -76,7 +76,7 @@ class YeelightScanner:
             self._listeners.append(
                 SsdpSearchListener(
                     async_callback=self._async_process_entry,
-                    service_type=SSDP_ST,
+                    search_target=SSDP_ST,
                     target=SSDP_TARGET,
                     source=source,
                     async_connect_callback=_wrap_async_connected_idx(idx),
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 97082d29719..57216b061f8 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -4,7 +4,7 @@ aiodiscover==1.4.13
 aiohttp==3.8.1
 aiohttp_cors==0.7.0
 astral==2.2
-async-upnp-client==0.31.2
+async-upnp-client==0.32.0
 async_timeout==4.0.2
 atomicwrites-homeassistant==1.4.1
 attrs==21.2.0
diff --git a/requirements_all.txt b/requirements_all.txt
index 74dc226c22e..cb50784a4ae 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -353,7 +353,7 @@ asterisk_mbox==0.5.0
 # homeassistant.components.ssdp
 # homeassistant.components.upnp
 # homeassistant.components.yeelight
-async-upnp-client==0.31.2
+async-upnp-client==0.32.0
 
 # homeassistant.components.supla
 asyncpysupla==0.0.5
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 9b4bb8659e3..ab00afd91da 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -307,7 +307,7 @@ arcam-fmj==0.12.0
 # homeassistant.components.ssdp
 # homeassistant.components.upnp
 # homeassistant.components.yeelight
-async-upnp-client==0.31.2
+async-upnp-client==0.32.0
 
 # homeassistant.components.sleepiq
 asyncsleepiq==1.2.3
diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py
index f8f8c20dbb2..186e019fcbb 100644
--- a/tests/components/default_config/test_init.py
+++ b/tests/components/default_config/test_init.py
@@ -12,7 +12,9 @@ from tests.components.blueprint.conftest import stub_blueprint_populate  # noqa:
 @pytest.fixture(autouse=True)
 def mock_ssdp():
     """Mock ssdp."""
-    with patch("homeassistant.components.ssdp.Scanner.async_scan"):
+    with patch("homeassistant.components.ssdp.Scanner.async_scan"), patch(
+        "homeassistant.components.ssdp.Server.async_start"
+    ), patch("homeassistant.components.ssdp.Server.async_stop"):
         yield
 
 
diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py
index 84aec044caf..521f770a8fa 100644
--- a/tests/components/dlna_dmr/conftest.py
+++ b/tests/components/dlna_dmr/conftest.py
@@ -116,13 +116,20 @@ def dmr_device_mock(domain_data_mock: Mock) -> Iterable[Mock]:
 
 @pytest.fixture(autouse=True)
 def ssdp_scanner_mock() -> Iterable[Mock]:
-    """Mock the SSDP module."""
+    """Mock the SSDP Scanner."""
     with patch("homeassistant.components.ssdp.Scanner", autospec=True) as mock_scanner:
         reg_callback = mock_scanner.return_value.async_register_callback
         reg_callback.return_value = Mock(return_value=None)
         yield mock_scanner.return_value
 
 
+@pytest.fixture(autouse=True)
+def ssdp_server_mock() -> Iterable[Mock]:
+    """Mock the SSDP Server."""
+    with patch("homeassistant.components.ssdp.Server", autospec=True):
+        yield
+
+
 @pytest.fixture(autouse=True)
 def async_get_local_ip_mock() -> Iterable[Mock]:
     """Mock the async_get_local_ip utility function to prevent network access."""
diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py
index 4dcd135ea86..5b785fb4ba5 100644
--- a/tests/components/dlna_dms/conftest.py
+++ b/tests/components/dlna_dms/conftest.py
@@ -129,13 +129,20 @@ def dms_device_mock(upnp_factory_mock: Mock) -> Iterable[Mock]:
 
 @pytest.fixture(autouse=True)
 def ssdp_scanner_mock() -> Iterable[Mock]:
-    """Mock the SSDP module."""
+    """Mock the SSDP Scanner."""
     with patch("homeassistant.components.ssdp.Scanner", autospec=True) as mock_scanner:
         reg_callback = mock_scanner.return_value.async_register_callback
         reg_callback.return_value = Mock(return_value=None)
         yield mock_scanner.return_value
 
 
+@pytest.fixture(autouse=True)
+def ssdp_server_mock() -> Iterable[Mock]:
+    """Mock the SSDP Server."""
+    with patch("homeassistant.components.ssdp.Server", autospec=True):
+        yield
+
+
 @pytest.fixture
 async def device_source_mock(
     hass: HomeAssistant,
diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py
index 764022f3501..73ad642f7e7 100644
--- a/tests/components/samsungtv/conftest.py
+++ b/tests/components/samsungtv/conftest.py
@@ -32,6 +32,10 @@ async def silent_ssdp_scanner(hass):
         "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
     ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
         "homeassistant.components.ssdp.Scanner.async_scan"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_start_upnp_servers"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_stop_upnp_servers"
     ):
         yield
 
diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py
index f776fb62d58..2ac1cb460cb 100644
--- a/tests/components/sonos/conftest.py
+++ b/tests/components/sonos/conftest.py
@@ -135,6 +135,10 @@ async def silent_ssdp_scanner(hass):
         "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
     ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
         "homeassistant.components.ssdp.Scanner.async_scan"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_start_upnp_servers"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_stop_upnp_servers"
     ):
         yield
 
diff --git a/tests/components/ssdp/conftest.py b/tests/components/ssdp/conftest.py
index 0b390ae469b..7b6d67895e5 100644
--- a/tests/components/ssdp/conftest.py
+++ b/tests/components/ssdp/conftest.py
@@ -1,6 +1,7 @@
 """Configuration for SSDP tests."""
 from unittest.mock import AsyncMock, patch
 
+from async_upnp_client.server import UpnpServer
 from async_upnp_client.ssdp_listener import SsdpListener
 import pytest
 
@@ -16,6 +17,15 @@ async def silent_ssdp_listener():
         yield SsdpListener
 
 
+@pytest.fixture(autouse=True)
+async def disabled_upnp_server():
+    """Disable UPnpServer."""
+    with patch("homeassistant.components.ssdp.UpnpServer.async_start"), patch(
+        "homeassistant.components.ssdp.UpnpServer.async_stop"
+    ), patch("homeassistant.components.ssdp._async_find_next_available_port"):
+        yield UpnpServer
+
+
 @pytest.fixture
 def mock_flow_init(hass):
     """Mock hass.config_entries.flow.async_init."""
diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py
index bf88f45acf9..e5d452af948 100644
--- a/tests/components/ssdp/test_init.py
+++ b/tests/components/ssdp/test_init.py
@@ -5,6 +5,7 @@ from datetime import datetime, timedelta
 from ipaddress import IPv4Address
 from unittest.mock import ANY, AsyncMock, patch
 
+from async_upnp_client.server import UpnpServer
 from async_upnp_client.ssdp import udn_from_headers
 from async_upnp_client.ssdp_listener import SsdpListener
 from async_upnp_client.utils import CaseInsensitiveDict
@@ -34,7 +35,7 @@ async def init_ssdp_component(hass: homeassistant) -> SsdpListener:
     """Initialize ssdp component and get SsdpListener."""
     await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
     await hass.async_block_till_done()
-    return hass.data[ssdp.DOMAIN]._ssdp_listeners[0]
+    return hass.data[ssdp.DOMAIN][ssdp.SSDP_SCANNER]._ssdp_listeners[0]
 
 
 @patch(
@@ -407,7 +408,7 @@ async def test_discovery_from_advertisement_sets_ssdp_st(
 
 
 @patch(
-    "homeassistant.components.ssdp.Scanner._async_build_source_set",
+    "homeassistant.components.ssdp.async_build_source_set",
     return_value={IPv4Address("192.168.1.1")},
 )
 @pytest.mark.usefixtures("mock_get_source_ip")
@@ -668,7 +669,7 @@ async def test_async_detect_interfaces_setting_empty_route(
     """Test without default interface config and the route returns nothing."""
     await init_ssdp_component(hass)
 
-    ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners
+    ssdp_listeners = hass.data[ssdp.DOMAIN][ssdp.SSDP_SCANNER]._ssdp_listeners
     sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners}
     assert sources == {("2001:db8::%1", 0, 0, 1), ("192.168.1.5", 0)}
 
@@ -698,14 +699,25 @@ async def test_bind_failure_skips_adapter(
             raise OSError
 
     SsdpListener.async_start = _async_start
+    UpnpServer.async_start = _async_start
     await init_ssdp_component(hass)
 
     assert "Failed to setup listener for" in caplog.text
 
-    ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners
+    ssdp_listeners: list[SsdpListener] = hass.data[ssdp.DOMAIN][
+        ssdp.SSDP_SCANNER
+    ]._ssdp_listeners
     sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners}
     assert sources == {("192.168.1.5", 0)}  # Note no SsdpListener for IPv6 address.
 
+    assert "Failed to setup server for" in caplog.text
+
+    upnp_servers: list[UpnpServer] = hass.data[ssdp.DOMAIN][
+        ssdp.UPNP_SERVER
+    ]._upnp_servers
+    sources = {upnp_server.source for upnp_server in upnp_servers}
+    assert sources == {("192.168.1.5", 0)}  # Note no UpnpServer for IPv6 address.
+
 
 @pytest.mark.usefixtures("mock_get_source_ip")
 @patch(
diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py
index b159a371d9a..aee25a5d112 100644
--- a/tests/components/upnp/conftest.py
+++ b/tests/components/upnp/conftest.py
@@ -122,6 +122,10 @@ async def silent_ssdp_scanner(hass):
         "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
     ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
         "homeassistant.components.ssdp.Scanner.async_scan"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_start_upnp_servers"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_stop_upnp_servers"
     ):
         yield
 
diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py
index a3fb0cf6211..b516bfe3843 100644
--- a/tests/components/yamaha_musiccast/test_config_flow.py
+++ b/tests/components/yamaha_musiccast/test_config_flow.py
@@ -21,6 +21,10 @@ async def silent_ssdp_scanner(hass):
         "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
     ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
         "homeassistant.components.ssdp.Scanner.async_scan"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_start_upnp_servers"
+    ), patch(
+        "homeassistant.components.ssdp.Server._async_stop_upnp_servers"
     ):
         yield
 
-- 
GitLab


From ff2d762f55613606f48b78409bff3e829012ad85 Mon Sep 17 00:00:00 2001
From: Jeef <jeeftor@users.noreply.github.com>
Date: Sat, 15 Oct 2022 12:43:47 -0600
Subject: [PATCH 502/985] Intellifire - Number Entity - Flame Height Control
 (#79901)

* Adding flame height control

* oops

* addressing comments

* fix coverage file

* addressing PR comments
---
 .coveragerc                                   |  1 +
 .../components/intellifire/__init__.py        |  1 +
 .../components/intellifire/number.py          | 77 +++++++++++++++++++
 .../components/intellifire/sensor.py          |  3 +-
 4 files changed, 81 insertions(+), 1 deletion(-)
 create mode 100644 homeassistant/components/intellifire/number.py

diff --git a/.coveragerc b/.coveragerc
index 9b5167eb68f..ecfa407013d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -582,6 +582,7 @@ omit =
     homeassistant/components/intellifire/coordinator.py
     homeassistant/components/intellifire/entity.py
     homeassistant/components/intellifire/fan.py
+    homeassistant/components/intellifire/number.py
     homeassistant/components/intellifire/sensor.py
     homeassistant/components/intellifire/switch.py
     homeassistant/components/intesishome/*
diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py
index 020136f078c..e4e4f1a66c9 100644
--- a/homeassistant/components/intellifire/__init__.py
+++ b/homeassistant/components/intellifire/__init__.py
@@ -24,6 +24,7 @@ PLATFORMS = [
     Platform.BINARY_SENSOR,
     Platform.CLIMATE,
     Platform.FAN,
+    Platform.NUMBER,
     Platform.SENSOR,
     Platform.SWITCH,
 ]
diff --git a/homeassistant/components/intellifire/number.py b/homeassistant/components/intellifire/number.py
new file mode 100644
index 00000000000..efa567d55cb
--- /dev/null
+++ b/homeassistant/components/intellifire/number.py
@@ -0,0 +1,77 @@
+"""Flame height number sensors."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from homeassistant.components.number import (
+    NumberEntity,
+    NumberEntityDescription,
+    NumberMode,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import DOMAIN, LOGGER
+from .coordinator import IntellifireDataUpdateCoordinator
+from .entity import IntellifireEntity
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up the fans."""
+    coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+
+    description = NumberEntityDescription(
+        key="flame_control",
+        name="Flame control",
+        icon="mdi:arrow-expand-vertical",
+    )
+
+    async_add_entities(
+        [
+            IntellifireFlameControlEntity(
+                coordinator=coordinator, description=description
+            )
+        ]
+    )
+
+
+@dataclass
+class IntellifireFlameControlEntity(IntellifireEntity, NumberEntity):
+    """Flame height control entity."""
+
+    _attr_native_max_value: float = 5
+    _attr_native_min_value: float = 1
+    _attr_native_step: float = 1
+    _attr_mode: NumberMode = NumberMode.SLIDER
+
+    def __init__(
+        self,
+        coordinator: IntellifireDataUpdateCoordinator,
+        description: NumberEntityDescription,
+    ) -> None:
+        """Initilaize Flame height Sensor."""
+        super().__init__(coordinator, description)
+
+    @property
+    def native_value(self) -> float | None:
+        """Return the current Flame Height segment number value."""
+        # UI uses 1-5 for flame height, backing lib uses 0-4
+        value = self.coordinator.read_api.data.flameheight + 1
+        return value
+
+    async def async_set_native_value(self, value: float) -> None:
+        """Slider change."""
+        value_to_send: int = int(value) - 1
+        LOGGER.debug(
+            "%s set flame height to %d with raw value %s",
+            self._attr_name,
+            value,
+            value_to_send,
+        )
+        await self.coordinator.control_api.set_flame_height(height=value_to_send)
+        await self.coordinator.async_refresh()
diff --git a/homeassistant/components/intellifire/sensor.py b/homeassistant/components/intellifire/sensor.py
index 3bb3614f71e..cbd31249133 100644
--- a/homeassistant/components/intellifire/sensor.py
+++ b/homeassistant/components/intellifire/sensor.py
@@ -60,7 +60,8 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
         icon="mdi:fire-circle",
         name="Flame Height",
         state_class=SensorStateClass.MEASUREMENT,
-        value_fn=lambda data: data.flameheight,
+        # UI uses 1-5 for flame height, backing lib uses 0-4
+        value_fn=lambda data: (data.flameheight + 1),
     ),
     IntellifireSensorEntityDescription(
         key="temperature",
-- 
GitLab


From 5efc7064737d58aa4f76f80669915d62a12e2a02 Mon Sep 17 00:00:00 2001
From: Jeef <jeeftor@users.noreply.github.com>
Date: Sat, 15 Oct 2022 14:09:00 -0600
Subject: [PATCH 503/985] Fix Intellifire UDP timeout (#80204)

---
 homeassistant/components/intellifire/config_flow.py | 4 ++--
 homeassistant/components/intellifire/manifest.json  | 8 ++++++--
 requirements_all.txt                                | 2 +-
 requirements_test_all.txt                           | 2 +-
 4 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py
index 4556668b702..d2b019cb381 100644
--- a/homeassistant/components/intellifire/config_flow.py
+++ b/homeassistant/components/intellifire/config_flow.py
@@ -38,7 +38,7 @@ async def validate_host_input(host: str, dhcp_mode: bool = False) -> str:
     """
     LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host)
     api = IntellifireAPILocal(fireplace_ip=host)
-    await api.poll(supress_warnings=dhcp_mode)
+    await api.poll(suppress_warnings=dhcp_mode)
     serial = api.data.serial
 
     LOGGER.debug("Found a fireplace: %s", serial)
@@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
     async def _find_fireplaces(self):
         """Perform UDP discovery."""
         fireplace_finder = AsyncUDPFireplaceFinder()
-        discovered_hosts = await fireplace_finder.search_fireplace(timeout=1)
+        discovered_hosts = await fireplace_finder.search_fireplace(timeout=12)
         configured_hosts = {
             entry.data[CONF_HOST]
             for entry in self._async_current_entries(include_ignore=False)
diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json
index e2ae4bb8abe..67dc5a96ad7 100644
--- a/homeassistant/components/intellifire/manifest.json
+++ b/homeassistant/components/intellifire/manifest.json
@@ -3,9 +3,13 @@
   "name": "IntelliFire",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/intellifire",
-  "requirements": ["intellifire4py==2.0.1"],
+  "requirements": ["intellifire4py==2.2.1"],
   "codeowners": ["@jeeftor"],
   "iot_class": "local_polling",
   "loggers": ["intellifire4py"],
-  "dhcp": [{ "hostname": "zentrios-*" }]
+  "dhcp": [
+    {
+      "hostname": "zentrios-*"
+    }
+  ]
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index cb50784a4ae..1e87e4d18a3 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -940,7 +940,7 @@ inkbird-ble==0.5.5
 insteon-frontend-home-assistant==0.2.0
 
 # homeassistant.components.intellifire
-intellifire4py==2.0.1
+intellifire4py==2.2.1
 
 # homeassistant.components.iotawatt
 iotawattpy==0.1.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index ab00afd91da..aa96762bd8b 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -696,7 +696,7 @@ inkbird-ble==0.5.5
 insteon-frontend-home-assistant==0.2.0
 
 # homeassistant.components.intellifire
-intellifire4py==2.0.1
+intellifire4py==2.2.1
 
 # homeassistant.components.iotawatt
 iotawattpy==0.1.0
-- 
GitLab


From 67bb6ebd132ba4d521ffa180b7b6b42a855b77f1 Mon Sep 17 00:00:00 2001
From: definitio <37266727+definitio@users.noreply.github.com>
Date: Sat, 15 Oct 2022 23:29:15 +0300
Subject: [PATCH 504/985] Fix "Unknown power_off command" for Samsung H6410
 (#80386)

---
 homeassistant/components/samsungtv/bridge.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py
index f1618bfba14..502c7f2bbb6 100644
--- a/homeassistant/components/samsungtv/bridge.py
+++ b/homeassistant/components/samsungtv/bridge.py
@@ -69,7 +69,7 @@ from .const import (
 
 KEY_PRESS_TIMEOUT = 1.2
 
-ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400"}
+ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400", "H6410"}
 ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"}
 
 REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError)
-- 
GitLab


From 7d69e24fc39e64f4d1b2c4fce46f014b4141c46d Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
Date: Sun, 16 Oct 2022 01:46:55 +0200
Subject: [PATCH 505/985] =?UTF-8?q?Bump=20fjar=C3=A5skupan=20to=202.2.0=20?=
 =?UTF-8?q?(#80401)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 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 6025665ec31..443991b5d70 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.1.0"],
+  "requirements": ["fjaraskupan==2.2.0"],
   "codeowners": ["@elupus"],
   "iot_class": "local_polling",
   "loggers": ["bleak", "fjaraskupan"],
diff --git a/requirements_all.txt b/requirements_all.txt
index 1e87e4d18a3..153642b855a 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.1.0
+fjaraskupan==2.2.0
 
 # homeassistant.components.flipr
 flipr-api==1.4.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index aa96762bd8b..f37afc21466 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.1.0
+fjaraskupan==2.2.0
 
 # homeassistant.components.flipr
 flipr-api==1.4.2
-- 
GitLab


From 3b313308826fca724b9611465ac4e10cfe12dedb Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sat, 15 Oct 2022 14:08:15 -1000
Subject: [PATCH 506/985] Bump bleak-retry-connector to 2.3.0 (#80397)

---
 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 1a5871f2e5d..ef6157706bc 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -7,7 +7,7 @@
   "quality_scale": "internal",
   "requirements": [
     "bleak==0.19.0",
-    "bleak-retry-connector==2.2.0",
+    "bleak-retry-connector==2.3.0",
     "bluetooth-adapters==0.6.0",
     "bluetooth-auto-recovery==0.3.4",
     "dbus-fast==1.45.0"
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 57216b061f8..5a0621f7216 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.2.0
+bleak-retry-connector==2.3.0
 bleak==0.19.0
 bluetooth-adapters==0.6.0
 bluetooth-auto-recovery==0.3.4
diff --git a/requirements_all.txt b/requirements_all.txt
index 153642b855a..c0161fc18ec 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.2.0
+bleak-retry-connector==2.3.0
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f37afc21466..1fed989720e 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.2.0
+bleak-retry-connector==2.3.0
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
-- 
GitLab


From 79e07253595b41b18c0e348a5d160b351ecdffd9 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sat, 15 Oct 2022 14:08:26 -1000
Subject: [PATCH 507/985] Bump aiohomekit to 2.0.2 (#80402)

---
 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 2f5f8911968..53ec03e5410 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.0.1"],
+  "requirements": ["aiohomekit==2.0.2"],
   "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 c0161fc18ec..e5b70ee9762 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.0.1
+aiohomekit==2.0.2
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 1fed989720e..e4e786e61dc 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.0.1
+aiohomekit==2.0.2
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From 3c48ce9ee700d448a1ce4a001efaab0bf3fc9f7e Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Sun, 16 Oct 2022 00:44:12 +0000
Subject: [PATCH 508/985] [ci skip] Translation update

---
 .../airthings_ble/translations/nb.json        |  8 ++++
 .../android_ip_webcam/translations/id.json    |  4 +-
 .../components/anthemav/translations/id.json  |  4 +-
 .../components/apcupsd/translations/id.json   |  4 +-
 .../components/apcupsd/translations/nb.json   | 18 +++++++++
 .../components/braviatv/translations/nb.json  |  7 ++++
 .../components/coinbase/translations/es.json  |  6 +++
 .../components/coinbase/translations/id.json  |  6 +++
 .../components/coinbase/translations/nb.json  |  7 ++++
 .../components/coinbase/translations/no.json  |  6 +++
 .../components/coinbase/translations/pl.json  |  6 +++
 .../devolo_home_network/translations/he.json  |  8 +++-
 .../devolo_home_network/translations/nb.json  | 11 ++++++
 .../dsmr_reader/translations/id.json          |  4 +-
 .../dsmr_reader/translations/nb.json          |  7 ++++
 .../components/google/translations/id.json    |  4 +-
 .../google_sheets/translations/nb.json        |  8 ++++
 .../huawei_lte/translations/he.json           | 10 +++++
 .../huawei_lte/translations/nb.json           | 12 ++++++
 .../components/ibeacon/translations/nb.json   |  7 ++++
 .../components/kegtron/translations/nb.json   |  7 ++++
 .../keymitt_ble/translations/nb.json          |  8 ++++
 .../components/lametric/translations/bg.json  |  1 +
 .../components/lametric/translations/de.json  |  2 +
 .../components/lametric/translations/es.json  |  2 +
 .../components/lametric/translations/et.json  |  2 +
 .../components/lametric/translations/he.json  |  1 +
 .../components/lametric/translations/id.json  |  4 +-
 .../components/lametric/translations/no.json  |  2 +
 .../components/lametric/translations/pl.json  |  2 +
 .../components/lametric/translations/ru.json  |  2 +
 .../lametric/translations/select.he.json      |  8 ++++
 .../lametric/translations/zh-Hant.json        |  2 +
 .../components/led_ble/translations/he.json   |  7 +++-
 .../components/lidarr/translations/nb.json    | 20 ++++++++++
 .../lutron_caseta/translations/bg.json        |  3 ++
 .../components/lyric/translations/id.json     |  4 +-
 .../components/mikrotik/translations/nb.json  | 11 ++++++
 .../components/mqtt/translations/id.json      |  2 +-
 .../components/nest/translations/id.json      |  4 +-
 .../nibe_heatpump/translations/nb.json        |  7 ++++
 .../components/octoprint/translations/nb.json |  5 +++
 .../openexchangerates/translations/id.json    |  4 +-
 .../components/prusalink/translations/he.json | 17 +++++++++
 .../components/pushover/translations/id.json  |  4 +-
 .../components/radarr/translations/id.json    |  4 +-
 .../components/radarr/translations/nb.json    | 15 ++++++++
 .../radiotherm/translations/id.json           |  2 +-
 .../components/schedule/translations/he.json  |  1 +
 .../components/senz/translations/id.json      |  4 +-
 .../components/shelly/translations/nb.json    |  6 +++
 .../simplepush/translations/id.json           |  4 +-
 .../components/skybell/translations/id.json   |  4 +-
 .../components/snooz/translations/he.json     | 21 ++++++++++
 .../components/snooz/translations/nb.json     |  7 ++++
 .../soundtouch/translations/id.json           |  4 +-
 .../components/spotify/translations/id.json   |  4 +-
 .../steam_online/translations/id.json         |  4 +-
 .../switch_as_x/translations/nb.json          | 14 +++++++
 .../components/switchbee/translations/nb.json | 12 ++++++
 .../components/threshold/translations/nb.json | 38 +++++++++++++++++++
 .../components/uptime/translations/nb.json    |  3 ++
 .../components/xbox/translations/id.json      |  4 +-
 63 files changed, 390 insertions(+), 39 deletions(-)
 create mode 100644 homeassistant/components/airthings_ble/translations/nb.json
 create mode 100644 homeassistant/components/apcupsd/translations/nb.json
 create mode 100644 homeassistant/components/braviatv/translations/nb.json
 create mode 100644 homeassistant/components/coinbase/translations/nb.json
 create mode 100644 homeassistant/components/devolo_home_network/translations/nb.json
 create mode 100644 homeassistant/components/dsmr_reader/translations/nb.json
 create mode 100644 homeassistant/components/google_sheets/translations/nb.json
 create mode 100644 homeassistant/components/huawei_lte/translations/nb.json
 create mode 100644 homeassistant/components/ibeacon/translations/nb.json
 create mode 100644 homeassistant/components/kegtron/translations/nb.json
 create mode 100644 homeassistant/components/keymitt_ble/translations/nb.json
 create mode 100644 homeassistant/components/lametric/translations/select.he.json
 create mode 100644 homeassistant/components/lidarr/translations/nb.json
 create mode 100644 homeassistant/components/mikrotik/translations/nb.json
 create mode 100644 homeassistant/components/nibe_heatpump/translations/nb.json
 create mode 100644 homeassistant/components/prusalink/translations/he.json
 create mode 100644 homeassistant/components/radarr/translations/nb.json
 create mode 100644 homeassistant/components/snooz/translations/he.json
 create mode 100644 homeassistant/components/snooz/translations/nb.json
 create mode 100644 homeassistant/components/switch_as_x/translations/nb.json
 create mode 100644 homeassistant/components/switchbee/translations/nb.json
 create mode 100644 homeassistant/components/threshold/translations/nb.json
 create mode 100644 homeassistant/components/uptime/translations/nb.json

diff --git a/homeassistant/components/airthings_ble/translations/nb.json b/homeassistant/components/airthings_ble/translations/nb.json
new file mode 100644
index 00000000000..be8e3d86b21
--- /dev/null
+++ b/homeassistant/components/airthings_ble/translations/nb.json
@@ -0,0 +1,8 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Enheten er allerede konfigurert",
+            "cannot_connect": "Tilkobling mislyktes"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/android_ip_webcam/translations/id.json b/homeassistant/components/android_ip_webcam/translations/id.json
index 593fa61dea3..d84e0a40fad 100644
--- a/homeassistant/components/android_ip_webcam/translations/id.json
+++ b/homeassistant/components/android_ip_webcam/translations/id.json
@@ -20,8 +20,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Android IP Webcam lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Android IP Webcam dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Android IP Webcam dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Android IP Webcam lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Android IP Webcam dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Android IP Webcam dalam proses penghapusan"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json
index 8c7e40b4c0b..99843443ab9 100644
--- a/homeassistant/components/anthemav/translations/id.json
+++ b/homeassistant/components/anthemav/translations/id.json
@@ -18,8 +18,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Receiver Anthem A/V lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Receiver Anthem A/V dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Anthem A/V Receiver dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Receiver Anthem A/V lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Receiver Anthem A/V dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Anthem A/V Receiver dalam proses penghapusan"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/apcupsd/translations/id.json b/homeassistant/components/apcupsd/translations/id.json
index db564f0c951..e6183aff4e2 100644
--- a/homeassistant/components/apcupsd/translations/id.json
+++ b/homeassistant/components/apcupsd/translations/id.json
@@ -19,8 +19,8 @@
     },
     "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"
+            "description": "Proses konfigurasi Integrasi APC UPS Daemon lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi APC UPS Daemon dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi Integrasi YAML APC UPS Daemon dalam proses penghapusan"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/apcupsd/translations/nb.json b/homeassistant/components/apcupsd/translations/nb.json
new file mode 100644
index 00000000000..d805bfdee74
--- /dev/null
+++ b/homeassistant/components/apcupsd/translations/nb.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Enheten er allerede konfigurert",
+            "no_status": "Ingen status er rapportert fra "
+        },
+        "error": {
+            "cannot_connect": "Tilkobling mislyktes"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "port": "Port"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/braviatv/translations/nb.json b/homeassistant/components/braviatv/translations/nb.json
new file mode 100644
index 00000000000..e9911d9e573
--- /dev/null
+++ b/homeassistant/components/braviatv/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "invalid_auth": "Ugyldig autentisering"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json
index 8d89b9b546b..d0df4dee967 100644
--- a/homeassistant/components/coinbase/translations/es.json
+++ b/homeassistant/components/coinbase/translations/es.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Se ha eliminado la configuraci\u00f3n de Coinbase mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.",
+            "title": "Se ha eliminado la configuraci\u00f3n YAML de Coinbase"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Tu API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.",
diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json
index 114c69acce2..7cb9c5c4992 100644
--- a/homeassistant/components/coinbase/translations/id.json
+++ b/homeassistant/components/coinbase/translations/id.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Proses konfigurasi Integrasi Coinbase 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 Coinbase telah dihapus"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Satu atau beberapa saldo mata uang yang diminta tidak disediakan oleh API Coinbase Anda.",
diff --git a/homeassistant/components/coinbase/translations/nb.json b/homeassistant/components/coinbase/translations/nb.json
new file mode 100644
index 00000000000..8ee9b92ee90
--- /dev/null
+++ b/homeassistant/components/coinbase/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "issues": {
+        "removed_yaml": {
+            "description": "Konfigurering av Coinbase med YAML er fjernet.\n\nDin eksisterende YAML-konfigurasjon brukes ikke av Home Assistant.\n\nFjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet."
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json
index c3f2b34cf92..1cac2a2e741 100644
--- a/homeassistant/components/coinbase/translations/no.json
+++ b/homeassistant/components/coinbase/translations/no.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Konfigurering av Coinbase med YAML er fjernet.\n\nDin eksisterende YAML-konfigurasjon brukes ikke av Home Assistant.\n\nFjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet.",
+            "title": "Coinbase YAML-konfigurasjonen er fjernet"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.",
diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json
index 70a1a021cdf..3b81c0f7aca 100644
--- a/homeassistant/components/coinbase/translations/pl.json
+++ b/homeassistant/components/coinbase/translations/pl.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "Konfiguracja Coinbase 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 Coinbase zosta\u0142a usuni\u0119ta"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Jeden lub wi\u0119cej \u017c\u0105danych sald walutowych nie jest dostarczanych przez interfejs API Coinbase.",
diff --git a/homeassistant/components/devolo_home_network/translations/he.json b/homeassistant/components/devolo_home_network/translations/he.json
index 6bb4c9a7ed3..5f1e6dbe49a 100644
--- a/homeassistant/components/devolo_home_network/translations/he.json
+++ b/homeassistant/components/devolo_home_network/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",
@@ -9,6 +10,11 @@
         },
         "flow_title": "{product} ({name})",
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "\u05e1\u05d9\u05e1\u05de\u05d4"
+                }
+            },
             "user": {
                 "data": {
                     "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP"
diff --git a/homeassistant/components/devolo_home_network/translations/nb.json b/homeassistant/components/devolo_home_network/translations/nb.json
new file mode 100644
index 00000000000..204b2dfd933
--- /dev/null
+++ b/homeassistant/components/devolo_home_network/translations/nb.json
@@ -0,0 +1,11 @@
+{
+    "config": {
+        "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "Passord"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr_reader/translations/id.json b/homeassistant/components/dsmr_reader/translations/id.json
index 0e99b099b2c..9102b5b202c 100644
--- a/homeassistant/components/dsmr_reader/translations/id.json
+++ b/homeassistant/components/dsmr_reader/translations/id.json
@@ -11,8 +11,8 @@
     },
     "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"
+            "description": "Proses konfigurasi Integrasi DSMR Reader lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi DSMR Reader dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi DSMR Reader dalam proses penghapusan"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/dsmr_reader/translations/nb.json b/homeassistant/components/dsmr_reader/translations/nb.json
new file mode 100644
index 00000000000..353183280b6
--- /dev/null
+++ b/homeassistant/components/dsmr_reader/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n enkelt konfigurasjon er mulig."
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json
index 6de37cee947..0488e5abbeb 100644
--- a/homeassistant/components/google/translations/id.json
+++ b/homeassistant/components/google/translations/id.json
@@ -35,8 +35,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Google Kalender di configuration.yaml dalam proses penghapusan di Home Assistant 2022.9.\n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Google Kalender dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Google Kalender di configuration.yaml dalam proses penghapusan di Home Assistant 2022.9.\n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Google Kalender dalam proses penghapusan"
         },
         "removed_track_new_yaml": {
             "description": "Anda telah menonaktifkan pelacakan entitas untuk Google Kalender di configuration.yaml, yang kini tidak lagi didukung. Anda harus secara manual mengubah Opsi Sistem integrasi di antarmuka untuk menonaktifkan entitas yang baru ditemukan di masa datang. Hapus pengaturan track_new dari configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
diff --git a/homeassistant/components/google_sheets/translations/nb.json b/homeassistant/components/google_sheets/translations/nb.json
new file mode 100644
index 00000000000..421a4051cd5
--- /dev/null
+++ b/homeassistant/components/google_sheets/translations/nb.json
@@ -0,0 +1,8 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Kontoen er allerede konfigurert",
+            "cannot_connect": "Tilkobling mislyktes"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json
index daad7429a83..8fbb6f735a8 100644
--- a/homeassistant/components/huawei_lte/translations/he.json
+++ b/homeassistant/components/huawei_lte/translations/he.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "abort": {
+            "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+        },
         "error": {
             "incorrect_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4",
             "incorrect_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05d2\u05d5\u05d9",
@@ -8,6 +11,13 @@
         },
         "flow_title": "{name}",
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "\u05e1\u05d9\u05e1\u05de\u05d4",
+                    "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9"
+                },
+                "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/huawei_lte/translations/nb.json b/homeassistant/components/huawei_lte/translations/nb.json
new file mode 100644
index 00000000000..5584869ffd5
--- /dev/null
+++ b/homeassistant/components/huawei_lte/translations/nb.json
@@ -0,0 +1,12 @@
+{
+    "config": {
+        "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "Passord",
+                    "username": "Brukernavn"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ibeacon/translations/nb.json b/homeassistant/components/ibeacon/translations/nb.json
new file mode 100644
index 00000000000..353183280b6
--- /dev/null
+++ b/homeassistant/components/ibeacon/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n enkelt konfigurasjon er mulig."
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kegtron/translations/nb.json b/homeassistant/components/kegtron/translations/nb.json
new file mode 100644
index 00000000000..6ba5a1f3978
--- /dev/null
+++ b/homeassistant/components/kegtron/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Enheten er allerede konfigurert"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keymitt_ble/translations/nb.json b/homeassistant/components/keymitt_ble/translations/nb.json
new file mode 100644
index 00000000000..ce3b34bc1cc
--- /dev/null
+++ b/homeassistant/components/keymitt_ble/translations/nb.json
@@ -0,0 +1,8 @@
+{
+    "config": {
+        "abort": {
+            "already_configured_device": "Enheten er allerede konfigurert",
+            "cannot_connect": "Tilkobling mislyktes"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/bg.json b/homeassistant/components/lametric/translations/bg.json
index 28104788cbd..6f85c1ddaf5 100644
--- a/homeassistant/components/lametric/translations/bg.json
+++ b/homeassistant/components/lametric/translations/bg.json
@@ -2,6 +2,7 @@
     "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\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",
             "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json
index 8f347963182..15d98a45dbb 100644
--- a/homeassistant/components/lametric/translations/de.json
+++ b/homeassistant/components/lametric/translations/de.json
@@ -8,6 +8,8 @@
             "missing_configuration": "Die LaMetric-Integration ist nicht konfiguriert. Bitte folge der Dokumentation.",
             "no_devices": "Der autorisierte Benutzer hat keine LaMetric Ger\u00e4te",
             "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
+            "reauth_device_not_found": "Das Ger\u00e4t, das du erneut authentifizieren m\u00f6chtest, wird in diesem LaMetric-Konto nicht gefunden",
+            "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
             "unknown": "Unerwarteter Fehler"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/es.json b/homeassistant/components/lametric/translations/es.json
index 7cc6fc36cf3..47fccfea279 100644
--- a/homeassistant/components/lametric/translations/es.json
+++ b/homeassistant/components/lametric/translations/es.json
@@ -8,6 +8,8 @@
             "missing_configuration": "La integraci\u00f3n de LaMetric no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.",
             "no_devices": "El usuario autorizado no tiene dispositivos LaMetric",
             "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})",
+            "reauth_device_not_found": "El dispositivo que est\u00e1s tratando de volver a autenticar no se encuentra en esta cuenta de LaMetric",
+            "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente",
             "unknown": "Error inesperado"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/et.json b/homeassistant/components/lametric/translations/et.json
index f2ec397481d..008b4e875de 100644
--- a/homeassistant/components/lametric/translations/et.json
+++ b/homeassistant/components/lametric/translations/et.json
@@ -8,6 +8,8 @@
             "missing_configuration": "LaMetricu integratsioon pole konfigureeritud. Palun j\u00e4rgige dokumentatsiooni.",
             "no_devices": "Volitatud kasutajal pole LaMetricu seadmeid",
             "no_url_available": "URL pole saadaval. Teavet selle veateate kohta saab [check the help section]({docs_url})",
+            "reauth_device_not_found": "Seadet, mida proovid uuesti autentida, ei leita sellelt LaMetricu kontolt",
+            "reauth_successful": "Taastuvastamine \u00f5nnestus",
             "unknown": "Ootamatu t\u00f5rge"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/he.json b/homeassistant/components/lametric/translations/he.json
index 46de952d566..97c060f6062 100644
--- a/homeassistant/components/lametric/translations/he.json
+++ b/homeassistant/components/lametric/translations/he.json
@@ -4,6 +4,7 @@
             "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})",
+            "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": {
diff --git a/homeassistant/components/lametric/translations/id.json b/homeassistant/components/lametric/translations/id.json
index e668efa0403..e6010f08e3d 100644
--- a/homeassistant/components/lametric/translations/id.json
+++ b/homeassistant/components/lametric/translations/id.json
@@ -8,6 +8,8 @@
             "missing_configuration": "Integrasi LaMetric tidak dikonfigurasi. Silakan ikuti dokumentasi.",
             "no_devices": "Pengguna yang diotorisasi tidak memiliki perangkat LaMetric",
             "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+            "reauth_device_not_found": "Perangkat yang Anda coba autentikasi ulang tidak ditemukan di akun LaMetric ini",
+            "reauth_successful": "Autentikasi ulang berhasil",
             "unknown": "Kesalahan yang tidak diharapkan"
         },
         "error": {
@@ -45,7 +47,7 @@
     "issues": {
         "manual_migration": {
             "description": "Integrasi LaMetric telah dimodernisasi: kini integrasinya dikonfigurasi dan disiapkan melalui antarmuka pengguna dan komunikasi menjadi lokal.\n\nSayangnya, tidak ada jalur migrasi otomatis yang mungkin dan oleh sebab itu Anda harus mengatur ulang integrasi LaMetric dengan Home Assistant. Baca dokumentasi integrasi LaMetric Home Assistant tentang cara persiapannya.\n\nHapus konfigurasi YAML LaMetric yang lama dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Migrasi manual diperlukan untuk LaMetric"
+            "title": "Migrasi manual diperlukan untuk Integrasi LaMetric"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/no.json b/homeassistant/components/lametric/translations/no.json
index 4984e190241..e1c1f331692 100644
--- a/homeassistant/components/lametric/translations/no.json
+++ b/homeassistant/components/lametric/translations/no.json
@@ -8,6 +8,8 @@
             "missing_configuration": "LaMetric-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
             "no_devices": "Den autoriserte brukeren har ingen LaMetric-enheter",
             "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})",
+            "reauth_device_not_found": "Enheten du pr\u00f8ver \u00e5 re-autentisere ble ikke funnet i denne LaMetric-kontoen",
+            "reauth_successful": "Re-autentisering var vellykket",
             "unknown": "Uventet feil"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/pl.json b/homeassistant/components/lametric/translations/pl.json
index 8ba3215cbe2..879ab701be7 100644
--- a/homeassistant/components/lametric/translations/pl.json
+++ b/homeassistant/components/lametric/translations/pl.json
@@ -8,6 +8,8 @@
             "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
             "no_devices": "Autoryzowany u\u017cytkownik nie posiada urz\u0105dze\u0144 LaMetric",
             "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})",
+            "reauth_device_not_found": "Urz\u0105dzenie, kt\u00f3re pr\u00f3bujesz ponownie uwierzytelni\u0107, nie znajduje si\u0119 na tym koncie LaMetric",
+            "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
             "unknown": "Nieoczekiwany b\u0142\u0105d"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/ru.json b/homeassistant/components/lametric/translations/ru.json
index cf9324b9abe..df712e4dc65 100644
--- a/homeassistant/components/lametric/translations/ru.json
+++ b/homeassistant/components/lametric/translations/ru.json
@@ -8,6 +8,8 @@
             "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f LaMetric \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
             "no_devices": "\u0423 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 LaMetric.",
             "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.",
+            "reauth_device_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0412\u044b \u043f\u044b\u0442\u0430\u0435\u0442\u0435\u0441\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c, \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 LaMetric.",
+            "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": {
diff --git a/homeassistant/components/lametric/translations/select.he.json b/homeassistant/components/lametric/translations/select.he.json
new file mode 100644
index 00000000000..c8f20cb8873
--- /dev/null
+++ b/homeassistant/components/lametric/translations/select.he.json
@@ -0,0 +1,8 @@
+{
+    "state": {
+        "lametric__brightness_mode": {
+            "auto": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9",
+            "manual": "\u05d9\u05d3\u05e0\u05d9\u05ea"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/zh-Hant.json b/homeassistant/components/lametric/translations/zh-Hant.json
index bcaa67ed4ad..56697150747 100644
--- a/homeassistant/components/lametric/translations/zh-Hant.json
+++ b/homeassistant/components/lametric/translations/zh-Hant.json
@@ -8,6 +8,8 @@
             "missing_configuration": "LaMetric \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
             "no_devices": "\u8a8d\u8b49\u4f7f\u7528\u8005\u6c92\u6709\u4efb\u4f55 LaMetric \u88dd\u7f6e",
             "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
+            "reauth_device_not_found": "\u65bc\u6b64 LaMetric \u5e33\u865f\u5167\u627e\u4e0d\u5230\u6240\u8a66\u8457\u91cd\u65b0\u8a8d\u8b49\u7684\u88dd\u7f6e",
+            "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f",
             "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
         },
         "error": {
diff --git a/homeassistant/components/led_ble/translations/he.json b/homeassistant/components/led_ble/translations/he.json
index 8b78650e6fe..6dc5ae75df8 100644
--- a/homeassistant/components/led_ble/translations/he.json
+++ b/homeassistant/components/led_ble/translations/he.json
@@ -3,6 +3,11 @@
         "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"
-        }
+        },
+        "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"
+        },
+        "flow_title": "{name}"
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/lidarr/translations/nb.json b/homeassistant/components/lidarr/translations/nb.json
new file mode 100644
index 00000000000..06882b4fac3
--- /dev/null
+++ b/homeassistant/components/lidarr/translations/nb.json
@@ -0,0 +1,20 @@
+{
+    "config": {
+        "error": {
+            "cannot_connect": "Tilkobling mislyktes",
+            "invalid_auth": "Ugyldig autentisering"
+        },
+        "step": {
+            "reauth_confirm": {
+                "data": {
+                    "api_key": "API-n\u00f8kkel"
+                }
+            },
+            "user": {
+                "data": {
+                    "api_key": "API-n\u00f8kkel"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/bg.json b/homeassistant/components/lutron_caseta/translations/bg.json
index 11705bad144..d5d9da2e378 100644
--- a/homeassistant/components/lutron_caseta/translations/bg.json
+++ b/homeassistant/components/lutron_caseta/translations/bg.json
@@ -19,6 +19,9 @@
             "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",
             "off": "\u0418\u0437\u043a\u043b.",
             "on": "\u0412\u043a\u043b."
         }
diff --git a/homeassistant/components/lyric/translations/id.json b/homeassistant/components/lyric/translations/id.json
index b75093d1718..4778e097c93 100644
--- a/homeassistant/components/lyric/translations/id.json
+++ b/homeassistant/components/lyric/translations/id.json
@@ -20,8 +20,8 @@
     },
     "issues": {
         "removed_yaml": {
-            "description": "Proses konfigurasi Honeywell Lyric 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 Honeywell Lyric telah dihapus"
+            "description": "Proses konfigurasi Integrasi Honeywell Lyric 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 Honeywell Lyric telah dihapus"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/mikrotik/translations/nb.json b/homeassistant/components/mikrotik/translations/nb.json
new file mode 100644
index 00000000000..204b2dfd933
--- /dev/null
+++ b/homeassistant/components/mikrotik/translations/nb.json
@@ -0,0 +1,11 @@
+{
+    "config": {
+        "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "Passord"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json
index 8a45f155815..4cb0fda8a91 100644
--- a/homeassistant/components/mqtt/translations/id.json
+++ b/homeassistant/components/mqtt/translations/id.json
@@ -51,7 +51,7 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "MQTT {platform}(s) yang dikonfigurasi secara manual ditemukan di bawah kunci platform `{platform}`.\n\nPindahkan konfigurasi ke kunci integrasi `mqtt` dan mulai ulang Home Assistant untuk memperbaiki masalah ini. Lihat [dokumentasi]({more_info_url}), untuk informasi lebih lanjut.",
+            "description": "MQTT {platform} yang dikonfigurasi secara manual ditemukan di bawah kunci platform `{platform}`.\n\nPindahkan konfigurasi ke kunci integrasi `mqtt` dan mulai ulang Home Assistant untuk memperbaiki masalah ini. Lihat [dokumentasi]({more_info_url}), untuk informasi lebih lanjut.",
             "title": "Entitas MQTT {platform} yang dikonfigurasi secara manual membutuhkan perhatian"
         }
     },
diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json
index 2d07df4dc62..9401016b990 100644
--- a/homeassistant/components/nest/translations/id.json
+++ b/homeassistant/components/nest/translations/id.json
@@ -99,8 +99,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Nest di configuration.yaml sedang dihapus di Home Assistant 2022.10. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Nest dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Nest di configuration.yaml sedang dihapus di Home Assistant 2022.10. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Nest dalam proses penghapusan"
         },
         "removed_app_auth": {
             "description": "Untuk meningkatkan keamanan dan mengurangi risiko phishing, Google telah menghentikan metode autentikasi yang digunakan oleh Home Assistant.\n\n**Tindakan berikut diperlukan untuk diselesaikan** ([info lebih lanjut]({more_info_url}))\n\n1. Kunjungi halaman integrasi\n1. Klik Konfigurasi Ulang pada integrasi Nest.\n1. Home Assistant akan memandu Anda melalui langkah-langkah untuk meningkatkan ke Autentikasi Web.\n\nLihat [instruksi integrasi]({documentation_url}) Nest untuk informasi pemecahan masalah.",
diff --git a/homeassistant/components/nibe_heatpump/translations/nb.json b/homeassistant/components/nibe_heatpump/translations/nb.json
new file mode 100644
index 00000000000..6ba5a1f3978
--- /dev/null
+++ b/homeassistant/components/nibe_heatpump/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Enheten er allerede konfigurert"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/octoprint/translations/nb.json b/homeassistant/components/octoprint/translations/nb.json
index 847c45368fd..c106bc179b3 100644
--- a/homeassistant/components/octoprint/translations/nb.json
+++ b/homeassistant/components/octoprint/translations/nb.json
@@ -1,6 +1,11 @@
 {
     "config": {
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "username": "Brukernavn"
+                }
+            },
             "user": {
                 "data": {
                     "username": "Brukernavn"
diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json
index 27f3503abc8..d53a8e9ef5f 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 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"
+            "description": "Proses konfigurasi Integrasi Open Exchange Rates lewat YAML telah dihapus.\n\nHapus konfigurasi YAML Integrasi Open Exchange Rates dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Open Exchange Rates telah dihapus"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/prusalink/translations/he.json b/homeassistant/components/prusalink/translations/he.json
new file mode 100644
index 00000000000..31a1d4ade82
--- /dev/null
+++ b/homeassistant/components/prusalink/translations/he.json
@@ -0,0 +1,17 @@
+{
+    "config": {
+        "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": {
+                    "api_key": "\u05de\u05e4\u05ea\u05d7 API",
+                    "host": "\u05de\u05d0\u05e8\u05d7"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/id.json b/homeassistant/components/pushover/translations/id.json
index 077ee9a450b..347f4ef5d3e 100644
--- a/homeassistant/components/pushover/translations/id.json
+++ b/homeassistant/components/pushover/translations/id.json
@@ -27,8 +27,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Pushover lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Pushover dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Pushover lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Pushover dalam proses penghapusan"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/radarr/translations/id.json b/homeassistant/components/radarr/translations/id.json
index f6ad3195c71..95f19f7136e 100644
--- a/homeassistant/components/radarr/translations/id.json
+++ b/homeassistant/components/radarr/translations/id.json
@@ -28,8 +28,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Radarr lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Radarr dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Radarr dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Radarr lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Radarr dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Radarr dalam proses penghapusan"
         },
         "removed_attributes": {
             "description": "Beberapa perubahan besar telah dilakukan dalam menonaktifkan sensor hitungan Film dengan alasan kehati-hatian.\n\nSensor ini bisa menyebabkan masalah dengan database yang sangat besar. Jika masih ingin menggunakannya, Anda dapat melakukannya.\n\nNama film tidak lagi disertakan sebagai atribut dalam sensor film.\n\nItem \"Yang akan datang\" telah dihapus. Sensor ini sedang dimodernisasi sebagaimana layaknya item kalender. Ruang disk sekarang dipecah ke dalam sensor yang berbeda, satu untuk setiap folder.\n\nStatus dan perintah telah dihapus karena tampaknya tidak membawa nilai dalam otomasi.",
diff --git a/homeassistant/components/radarr/translations/nb.json b/homeassistant/components/radarr/translations/nb.json
new file mode 100644
index 00000000000..7fa228d894a
--- /dev/null
+++ b/homeassistant/components/radarr/translations/nb.json
@@ -0,0 +1,15 @@
+{
+    "config": {
+        "error": {
+            "cannot_connect": "Tilkobling mislyktes",
+            "invalid_auth": "Ugyldig autentisering"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "api_key": "API-n\u00f8kkel"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/radiotherm/translations/id.json b/homeassistant/components/radiotherm/translations/id.json
index 21198e3fad1..41cc6d3736e 100644
--- a/homeassistant/components/radiotherm/translations/id.json
+++ b/homeassistant/components/radiotherm/translations/id.json
@@ -22,7 +22,7 @@
     "issues": {
         "deprecated_yaml": {
             "description": "Proses konfigurasi platform cuaca Radio Thermostat lewat YAML dalam proses penghapusan di Home Assistant 2022.9.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML platform cuaca Radio Thermostat dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Radio Thermostat dalam proses penghapusan"
+            "title": "Konfigurasi YAML Integrasi Radio Thermostat dalam proses penghapusan"
         }
     },
     "options": {
diff --git a/homeassistant/components/schedule/translations/he.json b/homeassistant/components/schedule/translations/he.json
index 1a4191f20fc..20d715bda0e 100644
--- a/homeassistant/components/schedule/translations/he.json
+++ b/homeassistant/components/schedule/translations/he.json
@@ -1,6 +1,7 @@
 {
     "state": {
         "_": {
+            "off": "\u05db\u05d1\u05d5\u05d9",
             "on": "\u05de\u05d5\u05e4\u05e2\u05dc"
         }
     },
diff --git a/homeassistant/components/senz/translations/id.json b/homeassistant/components/senz/translations/id.json
index 6f4eec8f3b9..80e198427fe 100644
--- a/homeassistant/components/senz/translations/id.json
+++ b/homeassistant/components/senz/translations/id.json
@@ -19,8 +19,8 @@
     },
     "issues": {
         "removed_yaml": {
-            "description": "Proses konfigurasi nVent RAYCHEM SENZ 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 nVent RAYCHEM SENZ telah dihapus"
+            "description": "Proses konfigurasi Integrasi nVent RAYCHEM SENZ 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 nVent RAYCHEM SENZ telah dihapus"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/shelly/translations/nb.json b/homeassistant/components/shelly/translations/nb.json
index ef07be6f70d..9c4f595ae82 100644
--- a/homeassistant/components/shelly/translations/nb.json
+++ b/homeassistant/components/shelly/translations/nb.json
@@ -6,6 +6,12 @@
                     "password": "Passord",
                     "username": "Brukernavn"
                 }
+            },
+            "reauth_confirm": {
+                "data": {
+                    "password": "Passord",
+                    "username": "Brukernavn"
+                }
             }
         }
     }
diff --git a/homeassistant/components/simplepush/translations/id.json b/homeassistant/components/simplepush/translations/id.json
index de54996d014..954e025cf89 100644
--- a/homeassistant/components/simplepush/translations/id.json
+++ b/homeassistant/components/simplepush/translations/id.json
@@ -24,8 +24,8 @@
             "title": "Konfigurasi YAML Simplepush dalam proses penghapusan"
         },
         "removed_yaml": {
-            "description": "Proses konfigurasi Simplepush lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Simplepush telah dihapus"
+            "description": "Proses konfigurasi Integrasi Simplepush lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Simplepush telah dihapus"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/skybell/translations/id.json b/homeassistant/components/skybell/translations/id.json
index 765c9cac274..ebf4ea8be24 100644
--- a/homeassistant/components/skybell/translations/id.json
+++ b/homeassistant/components/skybell/translations/id.json
@@ -27,8 +27,8 @@
     },
     "issues": {
         "removed_yaml": {
-            "description": "Proses konfigurasi Skybell 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 Skybell telah dihapus"
+            "description": "Proses konfigurasi Integrasi Skybell 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 Skybell telah dihapus"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/snooz/translations/he.json b/homeassistant/components/snooz/translations/he.json
new file mode 100644
index 00000000000..de780eb221a
--- /dev/null
+++ b/homeassistant/components/snooz/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",
+            "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": {
+                "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/snooz/translations/nb.json b/homeassistant/components/snooz/translations/nb.json
new file mode 100644
index 00000000000..6ba5a1f3978
--- /dev/null
+++ b/homeassistant/components/snooz/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Enheten er allerede konfigurert"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json
index b5114dcb398..cabbb3a6224 100644
--- a/homeassistant/components/soundtouch/translations/id.json
+++ b/homeassistant/components/soundtouch/translations/id.json
@@ -20,8 +20,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Bose SoundTouch lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Bose SoundTouch dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Bose SoundTouch dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Bose SoundTouch lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Bose SoundTouch dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Bose SoundTouch dalam proses penghapusan"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/id.json b/homeassistant/components/spotify/translations/id.json
index ef201bc638f..dda2a6ab6f9 100644
--- a/homeassistant/components/spotify/translations/id.json
+++ b/homeassistant/components/spotify/translations/id.json
@@ -21,8 +21,8 @@
     },
     "issues": {
         "removed_yaml": {
-            "description": "Proses konfigurasi Spotify 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 Spotify telah dihapus"
+            "description": "Proses konfigurasi Integrasi Spotify 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 Spotify telah dihapus"
         }
     },
     "system_health": {
diff --git a/homeassistant/components/steam_online/translations/id.json b/homeassistant/components/steam_online/translations/id.json
index 07130a2a7da..ebbe21e8e70 100644
--- a/homeassistant/components/steam_online/translations/id.json
+++ b/homeassistant/components/steam_online/translations/id.json
@@ -26,8 +26,8 @@
     },
     "issues": {
         "removed_yaml": {
-            "description": "Proses konfigurasi Steam 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 Steam telah dihapus"
+            "description": "Proses konfigurasi Integrasi Steam 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 Steam telah dihapus"
         }
     },
     "options": {
diff --git a/homeassistant/components/switch_as_x/translations/nb.json b/homeassistant/components/switch_as_x/translations/nb.json
new file mode 100644
index 00000000000..6c72f96873e
--- /dev/null
+++ b/homeassistant/components/switch_as_x/translations/nb.json
@@ -0,0 +1,14 @@
+{
+    "config": {
+        "step": {
+            "user": {
+                "data": {
+                    "entity_id": "Bryter",
+                    "target_domain": "Ny type"
+                },
+                "description": "Velg en bryter du vil vise i Home Assistant som lys, deksel eller noe annet. Den opprinnelige bryteren vil v\u00e6re skjult."
+            }
+        }
+    },
+    "title": "Endre enhetstype for en bryter"
+}
\ No newline at end of file
diff --git a/homeassistant/components/switchbee/translations/nb.json b/homeassistant/components/switchbee/translations/nb.json
new file mode 100644
index 00000000000..ef1398553b5
--- /dev/null
+++ b/homeassistant/components/switchbee/translations/nb.json
@@ -0,0 +1,12 @@
+{
+    "config": {
+        "step": {
+            "user": {
+                "data": {
+                    "password": "Passord",
+                    "username": "Brukernavn"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/threshold/translations/nb.json b/homeassistant/components/threshold/translations/nb.json
new file mode 100644
index 00000000000..1800e628f31
--- /dev/null
+++ b/homeassistant/components/threshold/translations/nb.json
@@ -0,0 +1,38 @@
+{
+    "config": {
+        "error": {
+            "need_lower_upper": "Nedre og \u00f8vre grense kan ikke begge v\u00e6re tomme"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "entity_id": "Inngangssensor",
+                    "hysteresis": "Hysterese",
+                    "lower": "Nedre grense",
+                    "name": "Navn",
+                    "upper": "\u00d8vre grense"
+                },
+                "description": "Lag en bin\u00e6r sensor som sl\u00e5s av og p\u00e5 avhengig av verdien til en sensor \n\n Kun nedre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er mindre enn den nedre grensen.\n Bare \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er st\u00f8rre enn den \u00f8vre grensen.\n B\u00e5de nedre og \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er innenfor omr\u00e5det [nedre grense .. \u00f8vre grense].",
+                "title": "Legg til terskelsensor"
+            }
+        }
+    },
+    "options": {
+        "error": {
+            "need_lower_upper": "Nedre og \u00f8vre grense kan ikke begge v\u00e6re tomme"
+        },
+        "step": {
+            "init": {
+                "data": {
+                    "entity_id": "Inngangssensor",
+                    "hysteresis": "Hysterese",
+                    "lower": "Nedre grense",
+                    "name": "Navn",
+                    "upper": "\u00d8vre grense"
+                },
+                "description": "Kun nedre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er mindre enn den nedre grensen.\n Bare \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er st\u00f8rre enn den \u00f8vre grensen.\n B\u00e5de nedre og \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er innenfor omr\u00e5det [nedre grense .. \u00f8vre grense]."
+            }
+        }
+    },
+    "title": "Terskelsensor"
+}
\ No newline at end of file
diff --git a/homeassistant/components/uptime/translations/nb.json b/homeassistant/components/uptime/translations/nb.json
new file mode 100644
index 00000000000..13c4d0e9385
--- /dev/null
+++ b/homeassistant/components/uptime/translations/nb.json
@@ -0,0 +1,3 @@
+{
+    "title": "Oppetid"
+}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/id.json b/homeassistant/components/xbox/translations/id.json
index 3df7f3ee8f2..e59a8a2989f 100644
--- a/homeassistant/components/xbox/translations/id.json
+++ b/homeassistant/components/xbox/translations/id.json
@@ -16,8 +16,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "Proses konfigurasi Xbox di configuration.yaml sedang dihapus di Home Assistant 2022.9. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
-            "title": "Konfigurasi YAML Xbox dalam proses penghapusan"
+            "description": "Proses konfigurasi Integrasi Xbox di configuration.yaml sedang dihapus di Home Assistant 2022.9. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.",
+            "title": "Konfigurasi YAML Integrasi Xbox dalam proses penghapusan"
         }
     }
 }
\ No newline at end of file
-- 
GitLab


From e1cf261379b03f3d4d18613d97716a4e629eb191 Mon Sep 17 00:00:00 2001
From: Benjamin Richter <richter.benjamin@gmail.com>
Date: Sun, 16 Oct 2022 04:01:54 +0200
Subject: [PATCH 509/985] Allow fints config as fallback for account type
 (#75680)

---
 homeassistant/components/fints/sensor.py | 107 ++++++++++++++++++-----
 1 file changed, 83 insertions(+), 24 deletions(-)

diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py
index 2e2ccd8e6b6..dc6b40982b7 100644
--- a/homeassistant/components/fints/sensor.py
+++ b/homeassistant/components/fints/sensor.py
@@ -3,10 +3,12 @@ from __future__ import annotations
 
 from collections import namedtuple
 from datetime import timedelta
+from functools import cached_property
 import logging
 from typing import Any
 
 from fints.client import FinTS3PinTanClient
+from fints.models import SEPAAccount
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
@@ -77,7 +79,7 @@ def setup_platform(
         acc[CONF_ACCOUNT]: acc[CONF_NAME] for acc in config[CONF_HOLDINGS]
     }
 
-    client = FinTsClient(credentials, fints_name)
+    client = FinTsClient(credentials, fints_name, account_config, holdings_config)
     balance_accounts, holdings_accounts = client.detect_accounts()
     accounts: list[SensorEntity] = []
 
@@ -115,21 +117,27 @@ class FinTsClient:
     Use this class as Context Manager to get the FinTS3Client object.
     """
 
-    def __init__(self, credentials: BankCredentials, name: str) -> None:
+    def __init__(
+        self,
+        credentials: BankCredentials,
+        name: str,
+        account_config: dict,
+        holdings_config: dict,
+    ) -> None:
         """Initialize a FinTsClient."""
         self._credentials = credentials
+        self._account_information: dict[str, dict] = {}
+        self._account_information_fetched = False
         self.name = name
+        self.account_config = account_config
+        self.holdings_config = holdings_config
 
-    @property
-    def client(self):
-        """Get the client object.
-
-        As the fints library is stateless, there is not benefit in caching
-        the client objects. If that ever changes, consider caching the client
-        object and also think about potential concurrency problems.
+    @cached_property
+    def client(self) -> FinTS3PinTanClient:
+        """Get the FinTS client object.
 
-        Note: As of version 2, the fints library is not stateless anymore.
-        This should be considered when reworking this integration.
+        The FinTS library persists the current dialog with the bank
+        and stores bank capabilities. So caching the client is beneficial.
         """
 
         return FinTS3PinTanClient(
@@ -139,26 +147,77 @@ class FinTsClient:
             self._credentials.url,
         )
 
-    def detect_accounts(self):
+    def get_account_information(self, iban: str) -> dict | None:
+        """Get a dictionary of account IBANs as key and account information as value."""
+
+        if not self._account_information_fetched:
+            self._account_information = {
+                account["iban"]: account
+                for account in self.client.get_information()["accounts"]
+            }
+            self._account_information_fetched = True
+
+        return self._account_information.get(iban, None)
+
+    def is_balance_account(self, account: SEPAAccount) -> bool:
+        """Determine if the given account is of type balance account."""
+        if not account.iban:
+            return False
+
+        account_information = self.get_account_information(account.iban)
+        if not account_information:
+            return False
+
+        if not account_information["type"]:
+            # bank does not support account types, use value from config
+            if (
+                account_information["iban"] in self.account_config
+                or account_information["account_number"] in self.account_config
+            ):
+                return True
+        elif 1 <= account_information["type"] <= 9:
+            return True
+
+        return False
+
+    def is_holdings_account(self, account: SEPAAccount) -> bool:
+        """Determine if the given account of type holdings account."""
+        if not account.iban:
+            return False
+
+        account_information = self.get_account_information(account.iban)
+        if not account_information:
+            return False
+
+        if not account_information["type"]:
+            # bank does not support account types, use value from config
+            if (
+                account_information["iban"] in self.holdings_config
+                or account_information["account_number"] in self.holdings_config
+            ):
+                return True
+        elif 30 <= account_information["type"] <= 39:
+            return True
+
+        return False
+
+    def detect_accounts(self) -> tuple[list, list]:
         """Identify the accounts of the bank."""
 
-        bank = self.client
-        accounts = bank.get_sepa_accounts()
-        account_types = {
-            x["iban"]: x["type"]
-            for x in bank.get_information()["accounts"]
-            if x["iban"] is not None
-        }
-
         balance_accounts = []
         holdings_accounts = []
-        for account in accounts:
-            account_type = account_types[account.iban]
-            if 1 <= account_type <= 9:  # 1-9 is balance account
+
+        for account in self.client.get_sepa_accounts():
+
+            if self.is_balance_account(account):
                 balance_accounts.append(account)
-            elif 30 <= account_type <= 39:  # 30-39 is holdings account
+
+            elif self.is_holdings_account(account):
                 holdings_accounts.append(account)
 
+            else:
+                _LOGGER.warning("Could not determine type of account %s", account.iban)
+
         return balance_accounts, holdings_accounts
 
 
-- 
GitLab


From 8d4c32e106ad71af263854c286bc42015d670e3a Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Sun, 16 Oct 2022 12:01:11 +0200
Subject: [PATCH 510/985] Update pip constraint to 22.4 (#80383)

---
 .github/workflows/ci.yaml             | 6 +++---
 homeassistant/package_constraints.txt | 2 +-
 pyproject.toml                        | 2 +-
 requirements.txt                      | 2 +-
 tox.ini                               | 2 +-
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8e6cc2398d3..97c87e9b545 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -20,8 +20,8 @@ on:
         type: boolean
 
 env:
-  CACHE_VERSION: 2
-  PIP_CACHE_VERSION: 2
+  CACHE_VERSION: 3
+  PIP_CACHE_VERSION: 3
   HA_SHORT_VERSION: 2022.11
   DEFAULT_PYTHON: 3.9
   ALL_PYTHON_VERSIONS: "['3.9', '3.10']"
@@ -546,7 +546,7 @@ jobs:
           python -m venv venv
           . venv/bin/activate
           python --version
-          pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.3" setuptools wheel
+          pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.4" setuptools wheel
           pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver
           pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver
           pip install -e .
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 5a0621f7216..584f42a2b82 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -29,7 +29,7 @@ lru-dict==1.1.8
 orjson==3.7.11
 paho-mqtt==1.6.1
 pillow==9.2.0
-pip>=21.0,<22.3
+pip>=21.0,<22.4
 psutil-home-assistant==0.0.1
 pyserial==3.5
 python-slugify==4.0.1
diff --git a/pyproject.toml b/pyproject.toml
index 09f326bbce5..c09d7f6854e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,7 +44,7 @@ dependencies    = [
     # PyJWT has loose dependency. We want the latest one.
     "cryptography==38.0.1",
     "orjson==3.7.11",
-    "pip>=21.0,<22.3",
+    "pip>=21.0,<22.4",
     "python-slugify==4.0.1",
     "pyyaml==6.0",
     "requests==2.28.1",
diff --git a/requirements.txt b/requirements.txt
index 623464874c1..785d66a08e3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,7 +18,7 @@ lru-dict==1.1.8
 PyJWT==2.5.0
 cryptography==38.0.1
 orjson==3.7.11
-pip>=21.0,<22.3
+pip>=21.0,<22.4
 python-slugify==4.0.1
 pyyaml==6.0
 requests==2.28.1
diff --git a/tox.ini b/tox.ini
index b96ab648fa2..cbc98968177 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,7 +7,7 @@ isolated_build = True
 [testenv]
 basepython = {env:PYTHON3_PATH:python3}
 # pip version duplicated in homeassistant/package_constraints.txt
-pip_version = pip>=21.0,<22.3
+pip_version = pip>=21.0,<22.4
 install_command = python -m pip install --use-deprecated legacy-resolver {opts} {packages}
 commands =
      {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs}
-- 
GitLab


From 86c6caabb293d6dea531c988f21fbc35810b1846 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 16 Oct 2022 02:05:56 -1000
Subject: [PATCH 511/985] Bump yalexs_ble to 1.9.4 for bleak 0.19.0 (#80406)

bump again for debug log fix
---
 homeassistant/components/yalexs_ble/manifest.json | 2 +-
 requirements_all.txt                              | 2 +-
 requirements_test_all.txt                         | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json
index c70669f7cc6..54638478dd5 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.2"],
+  "requirements": ["yalexs-ble==1.9.4"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco"],
   "bluetooth": [
diff --git a/requirements_all.txt b/requirements_all.txt
index e5b70ee9762..e1b800bfc2a 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2574,7 +2574,7 @@ xs1-api-client==3.0.0
 yalesmartalarmclient==0.3.9
 
 # homeassistant.components.yalexs_ble
-yalexs-ble==1.9.2
+yalexs-ble==1.9.4
 
 # homeassistant.components.august
 yalexs==1.2.6
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e4e786e61dc..c947bcfda29 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1784,7 +1784,7 @@ xmltodict==0.13.0
 yalesmartalarmclient==0.3.9
 
 # homeassistant.components.yalexs_ble
-yalexs-ble==1.9.2
+yalexs-ble==1.9.4
 
 # homeassistant.components.august
 yalexs==1.2.6
-- 
GitLab


From 29e110d4167e67521433c770442ce99475883dea Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 16 Oct 2022 02:12:52 -1000
Subject: [PATCH 512/985] Update led-ble to 1.0.0 for bleak 0.19 (#80403)

* Update led-ble for bleak 0.19

* bump version
---
 homeassistant/components/led_ble/__init__.py   | 4 +++-
 homeassistant/components/led_ble/manifest.json | 2 +-
 requirements_all.txt                           | 2 +-
 requirements_test_all.txt                      | 2 +-
 4 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py
index 5c454c6df7c..9dd6529df0d 100644
--- a/homeassistant/components/led_ble/__init__.py
+++ b/homeassistant/components/led_ble/__init__.py
@@ -43,7 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         change: bluetooth.BluetoothChange,
     ) -> None:
         """Update from a ble callback."""
-        led_ble.set_ble_device(service_info.device)
+        led_ble.set_ble_device_and_advertisement_data(
+            service_info.device, service_info.advertisement
+        )
 
     entry.async_on_unload(
         bluetooth.async_register_callback(
diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json
index 65725ed482a..6e20e0fa9cb 100644
--- a/homeassistant/components/led_ble/manifest.json
+++ b/homeassistant/components/led_ble/manifest.json
@@ -3,7 +3,7 @@
   "name": "LED BLE",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/ble_ble",
-  "requirements": ["led-ble==0.10.1"],
+  "requirements": ["led-ble==1.0.0"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco"],
   "bluetooth": [
diff --git a/requirements_all.txt b/requirements_all.txt
index e1b800bfc2a..924b821c45c 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -991,7 +991,7 @@ lakeside==0.12
 laundrify_aio==1.1.2
 
 # homeassistant.components.led_ble
-led-ble==0.10.1
+led-ble==1.0.0
 
 # homeassistant.components.foscam
 libpyfoscam==1.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index c947bcfda29..e3f504c15c0 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -732,7 +732,7 @@ lacrosse-view==0.0.9
 laundrify_aio==1.1.2
 
 # homeassistant.components.led_ble
-led-ble==0.10.1
+led-ble==1.0.0
 
 # homeassistant.components.foscam
 libpyfoscam==1.0
-- 
GitLab


From 40600991b3a0c88183e6b045ea9e7e53d0a678fe Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 16 Oct 2022 02:14:15 -1000
Subject: [PATCH 513/985] Handle TimeoutError during HKC setup attempts
 (#80399)

closes https://github.com/Jc2k/aiohomekit/issues/188
---
 homeassistant/components/homekit_controller/__init__.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index dac4afc0b22..54da6e71c8c 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -40,7 +40,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
     try:
         await conn.async_setup()
-    except (AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError) as ex:
+    except (
+        asyncio.TimeoutError,
+        AccessoryNotFoundError,
+        EncryptionError,
+        AccessoryDisconnectedError,
+    ) as ex:
         del hass.data[KNOWN_DEVICES][conn.unique_id]
         with contextlib.suppress(asyncio.TimeoutError):
             await conn.pairing.close()
-- 
GitLab


From bbe63bca4733b31b6f6cf29270cdd62765309fdf Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 16 Oct 2022 02:16:02 -1000
Subject: [PATCH 514/985] Bump pySwitchbot to 0.20.0 for bleak 0.19 changes
 (#80389)

---
 .../components/switchbot/coordinator.py          |  2 --
 homeassistant/components/switchbot/manifest.json |  2 +-
 homeassistant/components/switchbot/sensor.py     | 16 ++++++++++++----
 requirements_all.txt                             |  2 +-
 requirements_test_all.txt                        |  2 +-
 5 files changed, 15 insertions(+), 9 deletions(-)

diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py
index 7f4f3c77b99..ee93c74af37 100644
--- a/homeassistant/components/switchbot/coordinator.py
+++ b/homeassistant/components/switchbot/coordinator.py
@@ -60,7 +60,6 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
         self.device_name = device_name
         self.base_unique_id = base_unique_id
         self.model = model
-        self.service_info: bluetooth.BluetoothServiceInfoBleak | None = None
         self._ready_event = asyncio.Event()
 
     @callback
@@ -71,7 +70,6 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
     ) -> None:
         """Handle a Bluetooth event."""
         self.ble_device = service_info.device
-        self.service_info = service_info
         if adv := switchbot.parse_advertisement_data(
             service_info.device, service_info.advertisement
         ):
diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json
index 282bf6aa447..316adc6cd7a 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.15"],
+  "requirements": ["PySwitchbot==0.20.0"],
   "config_flow": true,
   "dependencies": ["bluetooth"],
   "codeowners": [
diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py
index 9dd0ef3900b..9b1baf805bb 100644
--- a/homeassistant/components/switchbot/sensor.py
+++ b/homeassistant/components/switchbot/sensor.py
@@ -1,6 +1,7 @@
 """Support for SwitchBot sensors."""
 from __future__ import annotations
 
+from homeassistant.components.bluetooth import async_last_service_info
 from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
@@ -106,7 +107,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
         self.entity_description = SENSOR_TYPES[sensor]
 
     @property
-    def native_value(self) -> str | int:
+    def native_value(self) -> str | int | None:
         """Return the state of the sensor."""
         return self.data["data"][self._sensor]
 
@@ -115,7 +116,14 @@ class SwitchbotRSSISensor(SwitchBotSensor):
     """Representation of a Switchbot RSSI sensor."""
 
     @property
-    def native_value(self) -> str | int:
+    def native_value(self) -> str | int | None:
         """Return the state of the sensor."""
-        assert self.coordinator.service_info is not None
-        return self.coordinator.service_info.rssi
+        # Switchbot supports both connectable and non-connectable devices
+        # so we need to request the rssi value based on the connectable instead
+        # of the nearest scanner since that is the RSSI that matters for controlling
+        # the device.
+        if service_info := async_last_service_info(
+            self.hass, self._address, self.coordinator.connectable
+        ):
+            return service_info.rssi
+        return None
diff --git a/requirements_all.txt b/requirements_all.txt
index 924b821c45c..2490a8f1aac 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
 PySocks==1.7.1
 
 # homeassistant.components.switchbot
-PySwitchbot==0.19.15
+PySwitchbot==0.20.0
 
 # homeassistant.components.transport_nsw
 PyTransportNSW==0.1.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e3f504c15c0..44cd73094e9 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
 PySocks==1.7.1
 
 # homeassistant.components.switchbot
-PySwitchbot==0.19.15
+PySwitchbot==0.20.0
 
 # homeassistant.components.transport_nsw
 PyTransportNSW==0.1.1
-- 
GitLab


From bf6efc758999b3b4d52113b60497400db39c04d6 Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
Date: Mon, 17 Oct 2022 01:05:53 +1100
Subject: [PATCH 515/985] Powerview sensors updates (#80417)

---
 .../hunterdouglas_powerview/const.py          |   3 +
 .../hunterdouglas_powerview/sensor.py         | 117 +++++++++++-------
 2 files changed, 78 insertions(+), 42 deletions(-)

diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py
index 9d99710f36d..5dd59fba39c 100644
--- a/homeassistant/components/hunterdouglas_powerview/const.py
+++ b/homeassistant/components/hunterdouglas_powerview/const.py
@@ -46,6 +46,9 @@ ROOM_ID = "id"
 SHADE_BATTERY_LEVEL = "batteryStrength"
 SHADE_BATTERY_LEVEL_MAX = 200
 
+ATTR_SIGNAL_STRENGTH = "signalStrength"
+ATTR_SIGNAL_STRENGTH_MAX = 4
+
 STATE_ATTRIBUTE_ROOM_NAME = "roomName"
 
 HUB_EXCEPTIONS = (
diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py
index 1887498e604..88e62d51937 100644
--- a/homeassistant/components/hunterdouglas_powerview/sensor.py
+++ b/homeassistant/components/hunterdouglas_powerview/sensor.py
@@ -13,6 +13,8 @@ from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import (
+    ATTR_SIGNAL_STRENGTH,
+    ATTR_SIGNAL_STRENGTH_MAX,
     DOMAIN,
     ROOM_ID_IN_SHADE,
     ROOM_NAME_UNICODE,
@@ -23,50 +25,36 @@ from .entity import ShadeEntity
 from .model import PowerviewEntryData
 
 
-async def async_setup_entry(
-    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
-) -> None:
-    """Set up the hunter douglas shades sensors."""
+class PowerViewSensor(ShadeEntity, SensorEntity):
+    """Representation of an shade battery charge sensor."""
 
-    pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
+    _attr_entity_category = EntityCategory.DIAGNOSTIC
+    _attr_state_class = SensorStateClass.MEASUREMENT
 
-    entities = []
-    for raw_shade in pv_entry.shade_data.values():
-        shade: BaseShade = PvShade(raw_shade, pv_entry.api)
-        if SHADE_BATTERY_LEVEL not in shade.raw_data:
-            continue
-        name_before_refresh = shade.name
-        room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
-        room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
-        entities.append(
-            PowerViewShadeBatterySensor(
-                pv_entry.coordinator,
-                pv_entry.device_info,
-                room_name,
-                shade,
-                name_before_refresh,
-            )
+    async def async_added_to_hass(self) -> None:
+        """When entity is added to hass."""
+        self.async_on_remove(
+            self.coordinator.async_add_listener(self._async_update_shade_from_group)
         )
-    async_add_entities(entities)
 
+    @callback
+    def _async_update_shade_from_group(self) -> None:
+        """Update with new data from the coordinator."""
+        self._shade.raw_data = self.data.get_raw_data(self._shade.id)
+        self.async_write_ha_state()
 
-class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity):
+
+class PowerViewShadeBatterySensor(PowerViewSensor):
     """Representation of an shade battery charge sensor."""
 
-    _attr_entity_category = EntityCategory.DIAGNOSTIC
     _attr_native_unit_of_measurement = PERCENTAGE
     _attr_device_class = SensorDeviceClass.BATTERY
-    _attr_state_class = SensorStateClass.MEASUREMENT
 
     def __init__(self, coordinator, device_info, room_name, shade, name):
         """Initialize the shade."""
         super().__init__(coordinator, device_info, room_name, shade, name)
         self._attr_unique_id = f"{self._attr_unique_id}_charge"
-
-    @property
-    def name(self):
-        """Name of the shade battery."""
-        return f"{self._shade_name} Battery"
+        self._attr_name = f"{self._shade_name} Battery"
 
     @property
     def native_value(self) -> int:
@@ -75,18 +63,63 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity):
             self._shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100
         )
 
-    async def async_added_to_hass(self) -> None:
-        """When entity is added to hass."""
-        self.async_on_remove(
-            self.coordinator.async_add_listener(self._async_update_shade_from_group)
-        )
+    async def async_update(self) -> None:
+        """Refresh shade battery."""
+        await self._shade.refresh_battery()
 
-    @callback
-    def _async_update_shade_from_group(self) -> None:
-        """Update with new data from the coordinator."""
-        self._shade.raw_data = self.data.get_raw_data(self._shade.id)
-        self.async_write_ha_state()
+
+class PowerViewShadeSignalSensor(PowerViewSensor):
+    """Representation of an shade signal sensor."""
+
+    _attr_native_unit_of_measurement = PERCENTAGE
+    _attr_state_class = SensorStateClass.MEASUREMENT
+
+    def __init__(self, coordinator, device_info, room_name, shade, name):
+        """Initialize the shade."""
+        super().__init__(coordinator, device_info, room_name, shade, name)
+        self._attr_unique_id = f"{self._attr_unique_id}_signal"
+        self._attr_name = f"{self._shade_name} Signal"
+
+    @property
+    def native_value(self) -> int:
+        """Get the current value in percentage."""
+        return round(
+            self._shade.raw_data[ATTR_SIGNAL_STRENGTH] / ATTR_SIGNAL_STRENGTH_MAX * 100
+        )
 
     async def async_update(self) -> None:
-        """Refresh shade battery."""
-        await self._shade.refreshBattery()
+        """Refresh signal strength."""
+        await self._shade.refresh()
+
+
+SENSOR_TYPES = {
+    PowerViewShadeBatterySensor: SHADE_BATTERY_LEVEL,
+    PowerViewShadeSignalSensor: ATTR_SIGNAL_STRENGTH,
+}
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+    """Set up the hunter douglas shades sensors."""
+
+    pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
+
+    entities: list[PowerViewSensor] = []
+    for raw_shade in pv_entry.shade_data.values():
+        shade: BaseShade = PvShade(raw_shade, pv_entry.api)
+        name_before_refresh = shade.name
+        room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
+        room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
+        for cls, attr in SENSOR_TYPES.items():
+            if attr in shade.raw_data:
+                entities.append(
+                    cls(
+                        pv_entry.coordinator,
+                        pv_entry.device_info,
+                        room_name,
+                        shade,
+                        name_before_refresh,
+                    )
+                )
+    async_add_entities(entities)
-- 
GitLab


From 4bfadb67454ef418dcfef6705d55090fa155317d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 16 Oct 2022 05:08:44 -1000
Subject: [PATCH 516/985] Bump aiohomekit to 2.1.0 for bleak 0.19.0 (#80426)

changelog: https://github.com/Jc2k/aiohomekit/compare/2.0.2...2.1.0
---
 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 53ec03e5410..07db0b4a256 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.0.2"],
+  "requirements": ["aiohomekit==2.1.0"],
   "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 2490a8f1aac..c59a744839b 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.0.2
+aiohomekit==2.1.0
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 44cd73094e9..485cac63cb5 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.0.2
+aiohomekit==2.1.0
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From fa24529d98501ed5263bccd728effb67fbe1855f Mon Sep 17 00:00:00 2001
From: mvn23 <schopdiedwaas@gmail.com>
Date: Sun, 16 Oct 2022 17:09:37 +0200
Subject: [PATCH 517/985] Bump pyotgw to 2.1.1 (#80421)

---
 homeassistant/components/opentherm_gw/manifest.json | 2 +-
 requirements_all.txt                                | 2 +-
 requirements_test_all.txt                           | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json
index 97767ab8383..99a10bc1539 100644
--- a/homeassistant/components/opentherm_gw/manifest.json
+++ b/homeassistant/components/opentherm_gw/manifest.json
@@ -2,7 +2,7 @@
   "domain": "opentherm_gw",
   "name": "OpenTherm Gateway",
   "documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
-  "requirements": ["pyotgw==2.0.3"],
+  "requirements": ["pyotgw==2.1.1"],
   "codeowners": ["@mvn23"],
   "config_flow": true,
   "iot_class": "local_push",
diff --git a/requirements_all.txt b/requirements_all.txt
index c59a744839b..23f1a53e3ed 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1781,7 +1781,7 @@ pyopnsense==0.2.0
 pyoppleio==1.0.5
 
 # homeassistant.components.opentherm_gw
-pyotgw==2.0.3
+pyotgw==2.1.1
 
 # homeassistant.auth.mfa_modules.notify
 # homeassistant.auth.mfa_modules.totp
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 485cac63cb5..5519f2e94b9 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1258,7 +1258,7 @@ pyopenuv==2022.04.0
 pyopnsense==0.2.0
 
 # homeassistant.components.opentherm_gw
-pyotgw==2.0.3
+pyotgw==2.1.1
 
 # homeassistant.auth.mfa_modules.notify
 # homeassistant.auth.mfa_modules.totp
-- 
GitLab


From 5d09fe8dc184488e399348877c21cfc9a39ca955 Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
Date: Sun, 16 Oct 2022 17:13:32 +0200
Subject: [PATCH 518/985] Avoid logging tracebacks for auth failures in philips
 js (#80381)

Avoid logging tracebacks for auth failures
---
 homeassistant/components/philips_js/__init__.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py
index 29c8ab36ba2..a31212be3f7 100644
--- a/homeassistant/components/philips_js/__init__.py
+++ b/homeassistant/components/philips_js/__init__.py
@@ -7,7 +7,7 @@ from datetime import timedelta
 import logging
 from typing import Any
 
-from haphilipsjs import ConnectionFailure, PhilipsTV
+from haphilipsjs import AutenticationFailure, ConnectionFailure, PhilipsTV
 from haphilipsjs.typing import SystemType
 
 from homeassistant.config_entries import ConfigEntry
@@ -21,7 +21,7 @@ from homeassistant.const import (
 from homeassistant.core import Context, HassJob, HomeAssistant, callback
 from homeassistant.helpers.debounce import Debouncer
 from homeassistant.helpers.trigger import TriggerActionType
-from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
 from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN
 
@@ -169,7 +169,11 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
 
     async def _notify_task(self):
         while self._notify_wanted:
-            res = await self.api.notifyChange(130)
+            try:
+                res = await self.api.notifyChange(130)
+            except (ConnectionFailure, AutenticationFailure):
+                res = None
+
             if res:
                 self.async_set_updated_data(None)
             elif res is None:
@@ -203,3 +207,5 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
             self._async_notify_schedule()
         except ConnectionFailure:
             pass
+        except AutenticationFailure as exception:
+            raise UpdateFailed(str(exception)) from exception
-- 
GitLab


From ef90fe9aee592aedd76993194b982a9a96fb2680 Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
Date: Sun, 16 Oct 2022 19:06:53 +0200
Subject: [PATCH 519/985] Display and log google_travel_time errors (#77604)

* Display and log google_travel_time errors

* Rename is_valid_config_entry to validate_config_entry

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>
---
 .../google_travel_time/config_flow.py         | 33 +++++----
 .../components/google_travel_time/helpers.py  | 46 ++++++++++---
 .../google_travel_time/strings.json           |  1 +
 .../google_travel_time/translations/en.json   |  3 +-
 .../components/google_travel_time/conftest.py | 20 +++++-
 .../google_travel_time/test_config_flow.py    | 67 +++++++++++++++++++
 6 files changed, 142 insertions(+), 28 deletions(-)

diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py
index f2e23b02cc0..3a4c576f218 100644
--- a/homeassistant/components/google_travel_time/config_flow.py
+++ b/homeassistant/components/google_travel_time/config_flow.py
@@ -1,13 +1,12 @@
 """Config flow for Google Maps Travel Time integration."""
 from __future__ import annotations
 
-import logging
-
 import voluptuous as vol
 
 from homeassistant import config_entries
 from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
 from homeassistant.core import callback
+from homeassistant.data_entry_flow import FlowResult
 import homeassistant.helpers.config_validation as cv
 
 from .const import (
@@ -36,9 +35,7 @@ from .const import (
     TRAVEL_MODEL,
     UNITS,
 )
-from .helpers import is_valid_config_entry
-
-_LOGGER = logging.getLogger(__name__)
+from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
 
 
 class GoogleOptionsFlow(config_entries.OptionsFlow):
@@ -48,7 +45,7 @@ class GoogleOptionsFlow(config_entries.OptionsFlow):
         """Initialize google options flow."""
         self.config_entry = config_entry
 
-    async def async_step_init(self, user_input=None):
+    async def async_step_init(self, user_input=None) -> FlowResult:
         """Handle the initial step."""
         if user_input is not None:
             time_type = user_input.pop(CONF_TIME_TYPE)
@@ -122,25 +119,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
         """Get the options flow for this handler."""
         return GoogleOptionsFlow(config_entry)
 
-    async def async_step_user(self, user_input=None):
+    async def async_step_user(self, user_input=None) -> FlowResult:
         """Handle the initial step."""
         errors = {}
         user_input = user_input or {}
         if user_input:
-            if await self.hass.async_add_executor_job(
-                is_valid_config_entry,
-                self.hass,
-                user_input[CONF_API_KEY],
-                user_input[CONF_ORIGIN],
-                user_input[CONF_DESTINATION],
-            ):
+            try:
+                await self.hass.async_add_executor_job(
+                    validate_config_entry,
+                    self.hass,
+                    user_input[CONF_API_KEY],
+                    user_input[CONF_ORIGIN],
+                    user_input[CONF_DESTINATION],
+                )
                 return self.async_create_entry(
                     title=user_input.get(CONF_NAME, DEFAULT_NAME),
                     data=user_input,
                 )
-
-            # If we get here, it's because we couldn't connect
-            errors["base"] = "cannot_connect"
+            except InvalidApiKeyException:
+                errors["base"] = "invalid_auth"
+            except UnknownException:
+                errors["base"] = "cannot_connect"
 
         return self.async_show_form(
             step_id="user",
diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py
index dc8fc32af7a..12394a23209 100644
--- a/homeassistant/components/google_travel_time/helpers.py
+++ b/homeassistant/components/google_travel_time/helpers.py
@@ -1,18 +1,46 @@
 """Helpers for Google Time Travel integration."""
+import logging
+
 from googlemaps import Client
 from googlemaps.distance_matrix import distance_matrix
-from googlemaps.exceptions import ApiError
+from googlemaps.exceptions import ApiError, Timeout, TransportError
 
+from homeassistant.core import HomeAssistant
 from homeassistant.helpers.location import find_coordinates
 
+_LOGGER = logging.getLogger(__name__)
+
 
-def is_valid_config_entry(hass, api_key, origin, destination):
+def validate_config_entry(
+    hass: HomeAssistant, api_key: str, origin: str, destination: str
+) -> None:
     """Return whether the config entry data is valid."""
-    origin = find_coordinates(hass, origin)
-    destination = find_coordinates(hass, destination)
-    client = Client(api_key, timeout=10)
+    resolved_origin = find_coordinates(hass, origin)
+    resolved_destination = find_coordinates(hass, destination)
     try:
-        distance_matrix(client, origin, destination, mode="driving")
-    except ApiError:
-        return False
-    return True
+        client = Client(api_key, timeout=10)
+    except ValueError as value_error:
+        _LOGGER.error("Malformed API key")
+        raise InvalidApiKeyException from value_error
+    try:
+        distance_matrix(client, resolved_origin, resolved_destination, mode="driving")
+    except ApiError as api_error:
+        if api_error.status == "REQUEST_DENIED":
+            _LOGGER.error("Request denied: %s", api_error.message)
+            raise InvalidApiKeyException from api_error
+        _LOGGER.error("Unknown error: %s", api_error.message)
+        raise UnknownException() from api_error
+    except TransportError as transport_error:
+        _LOGGER.error("Unknown error: %s", transport_error)
+        raise UnknownException() from transport_error
+    except Timeout as timeout_error:
+        _LOGGER.error("Timeout error")
+        raise UnknownException() from timeout_error
+
+
+class InvalidApiKeyException(Exception):
+    """Invalid API Key Error."""
+
+
+class UnknownException(Exception):
+    """Unknown API Error."""
diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json
index e0043a4b342..22a122b9a53 100644
--- a/homeassistant/components/google_travel_time/strings.json
+++ b/homeassistant/components/google_travel_time/strings.json
@@ -13,6 +13,7 @@
       }
     },
     "error": {
+      "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
     },
     "abort": {
diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json
index b0e08c1d63d..8e91fbf1df0 100644
--- a/homeassistant/components/google_travel_time/translations/en.json
+++ b/homeassistant/components/google_travel_time/translations/en.json
@@ -4,7 +4,8 @@
             "already_configured": "Location is already configured"
         },
         "error": {
-            "cannot_connect": "Failed to connect"
+            "cannot_connect": "Failed to connect",
+            "invalid_auth": "Invalid authentication"
         },
         "step": {
             "user": {
diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py
index 4ca7c5a9105..ec5a8f16917 100644
--- a/tests/components/google_travel_time/conftest.py
+++ b/tests/components/google_travel_time/conftest.py
@@ -1,7 +1,7 @@
 """Fixtures for Google Time Travel tests."""
 from unittest.mock import patch
 
-from googlemaps.exceptions import ApiError
+from googlemaps.exceptions import ApiError, Timeout, TransportError
 import pytest
 
 from homeassistant.components.google_travel_time.const import DOMAIN
@@ -58,3 +58,21 @@ def validate_config_entry_fixture():
 def invalidate_config_entry_fixture(validate_config_entry):
     """Return invalid config entry."""
     validate_config_entry.side_effect = ApiError("test")
+
+
+@pytest.fixture(name="invalid_api_key")
+def invalid_api_key_fixture(validate_config_entry):
+    """Throw a REQUEST_DENIED ApiError."""
+    validate_config_entry.side_effect = ApiError("REQUEST_DENIED", "Invalid API key.")
+
+
+@pytest.fixture(name="timeout")
+def timeout_fixture(validate_config_entry):
+    """Throw a Timeout exception."""
+    validate_config_entry.side_effect = Timeout()
+
+
+@pytest.fixture(name="transport_error")
+def transport_error_fixture(validate_config_entry):
+    """Throw a TransportError exception."""
+    validate_config_entry.side_effect = TransportError("Unknown.")
diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py
index 06cc1851421..ff7e0768301 100644
--- a/tests/components/google_travel_time/test_config_flow.py
+++ b/tests/components/google_travel_time/test_config_flow.py
@@ -67,6 +67,73 @@ async def test_invalid_config_entry(hass):
     assert result2["errors"] == {"base": "cannot_connect"}
 
 
+@pytest.mark.usefixtures("invalid_api_key")
+async def test_invalid_api_key(hass):
+    """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"] == {}
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        MOCK_CONFIG,
+    )
+
+    assert result2["type"] == data_entry_flow.FlowResultType.FORM
+    assert result2["errors"] == {"base": "invalid_auth"}
+
+
+@pytest.mark.usefixtures("transport_error")
+async def test_transport_error(hass):
+    """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"] == {}
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        MOCK_CONFIG,
+    )
+
+    assert result2["type"] == data_entry_flow.FlowResultType.FORM
+    assert result2["errors"] == {"base": "cannot_connect"}
+
+
+@pytest.mark.usefixtures("timeout")
+async def test_timeout(hass):
+    """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"] == {}
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        MOCK_CONFIG,
+    )
+
+    assert result2["type"] == data_entry_flow.FlowResultType.FORM
+    assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_malformed_api_key(hass):
+    """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"] == {}
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        MOCK_CONFIG,
+    )
+
+    assert result2["type"] == data_entry_flow.FlowResultType.FORM
+    assert result2["errors"] == {"base": "invalid_auth"}
+
+
 @pytest.mark.parametrize(
     "data,options",
     [
-- 
GitLab


From 1973f00f6b934f63ed6e995f24b931ec196d9cfe Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Sun, 16 Oct 2022 10:40:55 -0700
Subject: [PATCH 520/985] Bump gcal_sync to 1.1.0 (#80431)

---
 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 39c889786b7..1f1a6c32481 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==0.10.0", "oauth2client==4.1.3"],
+  "requirements": ["gcal-sync==1.1.0", "oauth2client==4.1.3"],
   "codeowners": ["@allenporter"],
   "iot_class": "cloud_polling",
   "loggers": ["googleapiclient"]
diff --git a/requirements_all.txt b/requirements_all.txt
index 23f1a53e3ed..774942a9074 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -728,7 +728,7 @@ gTTS==2.2.4
 garages-amsterdam==3.0.0
 
 # homeassistant.components.google
-gcal-sync==0.10.0
+gcal-sync==1.1.0
 
 # homeassistant.components.geniushub
 geniushub-client==0.6.30
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 5519f2e94b9..2f49c44d769 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -544,7 +544,7 @@ gTTS==2.2.4
 garages-amsterdam==3.0.0
 
 # homeassistant.components.google
-gcal-sync==0.10.0
+gcal-sync==1.1.0
 
 # homeassistant.components.geocaching
 geocachingapi==0.2.1
-- 
GitLab


From f5666641ce712775a18a06a42450e0ed0c0d75e0 Mon Sep 17 00:00:00 2001
From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com>
Date: Sun, 16 Oct 2022 20:32:38 +0200
Subject: [PATCH 521/985] Bump plugwise to v0.25.2 and adapt climate (#80347)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
---
 homeassistant/components/plugwise/climate.py  | 55 +++++++++++++++----
 .../components/plugwise/manifest.json         |  2 +-
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 .../anna_heatpump_heating/all_data.json       |  1 -
 .../fixtures/m_adam_cooling/all_data.json     |  6 +-
 .../m_anna_heatpump_cooling/all_data.json     |  2 -
 .../m_anna_heatpump_idle/all_data.json        |  2 -
 tests/components/plugwise/test_climate.py     | 34 ++++++------
 9 files changed, 65 insertions(+), 41 deletions(-)

diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py
index 84dc4576700..8d0d3578c2c 100644
--- a/homeassistant/components/plugwise/climate.py
+++ b/homeassistant/components/plugwise/climate.py
@@ -5,6 +5,8 @@ from collections.abc import Mapping
 from typing import Any
 
 from homeassistant.components.climate import (
+    ATTR_TARGET_TEMP_HIGH,
+    ATTR_TARGET_TEMP_LOW,
     ClimateEntity,
     ClimateEntityFeature,
     HVACAction,
@@ -52,8 +54,12 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
         self._attr_extra_state_attributes = {}
         self._attr_unique_id = f"{device_id}-climate"
 
-        # Determine preset modes
+        # Determine supported features
         self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
+        if self.coordinator.data.gateway["cooling_present"]:
+            self._attr_supported_features = (
+                ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
+            )
         if presets := self.device.get("preset_modes"):
             self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
             self._attr_preset_modes = presets
@@ -61,7 +67,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
         # Determine hvac modes and current hvac mode
         self._attr_hvac_modes = [HVACMode.HEAT]
         if self.coordinator.data.gateway["cooling_present"]:
-            self._attr_hvac_modes.append(HVACMode.COOL)
+            self._attr_hvac_modes = [HVACMode.HEAT_COOL]
         if self.device["available_schedules"] != ["None"]:
             self._attr_hvac_modes.append(HVACMode.AUTO)
 
@@ -79,12 +85,32 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
 
     @property
     def target_temperature(self) -> float:
-        """Return the temperature we try to reach."""
+        """Return the temperature we try to reach.
+
+        Connected to the HVACMode combination of AUTO-HEAT.
+        """
+
         return self.device["thermostat"]["setpoint"]
 
+    @property
+    def target_temperature_high(self) -> float:
+        """Return the temperature we try to reach in case of cooling.
+
+        Connected to the HVACMode combination of AUTO-HEAT_COOL.
+        """
+        return self.device["thermostat"]["setpoint_high"]
+
+    @property
+    def target_temperature_low(self) -> float:
+        """Return the heating temperature we try to reach in case of heating.
+
+        Connected to the HVACMode combination AUTO-HEAT_COOL.
+        """
+        return self.device["thermostat"]["setpoint_low"]
+
     @property
     def hvac_mode(self) -> HVACMode:
-        """Return HVAC operation ie. heat, cool mode."""
+        """Return HVAC operation ie. auto, heat, or heat_cool mode."""
         if (mode := self.device.get("mode")) is None or mode not in self.hvac_modes:
             return HVACMode.HEAT
         return HVACMode(mode)
@@ -127,12 +153,21 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity):
     @plugwise_command
     async def async_set_temperature(self, **kwargs: Any) -> None:
         """Set new target temperature."""
-        if ((temperature := kwargs.get(ATTR_TEMPERATURE)) is None) or not (
-            self._attr_min_temp <= temperature <= self._attr_max_temp
-        ):
-            raise ValueError("Invalid temperature change requested")
-
-        await self.coordinator.api.set_temperature(self.device["location"], temperature)
+        data: dict[str, Any] = {}
+        if ATTR_TEMPERATURE in kwargs:
+            data["setpoint"] = kwargs.get(ATTR_TEMPERATURE)
+        if ATTR_TARGET_TEMP_HIGH in kwargs:
+            data["setpoint_high"] = kwargs.get(ATTR_TARGET_TEMP_HIGH)
+        if ATTR_TARGET_TEMP_LOW in kwargs:
+            data["setpoint_low"] = kwargs.get(ATTR_TARGET_TEMP_LOW)
+
+        for temperature in data.values():
+            if temperature is None or not (
+                self._attr_min_temp <= temperature <= self._attr_max_temp
+            ):
+                raise ValueError("Invalid temperature change requested")
+
+        await self.coordinator.api.set_temperature(self.device["location"], data)
 
     @plugwise_command
     async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json
index f49e7b7c508..43da399a8f2 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.25.0"],
+  "requirements": ["plugwise==0.25.2"],
   "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
   "zeroconf": ["_plugwise._tcp.local."],
   "config_flow": true,
diff --git a/requirements_all.txt b/requirements_all.txt
index 774942a9074..921c816ffca 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.25.0
+plugwise==0.25.2
 
 # homeassistant.components.plum_lightpad
 plumlightpad==0.0.11
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 2f49c44d769..8762340a1e6 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.25.0
+plugwise==0.25.2
 
 # homeassistant.components.plum_lightpad
 plumlightpad==0.0.11
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 be2dd05011b..546a11b2c68 100644
--- a/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json
+++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/all_data.json
@@ -68,7 +68,6 @@
       "thermostat": {
         "setpoint_low": 20.5,
         "setpoint_high": 24.0,
-        "setpoint": 20.5,
         "lower_bound": 4.0,
         "upper_bound": 30.0,
         "resolution": 0.1
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 246ae5dff50..ae9a16b7d1a 100644
--- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json
+++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json
@@ -14,7 +14,6 @@
       "name": "Anna",
       "vendor": "Plugwise",
       "thermostat": {
-        "setpoint": 20.0,
         "setpoint_low": 20.0,
         "setpoint_high": 23.5,
         "lower_bound": 1.0,
@@ -28,10 +27,9 @@
       "selected_schedule": "None",
       "last_used": "Weekschema",
       "control_state": "cooling",
-      "mode": "cool",
+      "mode": "heat_cool",
       "sensors": {
         "temperature": 25.8,
-        "setpoint": 20.0,
         "setpoint_low": 20.0,
         "setpoint_high": 23.5
       }
@@ -63,7 +61,6 @@
       "zigbee_mac_address": "ABCD012345670A04",
       "vendor": "Plugwise",
       "thermostat": {
-        "setpoint": 19.0,
         "setpoint_low": 19.0,
         "setpoint_high": 25.0,
         "lower_bound": 0.0,
@@ -81,7 +78,6 @@
       "sensors": {
         "temperature": 239,
         "battery": 56,
-        "setpoint": 20.0,
         "setpoint_low": 20.0,
         "setpoint_high": 23.5
       }
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 d6d34801641..6326a02fedb 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
@@ -66,7 +66,6 @@
       "name": "Anna",
       "vendor": "Plugwise",
       "thermostat": {
-        "setpoint": 24.0,
         "setpoint_low": 20.5,
         "setpoint_high": 24.0,
         "lower_bound": 4.0,
@@ -85,7 +84,6 @@
         "illuminance": 86.0,
         "cooling_activation_outdoor_temperature": 21.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 ca9559ca073..cd2747f423b 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
@@ -66,7 +66,6 @@
       "name": "Anna",
       "vendor": "Plugwise",
       "thermostat": {
-        "setpoint": 20.5,
         "setpoint_low": 20.5,
         "setpoint_high": 24.0,
         "lower_bound": 4.0,
@@ -85,7 +84,6 @@
         "illuminance": 86.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/test_climate.py b/tests/components/plugwise/test_climate.py
index bcca1a32abb..ad5443a678c 100644
--- a/tests/components/plugwise/test_climate.py
+++ b/tests/components/plugwise/test_climate.py
@@ -75,11 +75,10 @@ async def test_adam_3_climate_entity_attributes(
     state = hass.states.get("climate.anna")
 
     assert state
-    assert state.state == HVACMode.COOL
+    assert state.state == HVACMode.HEAT_COOL
     assert state.attributes["hvac_action"] == "cooling"
     assert state.attributes["hvac_modes"] == [
-        HVACMode.HEAT,
-        HVACMode.COOL,
+        HVACMode.HEAT_COOL,
         HVACMode.AUTO,
     ]
 
@@ -133,7 +132,7 @@ async def test_adam_climate_entity_climate_changes(
 
     assert mock_smile_adam.set_temperature.call_count == 1
     mock_smile_adam.set_temperature.assert_called_with(
-        "c50f167537524366a5af7aa3942feb1e", 25.0
+        "c50f167537524366a5af7aa3942feb1e", {"setpoint": 25.0}
     )
 
     with pytest.raises(ValueError):
@@ -165,7 +164,7 @@ async def test_adam_climate_entity_climate_changes(
 
     assert mock_smile_adam.set_temperature.call_count == 2
     mock_smile_adam.set_temperature.assert_called_with(
-        "82fa13f017d240daa0d0ea1775420f24", 25.0
+        "82fa13f017d240daa0d0ea1775420f24", {"setpoint": 25.0}
     )
 
     await hass.services.async_call(
@@ -203,8 +202,7 @@ async def test_anna_climate_entity_attributes(
     assert state.state == HVACMode.AUTO
     assert state.attributes["hvac_action"] == "heating"
     assert state.attributes["hvac_modes"] == [
-        HVACMode.HEAT,
-        HVACMode.COOL,
+        HVACMode.HEAT_COOL,
         HVACMode.AUTO,
     ]
 
@@ -213,8 +211,9 @@ async def test_anna_climate_entity_attributes(
 
     assert state.attributes["current_temperature"] == 19.3
     assert state.attributes["preset_mode"] == "home"
-    assert state.attributes["supported_features"] == 17
-    assert state.attributes["temperature"] == 20.5
+    assert state.attributes["supported_features"] == 18
+    assert state.attributes["target_temp_high"] == 24.0
+    assert state.attributes["target_temp_low"] == 20.5
     assert state.attributes["min_temp"] == 4.0
     assert state.attributes["max_temp"] == 30.0
     assert state.attributes["target_temp_step"] == 0.1
@@ -231,12 +230,12 @@ async def test_anna_2_climate_entity_attributes(
     assert state.state == HVACMode.AUTO
     assert state.attributes["hvac_action"] == "cooling"
     assert state.attributes["hvac_modes"] == [
-        HVACMode.HEAT,
-        HVACMode.COOL,
+        HVACMode.HEAT_COOL,
         HVACMode.AUTO,
     ]
-    assert state.attributes["temperature"] == 24.0
-    assert state.attributes["supported_features"] == 17
+    assert state.attributes["supported_features"] == 18
+    assert state.attributes["target_temp_high"] == 24.0
+    assert state.attributes["target_temp_low"] == 20.5
 
 
 async def test_anna_3_climate_entity_attributes(
@@ -250,8 +249,7 @@ async def test_anna_3_climate_entity_attributes(
     assert state.state == HVACMode.AUTO
     assert state.attributes["hvac_action"] == "idle"
     assert state.attributes["hvac_modes"] == [
-        HVACMode.HEAT,
-        HVACMode.COOL,
+        HVACMode.HEAT_COOL,
         HVACMode.AUTO,
     ]
 
@@ -263,14 +261,14 @@ async def test_anna_climate_entity_climate_changes(
     await hass.services.async_call(
         "climate",
         "set_temperature",
-        {"entity_id": "climate.anna", "temperature": 25},
+        {"entity_id": "climate.anna", "target_temp_high": 25, "target_temp_low": 20},
         blocking=True,
     )
 
     assert mock_smile_anna.set_temperature.call_count == 1
     mock_smile_anna.set_temperature.assert_called_with(
         "c784ee9fdab44e1395b8dee7d7a497d5",
-        25.0,
+        {"setpoint_high": 25.0, "setpoint_low": 20.0},
     )
 
     await hass.services.async_call(
@@ -288,7 +286,7 @@ async def test_anna_climate_entity_climate_changes(
     await hass.services.async_call(
         "climate",
         "set_hvac_mode",
-        {"entity_id": "climate.anna", "hvac_mode": "heat"},
+        {"entity_id": "climate.anna", "hvac_mode": "heat_cool"},
         blocking=True,
     )
 
-- 
GitLab


From a7f33535c7af8261bd4cbd9321040bd7455d7237 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Sun, 16 Oct 2022 11:45:27 -0700
Subject: [PATCH 522/985] Fix google calendar event transparency filter
 (#80438)

---
 homeassistant/components/google/calendar.py | 3 ++-
 tests/components/google/test_calendar.py    | 6 ++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py
index 77ed922e511..995fb1ec98f 100644
--- a/homeassistant/components/google/calendar.py
+++ b/homeassistant/components/google/calendar.py
@@ -359,7 +359,8 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
     def _apply_coordinator_update(self) -> None:
         """Copy state from the coordinator to this entity."""
         events = self.coordinator.data
-        self._event = _get_calendar_event(next(iter(events))) if events else None
+        api_event = next(filter(self._event_filter, iter(events)), None)
+        self._event = _get_calendar_event(api_event) if api_event else None
         if self._event:
             (self._event.summary, self._offset_value) = extract_offset(
                 self._event.summary, self._offset
diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py
index f4129eb0926..49bfa8e1d72 100644
--- a/tests/components/google/test_calendar.py
+++ b/tests/components/google/test_calendar.py
@@ -24,6 +24,7 @@ from .conftest import (
     TEST_API_ENTITY,
     TEST_API_ENTITY_NAME,
     TEST_YAML_ENTITY,
+    TEST_YAML_ENTITY_NAME,
 )
 
 from tests.common import async_fire_time_changed
@@ -577,6 +578,11 @@ async def test_opaque_event(
     events = await response.json()
     assert (len(events) > 0) == expect_visible_event
 
+    # Verify entity state for upcoming event
+    state = hass.states.get(TEST_YAML_ENTITY)
+    assert state.name == TEST_YAML_ENTITY_NAME
+    assert state.state == (STATE_ON if expect_visible_event else STATE_OFF)
+
 
 @pytest.mark.parametrize("mock_test_setup", [None])
 async def test_scan_calendar_error(
-- 
GitLab


From e71bd2c20b485fbccce9243cfa20191a6d617362 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Sun, 16 Oct 2022 11:45:57 -0700
Subject: [PATCH 523/985] Fix google calendar test to match API behavior
 (#80436)

---
 tests/components/google/test_calendar.py | 14 +++-----------
 1 file changed, 3 insertions(+), 11 deletions(-)

diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py
index 49bfa8e1d72..778e4a843eb 100644
--- a/tests/components/google/test_calendar.py
+++ b/tests/components/google/test_calendar.py
@@ -85,15 +85,6 @@ def upcoming() -> dict[str, Any]:
     }
 
 
-def upcoming_date() -> dict[str, Any]:
-    """Create a test event with an arbitrary start/end date fetched from the api url."""
-    now = dt_util.now()
-    return {
-        "start": {"date": now.date().isoformat()},
-        "end": {"date": now.date().isoformat()},
-    }
-
-
 def upcoming_event_url(entity: str = TEST_ENTITY) -> str:
     """Return a calendar API to return events created by upcoming()."""
     now = dt_util.now()
@@ -463,7 +454,8 @@ async def test_http_api_all_day_event(
     """Test querying the API and fetching events from the server."""
     event = {
         **TEST_EVENT,
-        **upcoming_date(),
+        "start": {"date": "2022-03-27"},
+        "end": {"date": "2022-03-28"},
     }
     mock_events_list_items([event])
     assert await component_setup()
@@ -476,7 +468,7 @@ async def test_http_api_all_day_event(
     assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == {
         "summary": TEST_EVENT["summary"],
         "start": {"date": "2022-03-27"},
-        "end": {"date": "2022-03-27"},
+        "end": {"date": "2022-03-28"},
     }
 
 
-- 
GitLab


From 9eb4faf0375f0e12e2f0d34aafdf9372bf9e43e2 Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
Date: Sun, 16 Oct 2022 21:49:12 +0200
Subject: [PATCH 524/985] Fire bluetooth listener for all matching devices
 (#80440)

* Fire listener for all matching devices

* Add test case for seen device

* Avoid looping all data if we have address match

* Initialize to empty list
---
 homeassistant/components/bluetooth/manager.py | 22 +++++++++++--------
 tests/components/bluetooth/test_init.py       | 12 +++++++++-
 2 files changed, 24 insertions(+), 10 deletions(-)

diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index 9c7954eb86a..dbb165f7cd9 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -483,15 +483,19 @@ class BluetoothManager:
         # immediately with the last packet so the subscriber can see the
         # device.
         all_history = self._get_history_by_type(connectable)
-        if (
-            (address := callback_matcher.get(ADDRESS))
-            and (service_info := all_history.get(address))
-            and ble_device_matches(callback_matcher, service_info)
-        ):
-            try:
-                callback(service_info, BluetoothChange.ADVERTISEMENT)
-            except Exception:  # pylint: disable=broad-except
-                _LOGGER.exception("Error in bluetooth callback")
+        service_infos: Iterable[BluetoothServiceInfoBleak] = []
+        if address := callback_matcher.get(ADDRESS):
+            if service_info := all_history.get(address):
+                service_infos = [service_info]
+        else:
+            service_infos = all_history.values()
+
+        for service_info in service_infos:
+            if ble_device_matches(callback_matcher, service_info):
+                try:
+                    callback(service_info, BluetoothChange.ADVERTISEMENT)
+                except Exception:  # pylint: disable=broad-except
+                    _LOGGER.exception("Error in bluetooth callback")
 
         return _async_remove_callback
 
diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py
index 495877118e7..c9a5e6c78a7 100644
--- a/tests/components/bluetooth/test_init.py
+++ b/tests/components/bluetooth/test_init.py
@@ -1089,6 +1089,16 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo
         hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
         await hass.async_block_till_done()
 
+        seen_switchbot_device = BLEDevice("44:44:33:11:23:46", "wohand")
+        seen_switchbot_adv = generate_advertisement_data(
+            local_name="wohand",
+            service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+            manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
+            service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"},
+        )
+
+        inject_advertisement(hass, seen_switchbot_device, seen_switchbot_adv)
+
         cancel = bluetooth.async_register_callback(
             hass,
             _fake_subscriber,
@@ -1125,7 +1135,7 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo
         inject_advertisement(hass, empty_device, empty_adv)
         await hass.async_block_till_done()
 
-    assert len(callbacks) == 1
+    assert len(callbacks) == 2
 
     service_info: BluetoothServiceInfo = callbacks[0][0]
     assert service_info.name == "wohand"
-- 
GitLab


From b4a203c3c706d098db535f9bb1d975cd120a018c Mon Sep 17 00:00:00 2001
From: TheJulianJES <TheJulianJES@users.noreply.github.com>
Date: Mon, 17 Oct 2022 01:57:24 +0200
Subject: [PATCH 525/985] Add friendly name to ZHA identify button (#80446)

---
 homeassistant/components/zha/button.py     |   3 +-
 tests/components/zha/test_device_action.py |   2 +-
 tests/components/zha/zha_devices_list.py   | 356 ++++++++++-----------
 3 files changed, 181 insertions(+), 180 deletions(-)

diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py
index cb0463a855f..41f3846e97f 100644
--- a/homeassistant/components/zha/button.py
+++ b/homeassistant/components/zha/button.py
@@ -109,6 +109,7 @@ class ZHAIdentifyButton(ZHAButton):
 
     _attr_device_class: ButtonDeviceClass = ButtonDeviceClass.UPDATE
     _attr_entity_category = EntityCategory.DIAGNOSTIC
+    _attr_name = "Identify"
     _command_name = "identify"
 
     def get_args(self) -> list[Any]:
@@ -118,7 +119,7 @@ class ZHAIdentifyButton(ZHAButton):
 
 
 class ZHAAttributeButton(ZhaEntity, ButtonEntity):
-    """Defines a ZHA button, which stes value to an attribute."""
+    """Defines a ZHA button, which writes a value to an attribute."""
 
     _attribute_name: str
     _attribute_value: Any = None
diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py
index a5e23d420d9..584abbaecdb 100644
--- a/tests/components/zha/test_device_action.py
+++ b/tests/components/zha/test_device_action.py
@@ -183,7 +183,7 @@ async def test_get_inovelli_actions(hass, device_inovelli):
         {
             "device_id": inovelli_reg_device.id,
             "domain": Platform.BUTTON,
-            "entity_id": "button.inovelli_vzm31_sn_identifybutton",
+            "entity_id": "button.inovelli_vzm31_sn_identify",
             "metadata": {"secondary": True},
             "type": "press",
         },
diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py
index 72ce080781d..caa3da9ceef 100644
--- a/tests/components/zha/zha_devices_list.py
+++ b/tests/components/zha/zha_devices_list.py
@@ -38,7 +38,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008"],
         DEV_SIG_ENTITIES: [
-            "button.adurolight_adurolight_ncc_identifybutton",
+            "button.adurolight_adurolight_ncc_identify",
             "sensor.adurolight_adurolight_ncc_rssi",
             "sensor.adurolight_adurolight_ncc_lqi",
         ],
@@ -46,7 +46,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -76,7 +76,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["5:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.bosch_isw_zpr1_wp13_identifybutton",
+            "button.bosch_isw_zpr1_wp13_identify",
             "sensor.bosch_isw_zpr1_wp13_battery",
             "sensor.bosch_isw_zpr1_wp13_temperature",
             "binary_sensor.bosch_isw_zpr1_wp13_iaszone",
@@ -92,7 +92,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-5-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-5-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -132,7 +132,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.centralite_3130_identifybutton",
+            "button.centralite_3130_identify",
             "sensor.centralite_3130_battery",
             "sensor.centralite_3130_rssi",
             "sensor.centralite_3130_lqi",
@@ -141,7 +141,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.centralite_3130_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.centralite_3130_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -176,7 +176,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.centralite_3210_l_identifybutton",
+            "button.centralite_3210_l_identify",
             "sensor.centralite_3210_l_active_power",
             "sensor.centralite_3210_l_apparent_power",
             "sensor.centralite_3210_l_rms_current",
@@ -198,7 +198,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -268,7 +268,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.centralite_3310_s_identifybutton",
+            "button.centralite_3310_s_identify",
             "sensor.centralite_3310_s_battery",
             "sensor.centralite_3310_s_temperature",
             "sensor.centralite_3310_s_humidity",
@@ -279,7 +279,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -331,7 +331,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.centralite_3315_s_identifybutton",
+            "button.centralite_3315_s_identify",
             "sensor.centralite_3315_s_battery",
             "sensor.centralite_3315_s_temperature",
             "binary_sensor.centralite_3315_s_iaszone",
@@ -347,7 +347,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -394,7 +394,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.centralite_3320_l_identifybutton",
+            "button.centralite_3320_l_identify",
             "sensor.centralite_3320_l_battery",
             "sensor.centralite_3320_l_temperature",
             "binary_sensor.centralite_3320_l_iaszone",
@@ -410,7 +410,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -457,7 +457,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.centralite_3326_l_identifybutton",
+            "button.centralite_3326_l_identify",
             "sensor.centralite_3326_l_battery",
             "sensor.centralite_3326_l_temperature",
             "binary_sensor.centralite_3326_l_iaszone",
@@ -473,7 +473,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -520,7 +520,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.centralite_motion_sensor_a_identifybutton",
+            "button.centralite_motion_sensor_a_identify",
             "sensor.centralite_motion_sensor_a_battery",
             "sensor.centralite_motion_sensor_a_temperature",
             "binary_sensor.centralite_motion_sensor_a_iaszone",
@@ -537,7 +537,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -589,7 +589,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["4:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton",
+            "button.climaxtechnology_psmp5_00_00_02_02tc_identify",
             "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",
@@ -605,7 +605,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
                 DEV_SIG_CHANNELS: ["smartenergy_metering"],
@@ -645,7 +645,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton",
+            "button.climaxtechnology_sd8sc_00_00_03_12tc_identify",
             "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",
@@ -664,7 +664,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -719,7 +719,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton",
+            "button.climaxtechnology_ws15_00_00_03_03tc_identify",
             "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_iaszone",
             "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi",
             "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi",
@@ -733,7 +733,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -770,7 +770,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton",
+            "button.feibit_inc_co_fb56_zcw08ku1_1_identify",
             "light.feibit_inc_co_fb56_zcw08ku1_1_light",
             "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi",
             "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi",
@@ -784,7 +784,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-11-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-11-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -814,7 +814,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.heiman_smokesensor_em_identifybutton",
+            "button.heiman_smokesensor_em_identify",
             "sensor.heiman_smokesensor_em_battery",
             "binary_sensor.heiman_smokesensor_em_iaszone",
             "sensor.heiman_smokesensor_em_rssi",
@@ -834,7 +834,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -894,7 +894,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.heiman_co_v16_identifybutton",
+            "button.heiman_co_v16_identify",
             "binary_sensor.heiman_co_v16_iaszone",
             "sensor.heiman_co_v16_rssi",
             "sensor.heiman_co_v16_lqi",
@@ -908,7 +908,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -938,7 +938,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.heiman_warningdevice_identifybutton",
+            "button.heiman_warningdevice_identify",
             "binary_sensor.heiman_warningdevice_iaszone",
             "sensor.heiman_warningdevice_rssi",
             "sensor.heiman_warningdevice_lqi",
@@ -982,7 +982,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1012,7 +1012,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["6:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.hivehome_com_mot003_identifybutton",
+            "button.hivehome_com_mot003_identify",
             "sensor.hivehome_com_mot003_battery",
             "sensor.hivehome_com_mot003_illuminance",
             "sensor.hivehome_com_mot003_temperature",
@@ -1029,7 +1029,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-6-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-6-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -1081,7 +1081,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton",
+            "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identify",
             "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light",
             "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi",
             "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi",
@@ -1095,7 +1095,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1125,7 +1125,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton",
+            "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identify",
             "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi",
@@ -1139,7 +1139,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1169,7 +1169,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton",
+            "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identify",
             "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi",
@@ -1183,7 +1183,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1213,7 +1213,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton",
+            "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identify",
             "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi",
@@ -1227,7 +1227,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1257,7 +1257,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton",
+            "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identify",
             "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi",
             "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi",
@@ -1271,7 +1271,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1301,7 +1301,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_control_outlet_identifybutton",
+            "button.ikea_of_sweden_tradfri_control_outlet_identify",
             "switch.ikea_of_sweden_tradfri_control_outlet_switch",
             "sensor.ikea_of_sweden_tradfri_control_outlet_rssi",
             "sensor.ikea_of_sweden_tradfri_control_outlet_lqi",
@@ -1315,7 +1315,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1345,7 +1345,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton",
+            "button.ikea_of_sweden_tradfri_motion_sensor_identify",
             "sensor.ikea_of_sweden_tradfri_motion_sensor_battery",
             "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion",
             "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi",
@@ -1355,7 +1355,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -1395,7 +1395,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton",
+            "button.ikea_of_sweden_tradfri_on_off_switch_identify",
             "sensor.ikea_of_sweden_tradfri_on_off_switch_battery",
             "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi",
             "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi",
@@ -1404,7 +1404,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -1439,7 +1439,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_remote_control_identifybutton",
+            "button.ikea_of_sweden_tradfri_remote_control_identify",
             "sensor.ikea_of_sweden_tradfri_remote_control_battery",
             "sensor.ikea_of_sweden_tradfri_remote_control_rssi",
             "sensor.ikea_of_sweden_tradfri_remote_control_lqi",
@@ -1448,7 +1448,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -1490,7 +1490,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton",
+            "button.ikea_of_sweden_tradfri_signal_repeater_identify",
             "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi",
             "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi",
         ],
@@ -1498,7 +1498,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1528,7 +1528,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton",
+            "button.ikea_of_sweden_tradfri_wireless_dimmer_identify",
             "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery",
             "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi",
             "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi",
@@ -1537,7 +1537,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -1579,7 +1579,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"],
         DEV_SIG_ENTITIES: [
-            "button.jasco_products_45852_identifybutton",
+            "button.jasco_products_45852_identify",
             "sensor.jasco_products_45852_instantaneous_demand",
             "sensor.jasco_products_45852_summation_delivered",
             "light.jasco_products_45852_light",
@@ -1595,7 +1595,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
                 DEV_SIG_CHANNELS: ["smartenergy_metering"],
@@ -1642,7 +1642,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"],
         DEV_SIG_ENTITIES: [
-            "button.jasco_products_45856_identifybutton",
+            "button.jasco_products_45856_identify",
             "light.jasco_products_45856_light",
             "sensor.jasco_products_45856_instantaneous_demand",
             "sensor.jasco_products_45856_summation_delivered",
@@ -1658,7 +1658,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
                 DEV_SIG_CHANNELS: ["smartenergy_metering"],
@@ -1705,7 +1705,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"],
         DEV_SIG_ENTITIES: [
-            "button.jasco_products_45857_identifybutton",
+            "button.jasco_products_45857_identify",
             "light.jasco_products_45857_light",
             "sensor.jasco_products_45857_instantaneous_demand",
             "sensor.jasco_products_45857_summation_delivered",
@@ -1721,7 +1721,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
                 DEV_SIG_CHANNELS: ["smartenergy_metering"],
@@ -1761,7 +1761,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.keen_home_inc_sv02_610_mp_1_3_identifybutton",
+            "button.keen_home_inc_sv02_610_mp_1_3_identify",
             "sensor.keen_home_inc_sv02_610_mp_1_3_battery",
             "sensor.keen_home_inc_sv02_610_mp_1_3_pressure",
             "sensor.keen_home_inc_sv02_610_mp_1_3_temperature",
@@ -1773,7 +1773,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_identify",
             },
             ("cover", "00:11:22:33:44:55:66:77-1"): {
                 DEV_SIG_CHANNELS: ["level", "on_off"],
@@ -1823,7 +1823,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.keen_home_inc_sv02_612_mp_1_2_identifybutton",
+            "button.keen_home_inc_sv02_612_mp_1_2_identify",
             "sensor.keen_home_inc_sv02_612_mp_1_2_battery",
             "sensor.keen_home_inc_sv02_612_mp_1_2_pressure",
             "sensor.keen_home_inc_sv02_612_mp_1_2_temperature",
@@ -1835,7 +1835,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_identify",
             },
             ("cover", "00:11:22:33:44:55:66:77-1"): {
                 DEV_SIG_CHANNELS: ["level", "on_off"],
@@ -1885,7 +1885,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.keen_home_inc_sv02_612_mp_1_3_identifybutton",
+            "button.keen_home_inc_sv02_612_mp_1_3_identify",
             "sensor.keen_home_inc_sv02_612_mp_1_3_battery",
             "sensor.keen_home_inc_sv02_612_mp_1_3_pressure",
             "sensor.keen_home_inc_sv02_612_mp_1_3_temperature",
@@ -1897,7 +1897,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_identify",
             },
             ("cover", "00:11:22:33:44:55:66:77-1"): {
                 DEV_SIG_CHANNELS: ["level", "on_off"],
@@ -1947,7 +1947,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.king_of_fans_inc_hbuniversalcfremote_identifybutton",
+            "button.king_of_fans_inc_hbuniversalcfremote_identify",
             "light.king_of_fans_inc_hbuniversalcfremote_light",
             "fan.king_of_fans_inc_hbuniversalcfremote_fan",
             "sensor.king_of_fans_inc_hbuniversalcfremote_rssi",
@@ -1962,7 +1962,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -1997,7 +1997,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"],
         DEV_SIG_ENTITIES: [
-            "button.lds_zbt_cctswitch_d0001_identifybutton",
+            "button.lds_zbt_cctswitch_d0001_identify",
             "sensor.lds_zbt_cctswitch_d0001_battery",
             "sensor.lds_zbt_cctswitch_d0001_rssi",
             "sensor.lds_zbt_cctswitch_d0001_lqi",
@@ -2006,7 +2006,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -2041,7 +2041,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ledvance_a19_rgbw_identifybutton",
+            "button.ledvance_a19_rgbw_identify",
             "light.ledvance_a19_rgbw_light",
             "sensor.ledvance_a19_rgbw_rssi",
             "sensor.ledvance_a19_rgbw_lqi",
@@ -2055,7 +2055,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2085,7 +2085,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ledvance_flex_rgbw_identifybutton",
+            "button.ledvance_flex_rgbw_identify",
             "light.ledvance_flex_rgbw_light",
             "sensor.ledvance_flex_rgbw_rssi",
             "sensor.ledvance_flex_rgbw_lqi",
@@ -2099,7 +2099,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2129,7 +2129,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ledvance_plug_identifybutton",
+            "button.ledvance_plug_identify",
             "switch.ledvance_plug_switch",
             "sensor.ledvance_plug_rssi",
             "sensor.ledvance_plug_lqi",
@@ -2143,7 +2143,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2173,7 +2173,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.ledvance_rt_rgbw_identifybutton",
+            "button.ledvance_rt_rgbw_identify",
             "light.ledvance_rt_rgbw_light",
             "sensor.ledvance_rt_rgbw_rssi",
             "sensor.ledvance_rt_rgbw_lqi",
@@ -2187,7 +2187,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2238,7 +2238,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_plug_maus01_identifybutton",
+            "button.lumi_lumi_plug_maus01_identify",
             "sensor.lumi_lumi_plug_maus01_active_power",
             "sensor.lumi_lumi_plug_maus01_apparent_power",
             "sensor.lumi_lumi_plug_maus01_rms_current",
@@ -2267,7 +2267,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -2349,7 +2349,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_relay_c2acn01_identifybutton",
+            "button.lumi_lumi_relay_c2acn01_identify",
             "light.lumi_lumi_relay_c2acn01_light",
             "light.lumi_lumi_relay_c2acn01_light_2",
             "sensor.lumi_lumi_relay_c2acn01_active_power",
@@ -2376,7 +2376,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -2455,7 +2455,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_remote_b186acn01_identifybutton",
+            "button.lumi_lumi_remote_b186acn01_identify",
             "sensor.lumi_lumi_remote_b186acn01_battery",
             "sensor.lumi_lumi_remote_b186acn01_rssi",
             "sensor.lumi_lumi_remote_b186acn01_lqi",
@@ -2464,7 +2464,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -2513,7 +2513,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_remote_b286acn01_identifybutton",
+            "button.lumi_lumi_remote_b286acn01_identify",
             "sensor.lumi_lumi_remote_b286acn01_battery",
             "sensor.lumi_lumi_remote_b286acn01_rssi",
             "sensor.lumi_lumi_remote_b286acn01_lqi",
@@ -2522,7 +2522,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -2592,7 +2592,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_remote_b286opcn01_identifybutton",
+            "button.lumi_lumi_remote_b286opcn01_identify",
             "sensor.lumi_lumi_remote_b286opcn01_rssi",
             "sensor.lumi_lumi_remote_b286opcn01_lqi",
         ],
@@ -2600,7 +2600,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2665,7 +2665,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_remote_b486opcn01_identifybutton",
+            "button.lumi_lumi_remote_b486opcn01_identify",
             "sensor.lumi_lumi_remote_b486opcn01_rssi",
             "sensor.lumi_lumi_remote_b486opcn01_lqi",
         ],
@@ -2673,7 +2673,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2703,7 +2703,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_remote_b686opcn01_identifybutton",
+            "button.lumi_lumi_remote_b686opcn01_identify",
             "sensor.lumi_lumi_remote_b686opcn01_rssi",
             "sensor.lumi_lumi_remote_b686opcn01_lqi",
         ],
@@ -2711,7 +2711,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2776,7 +2776,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_remote_b686opcn01_identifybutton",
+            "button.lumi_lumi_remote_b686opcn01_identify",
             "sensor.lumi_lumi_remote_b686opcn01_rssi",
             "sensor.lumi_lumi_remote_b686opcn01_lqi",
         ],
@@ -2784,7 +2784,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -2946,7 +2946,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sen_ill_mgl01_identifybutton",
+            "button.lumi_lumi_sen_ill_mgl01_identify",
             "sensor.lumi_lumi_sen_ill_mgl01_illuminance",
             "sensor.lumi_lumi_sen_ill_mgl01_rssi",
             "sensor.lumi_lumi_sen_ill_mgl01_lqi",
@@ -2955,7 +2955,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1024"): {
                 DEV_SIG_CHANNELS: ["illuminance"],
@@ -3004,7 +3004,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_86sw1_identifybutton",
+            "button.lumi_lumi_sensor_86sw1_identify",
             "sensor.lumi_lumi_sensor_86sw1_battery",
             "sensor.lumi_lumi_sensor_86sw1_rssi",
             "sensor.lumi_lumi_sensor_86sw1_lqi",
@@ -3013,7 +3013,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3062,7 +3062,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_cube_aqgl01_identifybutton",
+            "button.lumi_lumi_sensor_cube_aqgl01_identify",
             "sensor.lumi_lumi_sensor_cube_aqgl01_battery",
             "sensor.lumi_lumi_sensor_cube_aqgl01_rssi",
             "sensor.lumi_lumi_sensor_cube_aqgl01_lqi",
@@ -3071,7 +3071,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3120,7 +3120,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_ht_identifybutton",
+            "button.lumi_lumi_sensor_ht_identify",
             "sensor.lumi_lumi_sensor_ht_battery",
             "sensor.lumi_lumi_sensor_ht_temperature",
             "sensor.lumi_lumi_sensor_ht_humidity",
@@ -3131,7 +3131,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3176,7 +3176,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_magnet_identifybutton",
+            "button.lumi_lumi_sensor_magnet_identify",
             "sensor.lumi_lumi_sensor_magnet_battery",
             "binary_sensor.lumi_lumi_sensor_magnet_opening",
             "sensor.lumi_lumi_sensor_magnet_rssi",
@@ -3186,7 +3186,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3226,7 +3226,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_magnet_aq2_identifybutton",
+            "button.lumi_lumi_sensor_magnet_aq2_identify",
             "sensor.lumi_lumi_sensor_magnet_aq2_battery",
             "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening",
             "sensor.lumi_lumi_sensor_magnet_aq2_rssi",
@@ -3236,7 +3236,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3276,7 +3276,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_motion_aq2_identifybutton",
+            "button.lumi_lumi_sensor_motion_aq2_identify",
             "sensor.lumi_lumi_sensor_motion_aq2_battery",
             "sensor.lumi_lumi_sensor_motion_aq2_illuminance",
             "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy",
@@ -3298,7 +3298,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3338,7 +3338,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_smoke_identifybutton",
+            "button.lumi_lumi_sensor_smoke_identify",
             "sensor.lumi_lumi_sensor_smoke_battery",
             "binary_sensor.lumi_lumi_sensor_smoke_iaszone",
             "sensor.lumi_lumi_sensor_smoke_rssi",
@@ -3353,7 +3353,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3388,7 +3388,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_switch_identifybutton",
+            "button.lumi_lumi_sensor_switch_identify",
             "sensor.lumi_lumi_sensor_switch_battery",
             "sensor.lumi_lumi_sensor_switch_rssi",
             "sensor.lumi_lumi_sensor_switch_lqi",
@@ -3397,7 +3397,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3508,7 +3508,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_sensor_wleak_aq1_identifybutton",
+            "button.lumi_lumi_sensor_wleak_aq1_identify",
             "sensor.lumi_lumi_sensor_wleak_aq1_battery",
             "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone",
             "sensor.lumi_lumi_sensor_wleak_aq1_rssi",
@@ -3529,7 +3529,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3571,7 +3571,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005"],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_vibration_aq1_identifybutton",
+            "button.lumi_lumi_vibration_aq1_identify",
             "sensor.lumi_lumi_vibration_aq1_battery",
             "binary_sensor.lumi_lumi_vibration_aq1_iaszone",
             "lock.lumi_lumi_vibration_aq1_doorlock",
@@ -3587,7 +3587,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3627,7 +3627,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.lumi_lumi_weather_identifybutton",
+            "button.lumi_lumi_weather_identify",
             "sensor.lumi_lumi_weather_battery",
             "sensor.lumi_lumi_weather_pressure",
             "sensor.lumi_lumi_weather_temperature",
@@ -3639,7 +3639,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3689,7 +3689,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.nyce_3010_identifybutton",
+            "button.nyce_3010_identify",
             "sensor.nyce_3010_battery",
             "binary_sensor.nyce_3010_iaszone",
             "sensor.nyce_3010_rssi",
@@ -3704,7 +3704,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.nyce_3010_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.nyce_3010_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3739,7 +3739,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.nyce_3014_identifybutton",
+            "button.nyce_3014_identify",
             "sensor.nyce_3014_battery",
             "binary_sensor.nyce_3014_iaszone",
             "sensor.nyce_3014_rssi",
@@ -3754,7 +3754,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.nyce_3014_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.nyce_3014_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3832,7 +3832,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["3:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.osram_lightify_a19_rgbw_identifybutton",
+            "button.osram_lightify_a19_rgbw_identify",
             "light.osram_lightify_a19_rgbw_light",
             "sensor.osram_lightify_a19_rgbw_rssi",
             "sensor.osram_lightify_a19_rgbw_lqi",
@@ -3846,7 +3846,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-3-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -3876,7 +3876,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.osram_lightify_dimming_switch_identifybutton",
+            "button.osram_lightify_dimming_switch_identify",
             "sensor.osram_lightify_dimming_switch_battery",
             "sensor.osram_lightify_dimming_switch_rssi",
             "sensor.osram_lightify_dimming_switch_lqi",
@@ -3885,7 +3885,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -3920,7 +3920,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["3:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.osram_lightify_flex_rgbw_identifybutton",
+            "button.osram_lightify_flex_rgbw_identify",
             "light.osram_lightify_flex_rgbw_light",
             "sensor.osram_lightify_flex_rgbw_rssi",
             "sensor.osram_lightify_flex_rgbw_lqi",
@@ -3934,7 +3934,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-3-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -3964,7 +3964,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["3:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.osram_lightify_rt_tunable_white_identifybutton",
+            "button.osram_lightify_rt_tunable_white_identify",
             "light.osram_lightify_rt_tunable_white_light",
             "sensor.osram_lightify_rt_tunable_white_active_power",
             "sensor.osram_lightify_rt_tunable_white_apparent_power",
@@ -3984,7 +3984,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-3-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-3-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -4044,7 +4044,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["3:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.osram_plug_01_identifybutton",
+            "button.osram_plug_01_identify",
             "sensor.osram_plug_01_active_power",
             "sensor.osram_plug_01_apparent_power",
             "sensor.osram_plug_01_rms_current",
@@ -4064,7 +4064,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-3-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-3-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -4230,7 +4230,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.philips_rwl020_identifybutton",
+            "button.philips_rwl020_identify",
             "sensor.philips_rwl020_battery",
             "binary_sensor.philips_rwl020_binaryinput",
             "sensor.philips_rwl020_rssi",
@@ -4255,7 +4255,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-2-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-2-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -4280,7 +4280,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.samjin_button_identifybutton",
+            "button.samjin_button_identify",
             "sensor.samjin_button_battery",
             "sensor.samjin_button_temperature",
             "binary_sensor.samjin_button_iaszone",
@@ -4296,7 +4296,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.samjin_button_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.samjin_button_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -4336,7 +4336,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.samjin_multi_identifybutton",
+            "button.samjin_multi_identify",
             "sensor.samjin_multi_battery",
             "sensor.samjin_multi_temperature",
             "binary_sensor.samjin_multi_iaszone",
@@ -4352,7 +4352,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.samjin_multi_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.samjin_multi_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -4392,7 +4392,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.samjin_water_identifybutton",
+            "button.samjin_water_identify",
             "sensor.samjin_water_battery",
             "sensor.samjin_water_temperature",
             "binary_sensor.samjin_water_iaszone",
@@ -4408,7 +4408,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.samjin_water_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.samjin_water_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -4448,7 +4448,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.securifi_ltd_unk_model_identifybutton",
+            "button.securifi_ltd_unk_model_identify",
             "sensor.securifi_ltd_unk_model_active_power",
             "sensor.securifi_ltd_unk_model_apparent_power",
             "sensor.securifi_ltd_unk_model_rms_current",
@@ -4463,7 +4463,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -4528,7 +4528,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sercomm_corp_sz_dws04n_sf_identifybutton",
+            "button.sercomm_corp_sz_dws04n_sf_identify",
             "sensor.sercomm_corp_sz_dws04n_sf_battery",
             "sensor.sercomm_corp_sz_dws04n_sf_temperature",
             "binary_sensor.sercomm_corp_sz_dws04n_sf_iaszone",
@@ -4544,7 +4544,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -4591,7 +4591,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"],
         DEV_SIG_ENTITIES: [
-            "button.sercomm_corp_sz_esw01_identifybutton",
+            "button.sercomm_corp_sz_esw01_identify",
             "sensor.sercomm_corp_sz_esw01_active_power",
             "sensor.sercomm_corp_sz_esw01_apparent_power",
             "sensor.sercomm_corp_sz_esw01_rms_current",
@@ -4613,7 +4613,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -4683,7 +4683,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sercomm_corp_sz_pir04_identifybutton",
+            "button.sercomm_corp_sz_pir04_identify",
             "sensor.sercomm_corp_sz_pir04_battery",
             "sensor.sercomm_corp_sz_pir04_illuminance",
             "sensor.sercomm_corp_sz_pir04_temperature",
@@ -4700,7 +4700,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -4745,7 +4745,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sinope_technologies_rm3250zb_identifybutton",
+            "button.sinope_technologies_rm3250zb_identify",
             "sensor.sinope_technologies_rm3250zb_active_power",
             "sensor.sinope_technologies_rm3250zb_apparent_power",
             "sensor.sinope_technologies_rm3250zb_rms_current",
@@ -4760,7 +4760,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -4832,7 +4832,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sinope_technologies_th1123zb_identifybutton",
+            "button.sinope_technologies_th1123zb_identify",
             "sensor.sinope_technologies_th1123zb_active_power",
             "sensor.sinope_technologies_th1123zb_apparent_power",
             "sensor.sinope_technologies_th1123zb_rms_current",
@@ -4849,7 +4849,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_identify",
             },
             ("climate", "00:11:22:33:44:55:66:77-1"): {
                 DEV_SIG_CHANNELS: ["thermostat"],
@@ -4931,7 +4931,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sinope_technologies_th1124zb_identifybutton",
+            "button.sinope_technologies_th1124zb_identify",
             "sensor.sinope_technologies_th1124zb_active_power",
             "sensor.sinope_technologies_th1124zb_apparent_power",
             "sensor.sinope_technologies_th1124zb_rms_current",
@@ -4948,7 +4948,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_identify",
             },
             ("climate", "00:11:22:33:44:55:66:77-1"): {
                 DEV_SIG_CHANNELS: ["thermostat"],
@@ -5023,7 +5023,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.smartthings_outletv4_identifybutton",
+            "button.smartthings_outletv4_identify",
             "sensor.smartthings_outletv4_active_power",
             "sensor.smartthings_outletv4_apparent_power",
             "sensor.smartthings_outletv4_rms_current",
@@ -5044,7 +5044,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-2820"): {
                 DEV_SIG_CHANNELS: ["electrical_measurement"],
@@ -5109,7 +5109,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.smartthings_tagv4_identifybutton",
+            "button.smartthings_tagv4_identify",
             "device_tracker.smartthings_tagv4_devicescanner",
             "binary_sensor.smartthings_tagv4_binaryinput",
             "sensor.smartthings_tagv4_rssi",
@@ -5129,7 +5129,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -5159,7 +5159,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.third_reality_inc_3rss007z_identifybutton",
+            "button.third_reality_inc_3rss007z_identify",
             "switch.third_reality_inc_3rss007z_switch",
             "sensor.third_reality_inc_3rss007z_rssi",
             "sensor.third_reality_inc_3rss007z_lqi",
@@ -5168,7 +5168,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): {
                 DEV_SIG_CHANNELS: ["basic"],
@@ -5203,7 +5203,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.third_reality_inc_3rss008z_identifybutton",
+            "button.third_reality_inc_3rss008z_identify",
             "sensor.third_reality_inc_3rss008z_battery",
             "switch.third_reality_inc_3rss008z_switch",
             "sensor.third_reality_inc_3rss008z_rssi",
@@ -5213,7 +5213,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -5253,7 +5253,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.visonic_mct_340_e_identifybutton",
+            "button.visonic_mct_340_e_identify",
             "sensor.visonic_mct_340_e_battery",
             "sensor.visonic_mct_340_e_temperature",
             "binary_sensor.visonic_mct_340_e_iaszone",
@@ -5269,7 +5269,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -5309,7 +5309,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.zen_within_zen_01_identifybutton",
+            "button.zen_within_zen_01_identify",
             "sensor.zen_within_zen_01_battery",
             "sensor.zen_within_zen_01_hvac_action",
             "climate.zen_within_zen_01_zenwithinthermostat",
@@ -5320,7 +5320,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_identify",
             },
             ("climate", "00:11:22:33:44:55:66:77-1"): {
                 DEV_SIG_CHANNELS: ["thermostat", "fan"],
@@ -5442,7 +5442,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.netvox_z308e3ed_identifybutton",
+            "button.netvox_z308e3ed_identify",
             "sensor.netvox_z308e3ed_battery",
             "binary_sensor.netvox_z308e3ed_iaszone",
             "sensor.netvox_z308e3ed_rssi",
@@ -5457,7 +5457,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1"): {
                 DEV_SIG_CHANNELS: ["power"],
@@ -5492,7 +5492,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sengled_e11_g13_identifybutton",
+            "button.sengled_e11_g13_identify",
             "light.sengled_e11_g13_mintransitionlight",
             "sensor.sengled_e11_g13_instantaneous_demand",
             "sensor.sengled_e11_g13_summation_delivered",
@@ -5508,7 +5508,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
                 DEV_SIG_CHANNELS: ["smartenergy_metering"],
@@ -5548,7 +5548,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sengled_e12_n14_identifybutton",
+            "button.sengled_e12_n14_identify",
             "light.sengled_e12_n14_mintransitionlight",
             "sensor.sengled_e12_n14_instantaneous_demand",
             "sensor.sengled_e12_n14_summation_delivered",
@@ -5564,7 +5564,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
                 DEV_SIG_CHANNELS: ["smartenergy_metering"],
@@ -5604,7 +5604,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: ["1:0x0019"],
         DEV_SIG_ENTITIES: [
-            "button.sengled_z01_a19nae26_identifybutton",
+            "button.sengled_z01_a19nae26_identify",
             "light.sengled_z01_a19nae26_mintransitionlight",
             "sensor.sengled_z01_a19nae26_instantaneous_demand",
             "sensor.sengled_z01_a19nae26_summation_delivered",
@@ -5620,7 +5620,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_identify",
             },
             ("sensor", "00:11:22:33:44:55:66:77-1-1794"): {
                 DEV_SIG_CHANNELS: ["smartenergy_metering"],
@@ -5660,7 +5660,7 @@ DEVICES = [
         },
         DEV_SIG_EVT_CHANNELS: [],
         DEV_SIG_ENTITIES: [
-            "button.unk_manufacturer_unk_model_identifybutton",
+            "button.unk_manufacturer_unk_model_identify",
             "cover.unk_manufacturer_unk_model_shade",
             "sensor.unk_manufacturer_unk_model_rssi",
             "sensor.unk_manufacturer_unk_model_lqi",
@@ -5669,7 +5669,7 @@ DEVICES = [
             ("button", "00:11:22:33:44:55:66:77-1-3"): {
                 DEV_SIG_CHANNELS: ["identify"],
                 DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton",
-                DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_identifybutton",
+                DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_identify",
             },
             ("cover", "00:11:22:33:44:55:66:77-1"): {
                 DEV_SIG_CHANNELS: ["level", "on_off", "shade"],
-- 
GitLab


From ed4b5ee9d88e48e04f8bca3d9604b11a92193d25 Mon Sep 17 00:00:00 2001
From: Jo De Boeck <deboeck.jo@gmail.com>
Date: Mon, 17 Oct 2022 02:09:28 +0200
Subject: [PATCH 526/985] Bump androidtv dependency to 0.0.69 (#80407)

Bump dependency to version 0.0.69

Fixes: #69723
Signed-off-by: Jo De Boeck <deboeck.jo@gmail.com>

Signed-off-by: Jo De Boeck <deboeck.jo@gmail.com>
---
 homeassistant/components/androidtv/manifest.json | 2 +-
 requirements_all.txt                             | 2 +-
 requirements_test_all.txt                        | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json
index 92d4f806b39..6b1f5669345 100644
--- a/homeassistant/components/androidtv/manifest.json
+++ b/homeassistant/components/androidtv/manifest.json
@@ -4,7 +4,7 @@
   "documentation": "https://www.home-assistant.io/integrations/androidtv",
   "requirements": [
     "adb-shell[async]==0.4.3",
-    "androidtv[async]==0.0.67",
+    "androidtv[async]==0.0.69",
     "pure-python-adb[async]==0.3.0.dev0"
   ],
   "codeowners": ["@JeffLIrion", "@ollo69"],
diff --git a/requirements_all.txt b/requirements_all.txt
index 921c816ffca..bc413fa8817 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -315,7 +315,7 @@ ambiclimate==0.2.1
 amcrest==1.9.7
 
 # homeassistant.components.androidtv
-androidtv[async]==0.0.67
+androidtv[async]==0.0.69
 
 # homeassistant.components.anel_pwrctrl
 anel_pwrctrl-homeassistant==0.0.1.dev2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 8762340a1e6..b42a9157c31 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -284,7 +284,7 @@ amberelectric==1.0.4
 ambiclimate==0.2.1
 
 # homeassistant.components.androidtv
-androidtv[async]==0.0.67
+androidtv[async]==0.0.69
 
 # homeassistant.components.anthemav
 anthemav==1.4.1
-- 
GitLab


From 1d16e614b8d1ec01672eda4f316a879c5b19ae1b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 16 Oct 2022 19:09:56 -0500
Subject: [PATCH 527/985] Bump pySwitchbot to 0.20.2 (#80435)

* Bump pySwitchbot to 0.20.1

fixes #80427

* bump again to fix contact timeout sensor
---
 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 316adc6cd7a..532edac7d43 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.20.0"],
+  "requirements": ["PySwitchbot==0.20.2"],
   "config_flow": true,
   "dependencies": ["bluetooth"],
   "codeowners": [
diff --git a/requirements_all.txt b/requirements_all.txt
index bc413fa8817..ddcb00bceea 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
 PySocks==1.7.1
 
 # homeassistant.components.switchbot
-PySwitchbot==0.20.0
+PySwitchbot==0.20.2
 
 # homeassistant.components.transport_nsw
 PyTransportNSW==0.1.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b42a9157c31..a0e68550c99 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
 PySocks==1.7.1
 
 # homeassistant.components.switchbot
-PySwitchbot==0.20.0
+PySwitchbot==0.20.2
 
 # homeassistant.components.transport_nsw
 PyTransportNSW==0.1.1
-- 
GitLab


From 388328adba6dd0bb1485fa0dd01cf40ad49621dd Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Mon, 17 Oct 2022 00:36:39 +0000
Subject: [PATCH 528/985] [ci skip] Translation update

---
 homeassistant/components/airnow/translations/nb.json |  7 +++++++
 .../components/airthings/translations/nb.json        |  7 +++++++
 .../components/airthings_ble/translations/nb.json    |  3 ++-
 .../components/airvisual/translations/nb.json        |  7 +++++++
 .../components/amberelectric/translations/nb.json    |  7 +++++++
 .../components/androidtv/translations/nb.json        |  7 +++++++
 .../components/apple_tv/translations/nb.json         | 10 ++++++++++
 .../components/aseko_pool_live/translations/nb.json  |  7 +++++++
 .../components/asuswrt/translations/nb.json          |  7 +++++++
 homeassistant/components/august/translations/nb.json |  3 +++
 .../components/aussie_broadband/translations/nb.json |  8 ++++++++
 homeassistant/components/awair/translations/nb.json  |  7 +++++++
 .../components/azure_event_hub/translations/nb.json  |  7 +++++++
 homeassistant/components/baf/translations/nb.json    |  7 +++++++
 homeassistant/components/balboa/translations/nb.json |  7 +++++++
 homeassistant/components/blebox/translations/nb.json |  7 +++++++
 homeassistant/components/blink/translations/nb.json  |  7 +++++++
 homeassistant/components/bond/translations/nb.json   |  7 +++++++
 .../components/bosch_shc/translations/nb.json        |  7 +++++++
 .../components/broadlink/translations/nb.json        | 10 ++++++++++
 homeassistant/components/brunt/translations/nb.json  |  3 +++
 homeassistant/components/canary/translations/nb.json |  7 +++++++
 .../components/cloudflare/translations/nb.json       |  7 +++++++
 .../components/co2signal/translations/nb.json        | 10 ++++++++++
 .../components/coinbase/translations/nb.json         | 10 ++++++++++
 .../components/coinbase/translations/pt-BR.json      |  6 ++++++
 .../components/control4/translations/nb.json         |  7 +++++++
 .../components/crownstone/translations/nb.json       |  7 +++++++
 homeassistant/components/daikin/translations/nb.json |  7 +++++++
 .../devolo_home_network/translations/nb.json         |  3 +++
 homeassistant/components/dexcom/translations/nb.json |  7 +++++++
 .../components/directv/translations/nb.json          |  7 +++++++
 .../components/discord/translations/nb.json          |  7 +++++++
 .../components/doorbird/translations/nb.json         |  7 +++++++
 .../components/ecowitt/translations/nb.json          |  7 +++++++
 homeassistant/components/efergy/translations/nb.json |  7 +++++++
 homeassistant/components/elkm1/translations/nb.json  | 10 ++++++++++
 homeassistant/components/elmax/translations/nb.json  |  3 +++
 .../components/emonitor/translations/nb.json         |  7 +++++++
 .../components/enphase_envoy/translations/nb.json    |  3 +++
 .../environment_canada/translations/nb.json          |  7 +++++++
 .../components/evil_genius_labs/translations/nb.json |  7 +++++++
 homeassistant/components/ezviz/translations/nb.json  |  3 +++
 .../components/faa_delays/translations/nb.json       |  7 +++++++
 homeassistant/components/fibaro/translations/nb.json |  7 +++++++
 homeassistant/components/fivem/translations/nb.json  |  7 +++++++
 .../components/flick_electric/translations/nb.json   |  7 +++++++
 homeassistant/components/flipr/translations/nb.json  |  7 +++++++
 homeassistant/components/flo/translations/nb.json    |  7 +++++++
 homeassistant/components/flume/translations/nb.json  |  7 +++++++
 .../components/forked_daapd/translations/nb.json     |  7 +++++++
 homeassistant/components/foscam/translations/nb.json |  7 +++++++
 .../components/freebox/translations/nb.json          |  7 +++++++
 .../components/fronius/translations/nb.json          |  3 +++
 .../components/fully_kiosk/translations/nb.json      |  7 +++++++
 .../garages_amsterdam/translations/nb.json           |  7 +++++++
 .../components/generic/translations/nb.json          | 12 ++++++++++++
 .../components/goalzero/translations/nb.json         | 10 ++++++++++
 .../components/google_sheets/translations/nb.json    |  3 ++-
 .../google_travel_time/translations/es.json          |  3 ++-
 .../google_travel_time/translations/et.json          |  3 ++-
 .../google_travel_time/translations/no.json          |  3 ++-
 .../components/habitica/translations/nb.json         |  7 +++++++
 .../components/hangouts/translations/nb.json         |  7 +++++++
 .../components/harmony/translations/nb.json          |  7 +++++++
 .../components/here_travel_time/translations/nb.json |  7 +++++++
 homeassistant/components/hive/translations/nb.json   |  3 +++
 .../components/hlk_sw16/translations/nb.json         |  7 +++++++
 .../homematicip_cloud/translations/nb.json           |  7 +++++++
 .../components/homewizard/translations/nb.json       |  7 +++++++
 .../components/huawei_lte/translations/nb.json       |  3 +++
 homeassistant/components/hue/translations/nb.json    | 10 ++++++++++
 .../components/huisbaasje/translations/nb.json       |  7 +++++++
 .../hunterdouglas_powerview/translations/nb.json     |  7 +++++++
 homeassistant/components/ialarm/translations/nb.json |  7 +++++++
 .../components/iotawatt/translations/nb.json         |  3 +++
 homeassistant/components/isy994/translations/nb.json |  7 +++++++
 .../components/jellyfin/translations/nb.json         |  3 +++
 .../components/juicenet/translations/nb.json         |  7 +++++++
 .../components/justnimbus/translations/nb.json       |  7 +++++++
 .../components/kaleidescape/translations/nb.json     |  7 +++++++
 .../components/keymitt_ble/translations/nb.json      |  3 ++-
 .../components/kmtronic/translations/nb.json         |  3 +++
 homeassistant/components/kodi/translations/nb.json   | 10 ++++++++++
 .../components/konnected/translations/nb.json        |  7 +++++++
 .../kostal_plenticore/translations/nb.json           |  7 +++++++
 .../components/lacrosse_view/translations/nb.json    |  7 +++++++
 .../components/lametric/translations/nb.json         | 10 ++++++++++
 .../components/lametric/translations/pt-BR.json      |  2 ++
 .../landisgyr_heat_meter/translations/nb.json        |  7 +++++++
 .../components/laundrify/translations/nb.json        |  7 +++++++
 .../components/led_ble/translations/nb.json          |  7 +++++++
 homeassistant/components/lidarr/translations/nb.json |  3 ++-
 .../components/life360/translations/nb.json          | 10 ++++++++++
 .../components/litterrobot/translations/nb.json      |  3 +++
 homeassistant/components/lookin/translations/nb.json |  7 +++++++
 homeassistant/components/mazda/translations/nb.json  |  7 +++++++
 homeassistant/components/meater/translations/nb.json |  7 +++++++
 .../components/melcloud/translations/nb.json         |  7 +++++++
 .../components/meteo_france/translations/nb.json     |  7 +++++++
 .../components/meteoclimatic/translations/nb.json    |  7 +++++++
 .../components/metoffice/translations/nb.json        |  7 +++++++
 .../moehlenhoff_alpha2/translations/nb.json          |  7 +++++++
 .../components/monoprice/translations/nb.json        |  7 +++++++
 .../components/motioneye/translations/nb.json        |  7 +++++++
 .../components/mullvad/translations/nb.json          |  7 +++++++
 .../components/mutesync/translations/nb.json         |  7 +++++++
 homeassistant/components/myq/translations/nb.json    |  7 +++++++
 .../components/mysensors/translations/nb.json        | 10 ++++++++++
 homeassistant/components/nam/translations/nb.json    |  3 +++
 .../components/nanoleaf/translations/nb.json         | 10 ++++++++++
 homeassistant/components/nest/translations/nb.json   |  7 +++++++
 homeassistant/components/nexia/translations/nb.json  |  7 +++++++
 .../components/nextdns/translations/nb.json          |  7 +++++++
 .../components/nfandroidtv/translations/nb.json      |  7 +++++++
 .../components/nibe_heatpump/translations/nb.json    |  3 +++
 .../components/nightscout/translations/nb.json       |  7 +++++++
 homeassistant/components/nina/translations/nb.json   | 12 ++++++++++++
 .../components/nobo_hub/translations/nb.json         |  7 +++++++
 homeassistant/components/notion/translations/nb.json |  7 +++++++
 homeassistant/components/nuheat/translations/nb.json |  7 +++++++
 homeassistant/components/nuki/translations/nb.json   |  7 +++++++
 homeassistant/components/nut/translations/nb.json    |  7 +++++++
 homeassistant/components/nws/translations/nb.json    |  7 +++++++
 homeassistant/components/nzbget/translations/nb.json |  7 +++++++
 .../components/octoprint/translations/nb.json        |  6 ++++++
 .../components/omnilogic/translations/nb.json        |  7 +++++++
 homeassistant/components/oncue/translations/nb.json  |  3 +++
 .../openexchangerates/translations/nb.json           |  7 +++++++
 .../components/opengarage/translations/nb.json       |  7 +++++++
 .../components/overkiz/translations/nb.json          |  3 +++
 .../components/panasonic_viera/translations/nb.json  |  7 +++++++
 .../components/philips_js/translations/nb.json       |  7 +++++++
 homeassistant/components/picnic/translations/nb.json |  3 +++
 homeassistant/components/plex/translations/nb.json   |  7 +++++++
 .../components/plugwise/translations/nb.json         |  7 +++++++
 .../components/powerwall/translations/nb.json        |  7 +++++++
 .../components/progettihwsw/translations/nb.json     |  7 +++++++
 .../components/prosegur/translations/nb.json         |  3 +++
 .../components/prusalink/translations/nb.json        |  7 +++++++
 homeassistant/components/rachio/translations/nb.json |  7 +++++++
 homeassistant/components/radarr/translations/nb.json |  3 ++-
 .../components/radiotherm/translations/nb.json       |  7 +++++++
 .../components/rainforest_eagle/translations/nb.json |  7 +++++++
 homeassistant/components/rfxtrx/translations/nb.json |  7 +++++++
 .../components/ridwell/translations/nb.json          |  3 +++
 homeassistant/components/ring/translations/nb.json   |  7 +++++++
 homeassistant/components/risco/translations/nb.json  |  7 +++++++
 .../rituals_perfume_genie/translations/nb.json       |  7 +++++++
 homeassistant/components/roku/translations/nb.json   |  7 +++++++
 homeassistant/components/roon/translations/nb.json   |  7 +++++++
 .../components/ruckus_unleashed/translations/nb.json |  7 +++++++
 .../components/samsungtv/translations/nb.json        |  7 +++++++
 homeassistant/components/sense/translations/nb.json  |  7 +++++++
 homeassistant/components/sentry/translations/nb.json |  7 +++++++
 .../components/sharkiq/translations/nb.json          |  5 ++++-
 homeassistant/components/shelly/translations/nb.json |  3 +++
 homeassistant/components/sia/translations/nb.json    |  7 +++++++
 .../components/simplisafe/translations/nb.json       |  7 +++++++
 .../components/skybell/translations/nb.json          |  7 +++++++
 homeassistant/components/slack/translations/nb.json  |  7 +++++++
 homeassistant/components/sma/translations/nb.json    |  7 +++++++
 .../smart_meter_texas/translations/nb.json           |  7 +++++++
 homeassistant/components/sms/translations/nb.json    |  7 +++++++
 homeassistant/components/solax/translations/nb.json  |  3 +++
 .../components/somfy_mylink/translations/nb.json     |  7 +++++++
 homeassistant/components/sonarr/translations/nb.json |  7 +++++++
 homeassistant/components/spider/translations/nb.json |  7 +++++++
 .../components/squeezebox/translations/nb.json       |  7 +++++++
 .../components/srp_energy/translations/nb.json       |  7 +++++++
 .../components/steam_online/translations/nb.json     |  7 +++++++
 .../components/steamist/translations/nb.json         |  7 +++++++
 .../components/surepetcare/translations/nb.json      |  3 +++
 .../components/switchbee/translations/nb.json        |  3 +++
 .../components/switchbot/translations/nb.json        |  7 +++++++
 .../components/synology_dsm/translations/nb.json     |  3 +++
 .../components/system_bridge/translations/nb.json    | 10 ++++++++++
 homeassistant/components/tado/translations/nb.json   |  7 +++++++
 .../components/tautulli/translations/nb.json         |  7 +++++++
 .../components/tellduslive/translations/nb.json      |  7 +++++++
 .../tesla_wall_connector/translations/nb.json        |  7 +++++++
 .../components/tomorrowio/translations/nb.json       |  7 +++++++
 .../components/tractive/translations/nb.json         |  7 +++++++
 .../components/ukraine_alarm/translations/nb.json    |  7 +++++++
 homeassistant/components/upb/translations/nb.json    |  7 +++++++
 .../components/uptimerobot/translations/nb.json      | 10 ++++++++++
 homeassistant/components/vallox/translations/nb.json | 10 ++++++++++
 .../components/venstar/translations/nb.json          |  3 +++
 .../components/verisure/translations/nb.json         |  7 +++++++
 homeassistant/components/vicare/translations/nb.json |  7 +++++++
 homeassistant/components/vilfo/translations/nb.json  |  7 +++++++
 .../components/vlc_telnet/translations/nb.json       | 10 ++++++++++
 .../components/volumio/translations/nb.json          |  7 +++++++
 .../components/volvooncall/translations/nb.json      |  7 +++++++
 .../components/wallbox/translations/nb.json          |  3 +++
 .../components/watttime/translations/nb.json         |  3 +++
 .../components/whirlpool/translations/nb.json        |  3 +++
 homeassistant/components/wiz/translations/nb.json    |  7 +++++++
 .../components/wolflink/translations/nb.json         |  7 +++++++
 homeassistant/components/ws66i/translations/nb.json  |  7 +++++++
 .../components/yalexs_ble/translations/nb.json       |  7 +++++++
 .../components/zwave_js/translations/nb.json         | 12 ++++++++++++
 202 files changed, 1312 insertions(+), 9 deletions(-)
 create mode 100644 homeassistant/components/airnow/translations/nb.json
 create mode 100644 homeassistant/components/airthings/translations/nb.json
 create mode 100644 homeassistant/components/airvisual/translations/nb.json
 create mode 100644 homeassistant/components/amberelectric/translations/nb.json
 create mode 100644 homeassistant/components/androidtv/translations/nb.json
 create mode 100644 homeassistant/components/apple_tv/translations/nb.json
 create mode 100644 homeassistant/components/aseko_pool_live/translations/nb.json
 create mode 100644 homeassistant/components/asuswrt/translations/nb.json
 create mode 100644 homeassistant/components/awair/translations/nb.json
 create mode 100644 homeassistant/components/azure_event_hub/translations/nb.json
 create mode 100644 homeassistant/components/baf/translations/nb.json
 create mode 100644 homeassistant/components/balboa/translations/nb.json
 create mode 100644 homeassistant/components/blebox/translations/nb.json
 create mode 100644 homeassistant/components/blink/translations/nb.json
 create mode 100644 homeassistant/components/bond/translations/nb.json
 create mode 100644 homeassistant/components/bosch_shc/translations/nb.json
 create mode 100644 homeassistant/components/broadlink/translations/nb.json
 create mode 100644 homeassistant/components/canary/translations/nb.json
 create mode 100644 homeassistant/components/cloudflare/translations/nb.json
 create mode 100644 homeassistant/components/co2signal/translations/nb.json
 create mode 100644 homeassistant/components/control4/translations/nb.json
 create mode 100644 homeassistant/components/crownstone/translations/nb.json
 create mode 100644 homeassistant/components/daikin/translations/nb.json
 create mode 100644 homeassistant/components/dexcom/translations/nb.json
 create mode 100644 homeassistant/components/directv/translations/nb.json
 create mode 100644 homeassistant/components/discord/translations/nb.json
 create mode 100644 homeassistant/components/doorbird/translations/nb.json
 create mode 100644 homeassistant/components/ecowitt/translations/nb.json
 create mode 100644 homeassistant/components/efergy/translations/nb.json
 create mode 100644 homeassistant/components/elkm1/translations/nb.json
 create mode 100644 homeassistant/components/emonitor/translations/nb.json
 create mode 100644 homeassistant/components/environment_canada/translations/nb.json
 create mode 100644 homeassistant/components/evil_genius_labs/translations/nb.json
 create mode 100644 homeassistant/components/faa_delays/translations/nb.json
 create mode 100644 homeassistant/components/fibaro/translations/nb.json
 create mode 100644 homeassistant/components/fivem/translations/nb.json
 create mode 100644 homeassistant/components/flick_electric/translations/nb.json
 create mode 100644 homeassistant/components/flipr/translations/nb.json
 create mode 100644 homeassistant/components/flo/translations/nb.json
 create mode 100644 homeassistant/components/flume/translations/nb.json
 create mode 100644 homeassistant/components/forked_daapd/translations/nb.json
 create mode 100644 homeassistant/components/foscam/translations/nb.json
 create mode 100644 homeassistant/components/freebox/translations/nb.json
 create mode 100644 homeassistant/components/fully_kiosk/translations/nb.json
 create mode 100644 homeassistant/components/garages_amsterdam/translations/nb.json
 create mode 100644 homeassistant/components/generic/translations/nb.json
 create mode 100644 homeassistant/components/goalzero/translations/nb.json
 create mode 100644 homeassistant/components/habitica/translations/nb.json
 create mode 100644 homeassistant/components/hangouts/translations/nb.json
 create mode 100644 homeassistant/components/harmony/translations/nb.json
 create mode 100644 homeassistant/components/here_travel_time/translations/nb.json
 create mode 100644 homeassistant/components/hlk_sw16/translations/nb.json
 create mode 100644 homeassistant/components/homematicip_cloud/translations/nb.json
 create mode 100644 homeassistant/components/homewizard/translations/nb.json
 create mode 100644 homeassistant/components/hue/translations/nb.json
 create mode 100644 homeassistant/components/huisbaasje/translations/nb.json
 create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/nb.json
 create mode 100644 homeassistant/components/ialarm/translations/nb.json
 create mode 100644 homeassistant/components/isy994/translations/nb.json
 create mode 100644 homeassistant/components/juicenet/translations/nb.json
 create mode 100644 homeassistant/components/justnimbus/translations/nb.json
 create mode 100644 homeassistant/components/kaleidescape/translations/nb.json
 create mode 100644 homeassistant/components/kodi/translations/nb.json
 create mode 100644 homeassistant/components/konnected/translations/nb.json
 create mode 100644 homeassistant/components/kostal_plenticore/translations/nb.json
 create mode 100644 homeassistant/components/lacrosse_view/translations/nb.json
 create mode 100644 homeassistant/components/lametric/translations/nb.json
 create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/nb.json
 create mode 100644 homeassistant/components/laundrify/translations/nb.json
 create mode 100644 homeassistant/components/led_ble/translations/nb.json
 create mode 100644 homeassistant/components/life360/translations/nb.json
 create mode 100644 homeassistant/components/lookin/translations/nb.json
 create mode 100644 homeassistant/components/mazda/translations/nb.json
 create mode 100644 homeassistant/components/meater/translations/nb.json
 create mode 100644 homeassistant/components/melcloud/translations/nb.json
 create mode 100644 homeassistant/components/meteo_france/translations/nb.json
 create mode 100644 homeassistant/components/meteoclimatic/translations/nb.json
 create mode 100644 homeassistant/components/metoffice/translations/nb.json
 create mode 100644 homeassistant/components/moehlenhoff_alpha2/translations/nb.json
 create mode 100644 homeassistant/components/monoprice/translations/nb.json
 create mode 100644 homeassistant/components/motioneye/translations/nb.json
 create mode 100644 homeassistant/components/mullvad/translations/nb.json
 create mode 100644 homeassistant/components/mutesync/translations/nb.json
 create mode 100644 homeassistant/components/myq/translations/nb.json
 create mode 100644 homeassistant/components/mysensors/translations/nb.json
 create mode 100644 homeassistant/components/nanoleaf/translations/nb.json
 create mode 100644 homeassistant/components/nest/translations/nb.json
 create mode 100644 homeassistant/components/nexia/translations/nb.json
 create mode 100644 homeassistant/components/nextdns/translations/nb.json
 create mode 100644 homeassistant/components/nfandroidtv/translations/nb.json
 create mode 100644 homeassistant/components/nightscout/translations/nb.json
 create mode 100644 homeassistant/components/nina/translations/nb.json
 create mode 100644 homeassistant/components/nobo_hub/translations/nb.json
 create mode 100644 homeassistant/components/notion/translations/nb.json
 create mode 100644 homeassistant/components/nuheat/translations/nb.json
 create mode 100644 homeassistant/components/nuki/translations/nb.json
 create mode 100644 homeassistant/components/nut/translations/nb.json
 create mode 100644 homeassistant/components/nws/translations/nb.json
 create mode 100644 homeassistant/components/nzbget/translations/nb.json
 create mode 100644 homeassistant/components/omnilogic/translations/nb.json
 create mode 100644 homeassistant/components/openexchangerates/translations/nb.json
 create mode 100644 homeassistant/components/opengarage/translations/nb.json
 create mode 100644 homeassistant/components/panasonic_viera/translations/nb.json
 create mode 100644 homeassistant/components/philips_js/translations/nb.json
 create mode 100644 homeassistant/components/plex/translations/nb.json
 create mode 100644 homeassistant/components/plugwise/translations/nb.json
 create mode 100644 homeassistant/components/powerwall/translations/nb.json
 create mode 100644 homeassistant/components/progettihwsw/translations/nb.json
 create mode 100644 homeassistant/components/prusalink/translations/nb.json
 create mode 100644 homeassistant/components/rachio/translations/nb.json
 create mode 100644 homeassistant/components/radiotherm/translations/nb.json
 create mode 100644 homeassistant/components/rainforest_eagle/translations/nb.json
 create mode 100644 homeassistant/components/rfxtrx/translations/nb.json
 create mode 100644 homeassistant/components/ring/translations/nb.json
 create mode 100644 homeassistant/components/risco/translations/nb.json
 create mode 100644 homeassistant/components/rituals_perfume_genie/translations/nb.json
 create mode 100644 homeassistant/components/roku/translations/nb.json
 create mode 100644 homeassistant/components/roon/translations/nb.json
 create mode 100644 homeassistant/components/ruckus_unleashed/translations/nb.json
 create mode 100644 homeassistant/components/samsungtv/translations/nb.json
 create mode 100644 homeassistant/components/sense/translations/nb.json
 create mode 100644 homeassistant/components/sentry/translations/nb.json
 create mode 100644 homeassistant/components/sia/translations/nb.json
 create mode 100644 homeassistant/components/simplisafe/translations/nb.json
 create mode 100644 homeassistant/components/skybell/translations/nb.json
 create mode 100644 homeassistant/components/slack/translations/nb.json
 create mode 100644 homeassistant/components/sma/translations/nb.json
 create mode 100644 homeassistant/components/smart_meter_texas/translations/nb.json
 create mode 100644 homeassistant/components/sms/translations/nb.json
 create mode 100644 homeassistant/components/somfy_mylink/translations/nb.json
 create mode 100644 homeassistant/components/sonarr/translations/nb.json
 create mode 100644 homeassistant/components/spider/translations/nb.json
 create mode 100644 homeassistant/components/squeezebox/translations/nb.json
 create mode 100644 homeassistant/components/srp_energy/translations/nb.json
 create mode 100644 homeassistant/components/steam_online/translations/nb.json
 create mode 100644 homeassistant/components/steamist/translations/nb.json
 create mode 100644 homeassistant/components/switchbot/translations/nb.json
 create mode 100644 homeassistant/components/system_bridge/translations/nb.json
 create mode 100644 homeassistant/components/tado/translations/nb.json
 create mode 100644 homeassistant/components/tautulli/translations/nb.json
 create mode 100644 homeassistant/components/tellduslive/translations/nb.json
 create mode 100644 homeassistant/components/tesla_wall_connector/translations/nb.json
 create mode 100644 homeassistant/components/tomorrowio/translations/nb.json
 create mode 100644 homeassistant/components/tractive/translations/nb.json
 create mode 100644 homeassistant/components/ukraine_alarm/translations/nb.json
 create mode 100644 homeassistant/components/upb/translations/nb.json
 create mode 100644 homeassistant/components/uptimerobot/translations/nb.json
 create mode 100644 homeassistant/components/vallox/translations/nb.json
 create mode 100644 homeassistant/components/verisure/translations/nb.json
 create mode 100644 homeassistant/components/vicare/translations/nb.json
 create mode 100644 homeassistant/components/vilfo/translations/nb.json
 create mode 100644 homeassistant/components/vlc_telnet/translations/nb.json
 create mode 100644 homeassistant/components/volumio/translations/nb.json
 create mode 100644 homeassistant/components/volvooncall/translations/nb.json
 create mode 100644 homeassistant/components/wiz/translations/nb.json
 create mode 100644 homeassistant/components/wolflink/translations/nb.json
 create mode 100644 homeassistant/components/ws66i/translations/nb.json
 create mode 100644 homeassistant/components/yalexs_ble/translations/nb.json
 create mode 100644 homeassistant/components/zwave_js/translations/nb.json

diff --git a/homeassistant/components/airnow/translations/nb.json b/homeassistant/components/airnow/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/airnow/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airthings/translations/nb.json b/homeassistant/components/airthings/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/airthings/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airthings_ble/translations/nb.json b/homeassistant/components/airthings_ble/translations/nb.json
index be8e3d86b21..469243aed3d 100644
--- a/homeassistant/components/airthings_ble/translations/nb.json
+++ b/homeassistant/components/airthings_ble/translations/nb.json
@@ -2,7 +2,8 @@
     "config": {
         "abort": {
             "already_configured": "Enheten er allerede konfigurert",
-            "cannot_connect": "Tilkobling mislyktes"
+            "cannot_connect": "Tilkobling mislyktes",
+            "unknown": "Uventet feil"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/translations/nb.json b/homeassistant/components/airvisual/translations/nb.json
new file mode 100644
index 00000000000..b5a62d86459
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "general_error": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/amberelectric/translations/nb.json b/homeassistant/components/amberelectric/translations/nb.json
new file mode 100644
index 00000000000..4518f3cd8cb
--- /dev/null
+++ b/homeassistant/components/amberelectric/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown_error": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/androidtv/translations/nb.json b/homeassistant/components/androidtv/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/androidtv/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/apple_tv/translations/nb.json b/homeassistant/components/apple_tv/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/apple_tv/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aseko_pool_live/translations/nb.json b/homeassistant/components/aseko_pool_live/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/aseko_pool_live/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/asuswrt/translations/nb.json b/homeassistant/components/asuswrt/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/asuswrt/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/nb.json b/homeassistant/components/august/translations/nb.json
index 0b5511ab845..33c32bb9d35 100644
--- a/homeassistant/components/august/translations/nb.json
+++ b/homeassistant/components/august/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user_validate": {
                 "data": {
diff --git a/homeassistant/components/aussie_broadband/translations/nb.json b/homeassistant/components/aussie_broadband/translations/nb.json
index 847c45368fd..cff2aa964e0 100644
--- a/homeassistant/components/aussie_broadband/translations/nb.json
+++ b/homeassistant/components/aussie_broadband/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
@@ -7,5 +10,10 @@
                 }
             }
         }
+    },
+    "options": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/awair/translations/nb.json b/homeassistant/components/awair/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/awair/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/azure_event_hub/translations/nb.json b/homeassistant/components/azure_event_hub/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/azure_event_hub/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/baf/translations/nb.json b/homeassistant/components/baf/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/baf/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/balboa/translations/nb.json b/homeassistant/components/balboa/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/balboa/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blebox/translations/nb.json b/homeassistant/components/blebox/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/blebox/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blink/translations/nb.json b/homeassistant/components/blink/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/blink/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bond/translations/nb.json b/homeassistant/components/bond/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/bond/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bosch_shc/translations/nb.json b/homeassistant/components/bosch_shc/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/bosch_shc/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/broadlink/translations/nb.json b/homeassistant/components/broadlink/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/broadlink/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/brunt/translations/nb.json b/homeassistant/components/brunt/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/brunt/translations/nb.json
+++ b/homeassistant/components/brunt/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/canary/translations/nb.json b/homeassistant/components/canary/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/canary/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/nb.json b/homeassistant/components/cloudflare/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/co2signal/translations/nb.json b/homeassistant/components/co2signal/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/co2signal/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/nb.json b/homeassistant/components/coinbase/translations/nb.json
index 8ee9b92ee90..4209449f49b 100644
--- a/homeassistant/components/coinbase/translations/nb.json
+++ b/homeassistant/components/coinbase/translations/nb.json
@@ -1,7 +1,17 @@
 {
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    },
     "issues": {
         "removed_yaml": {
             "description": "Konfigurering av Coinbase med YAML er fjernet.\n\nDin eksisterende YAML-konfigurasjon brukes ikke av Home Assistant.\n\nFjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet."
         }
+    },
+    "options": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/coinbase/translations/pt-BR.json b/homeassistant/components/coinbase/translations/pt-BR.json
index 5f2bb7d96e3..6a8afe42dba 100644
--- a/homeassistant/components/coinbase/translations/pt-BR.json
+++ b/homeassistant/components/coinbase/translations/pt-BR.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "A configura\u00e7\u00e3o do Coinbase 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 Coinbase foi removida"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Um ou mais dos saldos de moeda solicitados n\u00e3o s\u00e3o fornecidos pela sua API Coinbase.",
diff --git a/homeassistant/components/control4/translations/nb.json b/homeassistant/components/control4/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/control4/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/crownstone/translations/nb.json b/homeassistant/components/crownstone/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/crownstone/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/daikin/translations/nb.json b/homeassistant/components/daikin/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/daikin/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/devolo_home_network/translations/nb.json b/homeassistant/components/devolo_home_network/translations/nb.json
index 204b2dfd933..ad353f5c134 100644
--- a/homeassistant/components/devolo_home_network/translations/nb.json
+++ b/homeassistant/components/devolo_home_network/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "reauth_confirm": {
                 "data": {
diff --git a/homeassistant/components/dexcom/translations/nb.json b/homeassistant/components/dexcom/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/directv/translations/nb.json b/homeassistant/components/directv/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/directv/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/discord/translations/nb.json b/homeassistant/components/discord/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/discord/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/doorbird/translations/nb.json b/homeassistant/components/doorbird/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/doorbird/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ecowitt/translations/nb.json b/homeassistant/components/ecowitt/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/ecowitt/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/efergy/translations/nb.json b/homeassistant/components/efergy/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/efergy/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elkm1/translations/nb.json b/homeassistant/components/elkm1/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/elkm1/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/elmax/translations/nb.json b/homeassistant/components/elmax/translations/nb.json
index f126937f2fe..531c356bd24 100644
--- a/homeassistant/components/elmax/translations/nb.json
+++ b/homeassistant/components/elmax/translations/nb.json
@@ -3,6 +3,9 @@
         "abort": {
             "already_configured": "Enheten er allerede konfigurert"
         },
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/emonitor/translations/nb.json b/homeassistant/components/emonitor/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/emonitor/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/translations/nb.json b/homeassistant/components/enphase_envoy/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/enphase_envoy/translations/nb.json
+++ b/homeassistant/components/enphase_envoy/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/environment_canada/translations/nb.json b/homeassistant/components/environment_canada/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/environment_canada/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/evil_genius_labs/translations/nb.json b/homeassistant/components/evil_genius_labs/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/evil_genius_labs/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ezviz/translations/nb.json b/homeassistant/components/ezviz/translations/nb.json
index 533218a036a..a0814bbe622 100644
--- a/homeassistant/components/ezviz/translations/nb.json
+++ b/homeassistant/components/ezviz/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "confirm": {
                 "data": {
diff --git a/homeassistant/components/faa_delays/translations/nb.json b/homeassistant/components/faa_delays/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/faa_delays/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fibaro/translations/nb.json b/homeassistant/components/fibaro/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/fibaro/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fivem/translations/nb.json b/homeassistant/components/fivem/translations/nb.json
new file mode 100644
index 00000000000..4518f3cd8cb
--- /dev/null
+++ b/homeassistant/components/fivem/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown_error": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flick_electric/translations/nb.json b/homeassistant/components/flick_electric/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/flick_electric/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flipr/translations/nb.json b/homeassistant/components/flipr/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/flipr/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/translations/nb.json b/homeassistant/components/flo/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/flo/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flume/translations/nb.json b/homeassistant/components/flume/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/flume/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/nb.json b/homeassistant/components/forked_daapd/translations/nb.json
new file mode 100644
index 00000000000..4518f3cd8cb
--- /dev/null
+++ b/homeassistant/components/forked_daapd/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown_error": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/translations/nb.json b/homeassistant/components/foscam/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/foscam/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/freebox/translations/nb.json b/homeassistant/components/freebox/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/freebox/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fronius/translations/nb.json b/homeassistant/components/fronius/translations/nb.json
index 89900954d12..96b2a2b4cc2 100644
--- a/homeassistant/components/fronius/translations/nb.json
+++ b/homeassistant/components/fronius/translations/nb.json
@@ -2,6 +2,9 @@
     "config": {
         "abort": {
             "invalid_host": "Ugyldig vertsnavn eller IP-adresse"
+        },
+        "error": {
+            "unknown": "Uventet feil"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/fully_kiosk/translations/nb.json b/homeassistant/components/fully_kiosk/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/fully_kiosk/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/garages_amsterdam/translations/nb.json b/homeassistant/components/garages_amsterdam/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/garages_amsterdam/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/generic/translations/nb.json b/homeassistant/components/generic/translations/nb.json
new file mode 100644
index 00000000000..42a62fb5164
--- /dev/null
+++ b/homeassistant/components/generic/translations/nb.json
@@ -0,0 +1,12 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    },
+    "options": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/nb.json b/homeassistant/components/goalzero/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_sheets/translations/nb.json b/homeassistant/components/google_sheets/translations/nb.json
index 421a4051cd5..462154c139b 100644
--- a/homeassistant/components/google_sheets/translations/nb.json
+++ b/homeassistant/components/google_sheets/translations/nb.json
@@ -2,7 +2,8 @@
     "config": {
         "abort": {
             "already_configured": "Kontoen er allerede konfigurert",
-            "cannot_connect": "Tilkobling mislyktes"
+            "cannot_connect": "Tilkobling mislyktes",
+            "unknown": "Uventet feil"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json
index 44d227554cb..54c8320665c 100644
--- a/homeassistant/components/google_travel_time/translations/es.json
+++ b/homeassistant/components/google_travel_time/translations/es.json
@@ -4,7 +4,8 @@
             "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada"
         },
         "error": {
-            "cannot_connect": "No se pudo conectar"
+            "cannot_connect": "No se pudo conectar",
+            "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json
index a93451e9b67..0c8a90e8949 100644
--- a/homeassistant/components/google_travel_time/translations/et.json
+++ b/homeassistant/components/google_travel_time/translations/et.json
@@ -4,7 +4,8 @@
             "already_configured": "Asukoht on juba m\u00e4\u00e4ratud"
         },
         "error": {
-            "cannot_connect": "\u00dchendamine nurjus"
+            "cannot_connect": "\u00dchendamine nurjus",
+            "invalid_auth": "Tuvastamine nurjus"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/no.json b/homeassistant/components/google_travel_time/translations/no.json
index f8df651a6fc..2a056451bbe 100644
--- a/homeassistant/components/google_travel_time/translations/no.json
+++ b/homeassistant/components/google_travel_time/translations/no.json
@@ -4,7 +4,8 @@
             "already_configured": "Plasseringen er allerede konfigurert"
         },
         "error": {
-            "cannot_connect": "Tilkobling mislyktes"
+            "cannot_connect": "Tilkobling mislyktes",
+            "invalid_auth": "Ugyldig godkjenning"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/habitica/translations/nb.json b/homeassistant/components/habitica/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/habitica/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/translations/nb.json b/homeassistant/components/hangouts/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/hangouts/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/harmony/translations/nb.json b/homeassistant/components/harmony/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/harmony/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/here_travel_time/translations/nb.json b/homeassistant/components/here_travel_time/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/here_travel_time/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hive/translations/nb.json b/homeassistant/components/hive/translations/nb.json
index a9f534742c5..0baf44898bf 100644
--- a/homeassistant/components/hive/translations/nb.json
+++ b/homeassistant/components/hive/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "reauth": {
                 "data": {
diff --git a/homeassistant/components/hlk_sw16/translations/nb.json b/homeassistant/components/hlk_sw16/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/hlk_sw16/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homematicip_cloud/translations/nb.json b/homeassistant/components/homematicip_cloud/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homewizard/translations/nb.json b/homeassistant/components/homewizard/translations/nb.json
new file mode 100644
index 00000000000..5a2a5b3f912
--- /dev/null
+++ b/homeassistant/components/homewizard/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown_error": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huawei_lte/translations/nb.json b/homeassistant/components/huawei_lte/translations/nb.json
index 5584869ffd5..c876e9ae4bd 100644
--- a/homeassistant/components/huawei_lte/translations/nb.json
+++ b/homeassistant/components/huawei_lte/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "reauth_confirm": {
                 "data": {
diff --git a/homeassistant/components/hue/translations/nb.json b/homeassistant/components/hue/translations/nb.json
new file mode 100644
index 00000000000..25f95a55ac6
--- /dev/null
+++ b/homeassistant/components/hue/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "linking": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/huisbaasje/translations/nb.json b/homeassistant/components/huisbaasje/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/huisbaasje/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hunterdouglas_powerview/translations/nb.json b/homeassistant/components/hunterdouglas_powerview/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/hunterdouglas_powerview/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ialarm/translations/nb.json b/homeassistant/components/ialarm/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/ialarm/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iotawatt/translations/nb.json b/homeassistant/components/iotawatt/translations/nb.json
index b97053efa85..4630e6af747 100644
--- a/homeassistant/components/iotawatt/translations/nb.json
+++ b/homeassistant/components/iotawatt/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "auth": {
                 "data": {
diff --git a/homeassistant/components/isy994/translations/nb.json b/homeassistant/components/isy994/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/isy994/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/jellyfin/translations/nb.json b/homeassistant/components/jellyfin/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/jellyfin/translations/nb.json
+++ b/homeassistant/components/jellyfin/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/juicenet/translations/nb.json b/homeassistant/components/juicenet/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/juicenet/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/justnimbus/translations/nb.json b/homeassistant/components/justnimbus/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/justnimbus/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kaleidescape/translations/nb.json b/homeassistant/components/kaleidescape/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/kaleidescape/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/keymitt_ble/translations/nb.json b/homeassistant/components/keymitt_ble/translations/nb.json
index ce3b34bc1cc..08fc754319a 100644
--- a/homeassistant/components/keymitt_ble/translations/nb.json
+++ b/homeassistant/components/keymitt_ble/translations/nb.json
@@ -2,7 +2,8 @@
     "config": {
         "abort": {
             "already_configured_device": "Enheten er allerede konfigurert",
-            "cannot_connect": "Tilkobling mislyktes"
+            "cannot_connect": "Tilkobling mislyktes",
+            "unknown": "Uventet feil"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/kmtronic/translations/nb.json b/homeassistant/components/kmtronic/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/kmtronic/translations/nb.json
+++ b/homeassistant/components/kmtronic/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/kodi/translations/nb.json b/homeassistant/components/kodi/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/kodi/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/konnected/translations/nb.json b/homeassistant/components/konnected/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/konnected/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kostal_plenticore/translations/nb.json b/homeassistant/components/kostal_plenticore/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/kostal_plenticore/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lacrosse_view/translations/nb.json b/homeassistant/components/lacrosse_view/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/lacrosse_view/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/nb.json b/homeassistant/components/lametric/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/lametric/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/pt-BR.json b/homeassistant/components/lametric/translations/pt-BR.json
index b9834dbfa2f..8c419582d9d 100644
--- a/homeassistant/components/lametric/translations/pt-BR.json
+++ b/homeassistant/components/lametric/translations/pt-BR.json
@@ -8,6 +8,8 @@
             "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.",
             "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric",
             "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})",
+            "reauth_device_not_found": "O dispositivo que voc\u00ea est\u00e1 tentando autenticar novamente n\u00e3o foi encontrado nesta conta LaMetric",
+            "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida",
             "unknown": "Erro inesperado"
         },
         "error": {
diff --git a/homeassistant/components/landisgyr_heat_meter/translations/nb.json b/homeassistant/components/landisgyr_heat_meter/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/landisgyr_heat_meter/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/laundrify/translations/nb.json b/homeassistant/components/laundrify/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/laundrify/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/led_ble/translations/nb.json b/homeassistant/components/led_ble/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/led_ble/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lidarr/translations/nb.json b/homeassistant/components/lidarr/translations/nb.json
index 06882b4fac3..885ce0bcf88 100644
--- a/homeassistant/components/lidarr/translations/nb.json
+++ b/homeassistant/components/lidarr/translations/nb.json
@@ -2,7 +2,8 @@
     "config": {
         "error": {
             "cannot_connect": "Tilkobling mislyktes",
-            "invalid_auth": "Ugyldig autentisering"
+            "invalid_auth": "Ugyldig autentisering",
+            "unknown": "Uventet feil"
         },
         "step": {
             "reauth_confirm": {
diff --git a/homeassistant/components/life360/translations/nb.json b/homeassistant/components/life360/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/life360/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/litterrobot/translations/nb.json b/homeassistant/components/litterrobot/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/litterrobot/translations/nb.json
+++ b/homeassistant/components/litterrobot/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/lookin/translations/nb.json b/homeassistant/components/lookin/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/lookin/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mazda/translations/nb.json b/homeassistant/components/mazda/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/mazda/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meater/translations/nb.json b/homeassistant/components/meater/translations/nb.json
new file mode 100644
index 00000000000..9188eb3a336
--- /dev/null
+++ b/homeassistant/components/meater/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown_auth_error": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/melcloud/translations/nb.json b/homeassistant/components/melcloud/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/melcloud/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteo_france/translations/nb.json b/homeassistant/components/meteo_france/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/meteo_france/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/meteoclimatic/translations/nb.json b/homeassistant/components/meteoclimatic/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/meteoclimatic/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/metoffice/translations/nb.json b/homeassistant/components/metoffice/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/metoffice/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/nb.json b/homeassistant/components/moehlenhoff_alpha2/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/moehlenhoff_alpha2/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/monoprice/translations/nb.json b/homeassistant/components/monoprice/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/monoprice/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motioneye/translations/nb.json b/homeassistant/components/motioneye/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/motioneye/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mullvad/translations/nb.json b/homeassistant/components/mullvad/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/mullvad/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mutesync/translations/nb.json b/homeassistant/components/mutesync/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/mutesync/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/myq/translations/nb.json b/homeassistant/components/myq/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/myq/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mysensors/translations/nb.json b/homeassistant/components/mysensors/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/mysensors/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nam/translations/nb.json b/homeassistant/components/nam/translations/nb.json
index 5d04c17f932..bff53ea8902 100644
--- a/homeassistant/components/nam/translations/nb.json
+++ b/homeassistant/components/nam/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "credentials": {
                 "data": {
diff --git a/homeassistant/components/nanoleaf/translations/nb.json b/homeassistant/components/nanoleaf/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/nanoleaf/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/nb.json b/homeassistant/components/nest/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nest/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nexia/translations/nb.json b/homeassistant/components/nexia/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nexia/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nextdns/translations/nb.json b/homeassistant/components/nextdns/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nextdns/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nfandroidtv/translations/nb.json b/homeassistant/components/nfandroidtv/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nfandroidtv/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nibe_heatpump/translations/nb.json b/homeassistant/components/nibe_heatpump/translations/nb.json
index 6ba5a1f3978..2e302a80d31 100644
--- a/homeassistant/components/nibe_heatpump/translations/nb.json
+++ b/homeassistant/components/nibe_heatpump/translations/nb.json
@@ -2,6 +2,9 @@
     "config": {
         "abort": {
             "already_configured": "Enheten er allerede konfigurert"
+        },
+        "error": {
+            "unknown": "Uventet feil"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/nb.json b/homeassistant/components/nightscout/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nightscout/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nina/translations/nb.json b/homeassistant/components/nina/translations/nb.json
new file mode 100644
index 00000000000..42a62fb5164
--- /dev/null
+++ b/homeassistant/components/nina/translations/nb.json
@@ -0,0 +1,12 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    },
+    "options": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nobo_hub/translations/nb.json b/homeassistant/components/nobo_hub/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nobo_hub/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/notion/translations/nb.json b/homeassistant/components/notion/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/notion/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuheat/translations/nb.json b/homeassistant/components/nuheat/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nuheat/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nuki/translations/nb.json b/homeassistant/components/nuki/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nuki/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nut/translations/nb.json b/homeassistant/components/nut/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nut/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nws/translations/nb.json b/homeassistant/components/nws/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/nws/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nzbget/translations/nb.json b/homeassistant/components/nzbget/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/octoprint/translations/nb.json b/homeassistant/components/octoprint/translations/nb.json
index c106bc179b3..f9907ba2eae 100644
--- a/homeassistant/components/octoprint/translations/nb.json
+++ b/homeassistant/components/octoprint/translations/nb.json
@@ -1,5 +1,11 @@
 {
     "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "reauth_confirm": {
                 "data": {
diff --git a/homeassistant/components/omnilogic/translations/nb.json b/homeassistant/components/omnilogic/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/omnilogic/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/oncue/translations/nb.json b/homeassistant/components/oncue/translations/nb.json
index ef1398553b5..5e81332ac80 100644
--- a/homeassistant/components/oncue/translations/nb.json
+++ b/homeassistant/components/oncue/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/openexchangerates/translations/nb.json b/homeassistant/components/openexchangerates/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/openexchangerates/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opengarage/translations/nb.json b/homeassistant/components/opengarage/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/opengarage/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/overkiz/translations/nb.json b/homeassistant/components/overkiz/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/overkiz/translations/nb.json
+++ b/homeassistant/components/overkiz/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/panasonic_viera/translations/nb.json b/homeassistant/components/panasonic_viera/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/panasonic_viera/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/philips_js/translations/nb.json b/homeassistant/components/philips_js/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/philips_js/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/picnic/translations/nb.json b/homeassistant/components/picnic/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/picnic/translations/nb.json
+++ b/homeassistant/components/picnic/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/plex/translations/nb.json b/homeassistant/components/plex/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/plex/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/nb.json b/homeassistant/components/plugwise/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/powerwall/translations/nb.json b/homeassistant/components/powerwall/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/powerwall/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/progettihwsw/translations/nb.json b/homeassistant/components/progettihwsw/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/progettihwsw/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/prosegur/translations/nb.json b/homeassistant/components/prosegur/translations/nb.json
index c106bc179b3..65b9b20886d 100644
--- a/homeassistant/components/prosegur/translations/nb.json
+++ b/homeassistant/components/prosegur/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "reauth_confirm": {
                 "data": {
diff --git a/homeassistant/components/prusalink/translations/nb.json b/homeassistant/components/prusalink/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/prusalink/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rachio/translations/nb.json b/homeassistant/components/rachio/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/rachio/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/radarr/translations/nb.json b/homeassistant/components/radarr/translations/nb.json
index 7fa228d894a..e7b16917b28 100644
--- a/homeassistant/components/radarr/translations/nb.json
+++ b/homeassistant/components/radarr/translations/nb.json
@@ -2,7 +2,8 @@
     "config": {
         "error": {
             "cannot_connect": "Tilkobling mislyktes",
-            "invalid_auth": "Ugyldig autentisering"
+            "invalid_auth": "Ugyldig autentisering",
+            "unknown": "Uventet feil"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/radiotherm/translations/nb.json b/homeassistant/components/radiotherm/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/radiotherm/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rainforest_eagle/translations/nb.json b/homeassistant/components/rainforest_eagle/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/rainforest_eagle/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/nb.json b/homeassistant/components/rfxtrx/translations/nb.json
new file mode 100644
index 00000000000..70b79024242
--- /dev/null
+++ b/homeassistant/components/rfxtrx/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "options": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ridwell/translations/nb.json b/homeassistant/components/ridwell/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/ridwell/translations/nb.json
+++ b/homeassistant/components/ridwell/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/ring/translations/nb.json b/homeassistant/components/ring/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/ring/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/risco/translations/nb.json b/homeassistant/components/risco/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/risco/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/rituals_perfume_genie/translations/nb.json b/homeassistant/components/rituals_perfume_genie/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/rituals_perfume_genie/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roku/translations/nb.json b/homeassistant/components/roku/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/roku/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/roon/translations/nb.json b/homeassistant/components/roon/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/roon/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/nb.json b/homeassistant/components/ruckus_unleashed/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/samsungtv/translations/nb.json b/homeassistant/components/samsungtv/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/samsungtv/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sense/translations/nb.json b/homeassistant/components/sense/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/sense/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sentry/translations/nb.json b/homeassistant/components/sentry/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/sentry/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sharkiq/translations/nb.json b/homeassistant/components/sharkiq/translations/nb.json
index c7b6400d476..02b7be76c7e 100644
--- a/homeassistant/components/sharkiq/translations/nb.json
+++ b/homeassistant/components/sharkiq/translations/nb.json
@@ -2,7 +2,10 @@
     "config": {
         "abort": {
             "cannot_connect": "Tilkobling mislyktes",
-            "unknown": "Uforventet feil"
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
         },
         "step": {
             "reauth": {
diff --git a/homeassistant/components/shelly/translations/nb.json b/homeassistant/components/shelly/translations/nb.json
index 9c4f595ae82..c471008ba7c 100644
--- a/homeassistant/components/shelly/translations/nb.json
+++ b/homeassistant/components/shelly/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "credentials": {
                 "data": {
diff --git a/homeassistant/components/sia/translations/nb.json b/homeassistant/components/sia/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/sia/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/simplisafe/translations/nb.json b/homeassistant/components/simplisafe/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/simplisafe/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/skybell/translations/nb.json b/homeassistant/components/skybell/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/skybell/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/slack/translations/nb.json b/homeassistant/components/slack/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/slack/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sma/translations/nb.json b/homeassistant/components/sma/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/sma/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smart_meter_texas/translations/nb.json b/homeassistant/components/smart_meter_texas/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/smart_meter_texas/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sms/translations/nb.json b/homeassistant/components/sms/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/sms/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solax/translations/nb.json b/homeassistant/components/solax/translations/nb.json
index 66b7784c3fc..f092dc69c22 100644
--- a/homeassistant/components/solax/translations/nb.json
+++ b/homeassistant/components/solax/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/somfy_mylink/translations/nb.json b/homeassistant/components/somfy_mylink/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/somfy_mylink/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonarr/translations/nb.json b/homeassistant/components/sonarr/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/spider/translations/nb.json b/homeassistant/components/spider/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/spider/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/squeezebox/translations/nb.json b/homeassistant/components/squeezebox/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/squeezebox/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/nb.json b/homeassistant/components/srp_energy/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/steam_online/translations/nb.json b/homeassistant/components/steam_online/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/steam_online/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/steamist/translations/nb.json b/homeassistant/components/steamist/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/steamist/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/surepetcare/translations/nb.json b/homeassistant/components/surepetcare/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/surepetcare/translations/nb.json
+++ b/homeassistant/components/surepetcare/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/switchbee/translations/nb.json b/homeassistant/components/switchbee/translations/nb.json
index ef1398553b5..5e81332ac80 100644
--- a/homeassistant/components/switchbee/translations/nb.json
+++ b/homeassistant/components/switchbee/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/switchbot/translations/nb.json b/homeassistant/components/switchbot/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/switchbot/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/synology_dsm/translations/nb.json b/homeassistant/components/synology_dsm/translations/nb.json
index 2ba01a2ddc4..74cefe6d321 100644
--- a/homeassistant/components/synology_dsm/translations/nb.json
+++ b/homeassistant/components/synology_dsm/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "reauth_confirm": {
                 "data": {
diff --git a/homeassistant/components/system_bridge/translations/nb.json b/homeassistant/components/system_bridge/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/system_bridge/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tado/translations/nb.json b/homeassistant/components/tado/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/tado/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tautulli/translations/nb.json b/homeassistant/components/tautulli/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/tautulli/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tellduslive/translations/nb.json b/homeassistant/components/tellduslive/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/tellduslive/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tesla_wall_connector/translations/nb.json b/homeassistant/components/tesla_wall_connector/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/tesla_wall_connector/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tomorrowio/translations/nb.json b/homeassistant/components/tomorrowio/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/tomorrowio/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tractive/translations/nb.json b/homeassistant/components/tractive/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/tractive/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ukraine_alarm/translations/nb.json b/homeassistant/components/ukraine_alarm/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/ukraine_alarm/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upb/translations/nb.json b/homeassistant/components/upb/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/upb/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/uptimerobot/translations/nb.json b/homeassistant/components/uptimerobot/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/uptimerobot/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vallox/translations/nb.json b/homeassistant/components/vallox/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/vallox/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/venstar/translations/nb.json b/homeassistant/components/venstar/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/venstar/translations/nb.json
+++ b/homeassistant/components/venstar/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/verisure/translations/nb.json b/homeassistant/components/verisure/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/verisure/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vicare/translations/nb.json b/homeassistant/components/vicare/translations/nb.json
new file mode 100644
index 00000000000..11a4fc139b8
--- /dev/null
+++ b/homeassistant/components/vicare/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vilfo/translations/nb.json b/homeassistant/components/vilfo/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/vilfo/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vlc_telnet/translations/nb.json b/homeassistant/components/vlc_telnet/translations/nb.json
new file mode 100644
index 00000000000..d00b0b51267
--- /dev/null
+++ b/homeassistant/components/vlc_telnet/translations/nb.json
@@ -0,0 +1,10 @@
+{
+    "config": {
+        "abort": {
+            "unknown": "Uventet feil"
+        },
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volumio/translations/nb.json b/homeassistant/components/volumio/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/volumio/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/volvooncall/translations/nb.json b/homeassistant/components/volvooncall/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/volvooncall/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wallbox/translations/nb.json b/homeassistant/components/wallbox/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/wallbox/translations/nb.json
+++ b/homeassistant/components/wallbox/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/watttime/translations/nb.json b/homeassistant/components/watttime/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/watttime/translations/nb.json
+++ b/homeassistant/components/watttime/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/whirlpool/translations/nb.json b/homeassistant/components/whirlpool/translations/nb.json
index 847c45368fd..fc3d4c4023c 100644
--- a/homeassistant/components/whirlpool/translations/nb.json
+++ b/homeassistant/components/whirlpool/translations/nb.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/wiz/translations/nb.json b/homeassistant/components/wiz/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/wiz/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wolflink/translations/nb.json b/homeassistant/components/wolflink/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/wolflink/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ws66i/translations/nb.json b/homeassistant/components/ws66i/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/ws66i/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yalexs_ble/translations/nb.json b/homeassistant/components/yalexs_ble/translations/nb.json
new file mode 100644
index 00000000000..a22f7eef3d6
--- /dev/null
+++ b/homeassistant/components/yalexs_ble/translations/nb.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/nb.json b/homeassistant/components/zwave_js/translations/nb.json
new file mode 100644
index 00000000000..42a62fb5164
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/nb.json
@@ -0,0 +1,12 @@
+{
+    "config": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    },
+    "options": {
+        "error": {
+            "unknown": "Uventet feil"
+        }
+    }
+}
\ No newline at end of file
-- 
GitLab


From d537968517c5c36d2f379cbde625353f5410e206 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Sun, 16 Oct 2022 19:05:56 -0700
Subject: [PATCH 529/985] Bump google-cloud-pubsub to 2.13.10 (#80433)

---
 homeassistant/components/google_pubsub/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_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json
index c6690edb52d..d7318e597e1 100644
--- a/homeassistant/components/google_pubsub/manifest.json
+++ b/homeassistant/components/google_pubsub/manifest.json
@@ -2,7 +2,7 @@
   "domain": "google_pubsub",
   "name": "Google Pub/Sub",
   "documentation": "https://www.home-assistant.io/integrations/google_pubsub",
-  "requirements": ["google-cloud-pubsub==2.11.0"],
+  "requirements": ["google-cloud-pubsub==2.13.10"],
   "codeowners": [],
   "iot_class": "cloud_push"
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index ddcb00bceea..2013c7c7c54 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -771,7 +771,7 @@ goalzero==0.2.1
 goodwe==0.2.18
 
 # homeassistant.components.google_pubsub
-google-cloud-pubsub==2.11.0
+google-cloud-pubsub==2.13.10
 
 # homeassistant.components.google_cloud
 google-cloud-texttospeech==2.12.3
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index a0e68550c99..2fe4bb98109 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -581,7 +581,7 @@ goalzero==0.2.1
 goodwe==0.2.18
 
 # homeassistant.components.google_pubsub
-google-cloud-pubsub==2.11.0
+google-cloud-pubsub==2.13.10
 
 # homeassistant.components.nest
 google-nest-sdm==2.0.0
-- 
GitLab


From 551fb449752e1c3f55eb688d24509876020852d1 Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
Date: Mon, 17 Oct 2022 04:06:48 +0200
Subject: [PATCH 530/985] Stop coordinator before connection in nibe_heatpump
 (#80396)

Stop coordinator in nibe_heatpump
---
 .../components/nibe_heatpump/__init__.py      | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py
index b9921df4e1e..053d6db2a34 100644
--- a/homeassistant/components/nibe_heatpump/__init__.py
+++ b/homeassistant/components/nibe_heatpump/__init__.py
@@ -1,6 +1,7 @@
 """The Nibe Heat Pump integration."""
 from __future__ import annotations
 
+import asyncio
 from collections import defaultdict
 from collections.abc import Callable, Iterable
 from datetime import timedelta
@@ -103,7 +104,7 @@ 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):
         coordinator: Coordinator = hass.data[DOMAIN].pop(entry.entry_id)
-        await coordinator.connection.stop()
+        await coordinator.async_shutdown()
 
     return unload_ok
 
@@ -173,6 +174,7 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]):
         self.seed: dict[int, Coil] = {}
         self.connection = connection
         self.heatpump = heatpump
+        self.task: asyncio.Task | None = None
 
         heatpump.subscribe(heatpump.COIL_UPDATE_EVENT, self._on_coil_update)
 
@@ -219,6 +221,13 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]):
         self.async_update_context_listeners([coil.address])
 
     async def _async_update_data(self) -> dict[int, Coil]:
+        self.task = asyncio.current_task()
+        try:
+            return await self._async_update_data_internal()
+        finally:
+            self.task = None
+
+    async def _async_update_data_internal(self) -> dict[int, Coil]:
         @retry(
             retry=retry_if_exception_type(CoilReadException),
             stop=stop_after_attempt(COIL_READ_RETRIES),
@@ -249,6 +258,14 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]):
 
         return result
 
+    async def async_shutdown(self):
+        """Make sure a coordinator is shut down as well as it's connection."""
+        if self.task:
+            self.task.cancel()
+            await asyncio.wait((self.task,))
+        self._unschedule_refresh()
+        await self.connection.stop()
+
 
 class CoilEntity(CoordinatorEntity[Coordinator]):
     """Base for coil based entities."""
-- 
GitLab


From dfd3476cfff2e5c4f05a62a9e9323e26e9f5375b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Jedelsk=C3=BD?=
 <8687359+jjedelsky@users.noreply.github.com>
Date: Mon, 17 Oct 2022 09:42:20 +0200
Subject: [PATCH 531/985] Handle ReadTimeout during wolflink setup (#78135)

* Handle ReadTimeout during wolflink setup

* Reorder imports

Co-authored-by: Yevhenii Vaskivskyi <yevhenii.vaskivskyi@gmail.com>

* Reorder exceptions

Co-authored-by: Yevhenii Vaskivskyi <yevhenii.vaskivskyi@gmail.com>

* Use RequestError instead of ConnectError, ReadTimeout, and ConnectTimeout

Co-authored-by: Yevhenii Vaskivskyi <yevhenii.vaskivskyi@gmail.com>
---
 homeassistant/components/wolflink/__init__.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py
index 9d76c61806b..290e7337617 100644
--- a/homeassistant/components/wolflink/__init__.py
+++ b/homeassistant/components/wolflink/__init__.py
@@ -2,7 +2,7 @@
 from datetime import timedelta
 import logging
 
-from httpx import ConnectError, ConnectTimeout
+from httpx import RequestError
 from wolf_smartset.token_auth import InvalidAuth
 from wolf_smartset.wolf_client import FetchFailed, ParameterReadError, WolfClient
 
@@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
                 for parameter in parameters
                 if parameter.value_id in values
             }
-        except ConnectError as exception:
+        except RequestError as exception:
             raise UpdateFailed(
                 f"Error communicating with API: {exception}"
             ) from exception
@@ -134,7 +134,7 @@ async def fetch_parameters_init(client: WolfClient, gateway_id: int, device_id:
     """Fetch all available parameters with usage of WolfClient but handles all exceptions and results in ConfigEntryNotReady."""
     try:
         return await fetch_parameters(client, gateway_id, device_id)
-    except (ConnectError, ConnectTimeout, FetchFailed) as exception:
+    except (FetchFailed, RequestError) as exception:
         raise ConfigEntryNotReady(
             f"Error communicating with API: {exception}"
         ) from exception
-- 
GitLab


From d49a223a0236e2335983798a17d063da192a720b Mon Sep 17 00:00:00 2001
From: Sean Vig <sean.v.775@gmail.com>
Date: Mon, 17 Oct 2022 04:13:11 -0400
Subject: [PATCH 532/985] Fix updating Amcrest binary sensors (#80365)

* Fix updating Amcrest binary sensors

As detailed in https://bugs.python.org/issue32113, a generator
expression cannot be used with asynchronous components, even that the
resulting elements of the generator are normal objects.  Manually
iterate over the event codes and check if the events have happened.
Escape early on the first event that is triggered such that this is
functionally equivalent to using `any`.

* Update homeassistant/components/amcrest/binary_sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 homeassistant/components/amcrest/binary_sensor.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py
index 4d438c7c3bf..e71a5cda538 100644
--- a/homeassistant/components/amcrest/binary_sensor.py
+++ b/homeassistant/components/amcrest/binary_sensor.py
@@ -215,10 +215,12 @@ class AmcrestBinarySensor(BinarySensorEntity):
             raise ValueError(f"Binary sensor {self.name} event codes not set")
 
         try:
-            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
-            )
+            for event_code in event_codes:
+                if await self._api.async_event_channels_happened(event_code):
+                    self._attr_is_on = True
+                    break
+            else:
+                self._attr_is_on = False
         except AmcrestError as error:
             log_update_error(_LOGGER, "update", self.name, "binary sensor", error)
             return
-- 
GitLab


From bbb0b2a0e1ed41db0fb6e46c2c5476ad4d16e0f4 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 17 Oct 2022 10:13:53 +0200
Subject: [PATCH 533/985] Refactor the core config store (#80457)

* Add helper for config Store

* Use internal class
---
 homeassistant/core.py | 41 +++++++++++++++++++----------------------
 1 file changed, 19 insertions(+), 22 deletions(-)

diff --git a/homeassistant/core.py b/homeassistant/core.py
index 872b4298c5d..089e7e3b298 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -1962,17 +1962,7 @@ class Config:
 
     async def async_load(self) -> None:
         """Load [homeassistant] core config."""
-        # Circular dep
-        # pylint: disable=import-outside-toplevel
-        from .helpers.storage import Store
-
-        store = Store[dict[str, Any]](
-            self.hass,
-            CORE_STORAGE_VERSION,
-            CORE_STORAGE_KEY,
-            private=True,
-            atomic_writes=True,
-        )
+        store = self._ConfigStore(self.hass)
 
         if not (data := await store.async_load()):
             return
@@ -2006,10 +1996,6 @@ class Config:
 
     async def async_store(self) -> None:
         """Store [homeassistant] core config."""
-        # Circular dep
-        # pylint: disable=import-outside-toplevel
-        from .helpers.storage import Store
-
         data = {
             "latitude": self.latitude,
             "longitude": self.longitude,
@@ -2024,11 +2010,22 @@ class Config:
             "currency": self.currency,
         }
 
-        store: Store[dict[str, Any]] = Store(
-            self.hass,
-            CORE_STORAGE_VERSION,
-            CORE_STORAGE_KEY,
-            private=True,
-            atomic_writes=True,
-        )
+        store = self._ConfigStore(self.hass)
         await store.async_save(data)
+
+    # Circular dependency prevents us from generating the class at top level
+    # pylint: disable-next=import-outside-toplevel
+    from .helpers.storage import Store
+
+    class _ConfigStore(Store[dict[str, Any]]):
+        """Class to help storing Config data."""
+
+        def __init__(self, hass: HomeAssistant) -> None:
+            """Initialize storage class."""
+            super().__init__(
+                hass,
+                CORE_STORAGE_VERSION,
+                CORE_STORAGE_KEY,
+                private=True,
+                atomic_writes=True,
+            )
-- 
GitLab


From abec592a248607869dc1d495f956ca397dc189f4 Mon Sep 17 00:00:00 2001
From: Florent Thoumie <florent@thoumie.net>
Date: Mon, 17 Oct 2022 01:14:29 -0700
Subject: [PATCH 534/985] Update to iaqualink 0.5.0 (#80304)

* Update to iaqualink 0.5.0.

* Boolean conditional style fix

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix black formatting

* Update iaqualink tests after update to 0.5.x

* Remove debug print statements

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 .../components/iaqualink/__init__.py          | 93 +++++++++++--------
 homeassistant/components/iaqualink/climate.py | 47 ++--------
 homeassistant/components/iaqualink/const.py   |  2 +-
 homeassistant/components/iaqualink/light.py   |  4 +-
 .../components/iaqualink/manifest.json        |  2 +-
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 tests/components/iaqualink/conftest.py        | 23 ++++-
 tests/components/iaqualink/test_init.py       | 48 +++++-----
 9 files changed, 110 insertions(+), 113 deletions(-)

diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py
index 61f27e8970c..1bd9d7d72cf 100644
--- a/homeassistant/components/iaqualink/__init__.py
+++ b/homeassistant/components/iaqualink/__init__.py
@@ -14,8 +14,8 @@ from iaqualink.device import (
     AqualinkDevice,
     AqualinkLight,
     AqualinkSensor,
+    AqualinkSwitch,
     AqualinkThermostat,
-    AqualinkToggle,
 )
 from iaqualink.exception import AqualinkServiceException
 from typing_extensions import Concatenate, ParamSpec
@@ -29,7 +29,6 @@ from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.dispatcher import (
     async_dispatcher_connect,
     async_dispatcher_send,
@@ -56,7 +55,9 @@ PLATFORMS = [
 ]
 
 
-async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+async def async_setup_entry(  # noqa: C901
+    hass: HomeAssistant, entry: ConfigEntry
+) -> bool:
     """Set up Aqualink from a config entry."""
     username = entry.data[CONF_USERNAME]
     password = entry.data[CONF_PASSWORD]
@@ -70,17 +71,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = []
     switches = hass.data[DOMAIN][SWITCH_DOMAIN] = []
 
-    session = async_get_clientsession(hass)
-    aqualink = AqualinkClient(username, password, session)
+    aqualink = AqualinkClient(username, password)
     try:
         await aqualink.login()
     except AqualinkServiceException as login_exception:
         _LOGGER.error("Failed to login: %s", login_exception)
+        await aqualink.close()
         return False
     except (
         asyncio.TimeoutError,
         aiohttp.client_exceptions.ClientConnectorError,
     ) as aio_exception:
+        await aqualink.close()
         raise ConfigEntryNotReady(
             f"Error while attempting login: {aio_exception}"
         ) from aio_exception
@@ -88,6 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     try:
         systems = await aqualink.get_systems()
     except AqualinkServiceException as svc_exception:
+        await aqualink.close()
         raise ConfigEntryNotReady(
             f"Error while attempting to retrieve systems list: {svc_exception}"
         ) from svc_exception
@@ -95,27 +98,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     systems = list(systems.values())
     if not systems:
         _LOGGER.error("No systems detected or supported")
+        await aqualink.close()
         return False
 
-    # Only supporting the first system for now.
-    try:
-        devices = await systems[0].get_devices()
-    except AqualinkServiceException as svc_exception:
-        raise ConfigEntryNotReady(
-            f"Error while attempting to retrieve devices list: {svc_exception}"
-        ) from svc_exception
-
-    for dev in devices.values():
-        if isinstance(dev, AqualinkThermostat):
-            climates += [dev]
-        elif isinstance(dev, AqualinkLight):
-            lights += [dev]
-        elif isinstance(dev, AqualinkBinarySensor):
-            binary_sensors += [dev]
-        elif isinstance(dev, AqualinkSensor):
-            sensors += [dev]
-        elif isinstance(dev, AqualinkToggle):
-            switches += [dev]
+    for system in systems:
+        try:
+            devices = await system.get_devices()
+        except AqualinkServiceException as svc_exception:
+            await aqualink.close()
+            raise ConfigEntryNotReady(
+                f"Error while attempting to retrieve devices list: {svc_exception}"
+            ) from svc_exception
+
+        for dev in devices.values():
+            if isinstance(dev, AqualinkThermostat):
+                climates += [dev]
+            elif isinstance(dev, AqualinkLight):
+                lights += [dev]
+            elif isinstance(dev, AqualinkSwitch):
+                switches += [dev]
+            elif isinstance(dev, AqualinkBinarySensor):
+                binary_sensors += [dev]
+            elif isinstance(dev, AqualinkSensor):
+                sensors += [dev]
 
     platforms = []
     if binary_sensors:
@@ -134,23 +139,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         _LOGGER.debug("Got %s switches: %s", len(switches), switches)
         platforms.append(Platform.SWITCH)
 
+    hass.data[DOMAIN]["client"] = aqualink
+
     await hass.config_entries.async_forward_entry_setups(entry, platforms)
 
     async def _async_systems_update(now):
         """Refresh internal state for all systems."""
-        prev = systems[0].online
-
-        try:
-            await systems[0].update()
-        except AqualinkServiceException as svc_exception:
-            if prev is not None:
-                _LOGGER.warning("Failed to refresh iAqualink state: %s", svc_exception)
-        else:
-            cur = systems[0].online
-            if cur is True and prev is not True:
-                _LOGGER.warning("Reconnected to iAqualink")
-
-        async_dispatcher_send(hass, DOMAIN)
+        for system in systems:
+            prev = system.online
+
+            try:
+                await system.update()
+            except AqualinkServiceException as svc_exception:
+                if prev is not None:
+                    _LOGGER.warning(
+                        "Failed to refresh system %s state: %s",
+                        system.serial,
+                        svc_exception,
+                    )
+            else:
+                cur = system.online
+                if cur and not prev:
+                    _LOGGER.warning("System %s reconnected to iAqualink", system.serial)
+
+            async_dispatcher_send(hass, DOMAIN)
 
     async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL)
 
@@ -159,6 +171,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Unload a config entry."""
+    aqualink = hass.data[DOMAIN]["client"]
+    await aqualink.close()
+
     platforms_to_unload = [
         platform for platform in PLATFORMS if platform in hass.data[DOMAIN]
     ]
@@ -226,8 +241,8 @@ class AqualinkEntity(Entity):
         """Return the device info."""
         return DeviceInfo(
             identifiers={(DOMAIN, self.unique_id)},
-            manufacturer="Jandy",
-            model=self.dev.__class__.__name__.replace("Aqualink", ""),
+            manufacturer=self.dev.manufacturer,
+            model=self.dev.model,
             name=self.name,
             via_device=(DOMAIN, self.dev.system.serial),
         )
diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py
index 725f1b9084e..408bd56778e 100644
--- a/homeassistant/components/iaqualink/climate.py
+++ b/homeassistant/components/iaqualink/climate.py
@@ -4,14 +4,6 @@ from __future__ import annotations
 import logging
 from typing import Any
 
-from iaqualink.const import (
-    AQUALINK_TEMP_CELSIUS_HIGH,
-    AQUALINK_TEMP_CELSIUS_LOW,
-    AQUALINK_TEMP_FAHRENHEIT_HIGH,
-    AQUALINK_TEMP_FAHRENHEIT_LOW,
-)
-from iaqualink.device import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState
-
 from homeassistant.components.climate import (
     DOMAIN as CLIMATE_DOMAIN,
     ClimateEntity,
@@ -55,17 +47,10 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
         """Return the name of the thermostat."""
         return self.dev.label.split(" ")[0]
 
-    @property
-    def pump(self) -> AqualinkPump:
-        """Return the pump device for the current thermostat."""
-        pump = f"{self.name.lower()}_pump"
-        return self.dev.system.devices[pump]
-
     @property
     def hvac_mode(self) -> HVACMode:
         """Return the current HVAC mode."""
-        state = AqualinkState(self.heater.state)
-        if state == AqualinkState.ON:
+        if self.dev.is_on is True:
             return HVACMode.HEAT
         return HVACMode.OFF
 
@@ -73,32 +58,28 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
     async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
         """Turn the underlying heater switch on or off."""
         if hvac_mode == HVACMode.HEAT:
-            await await_or_reraise(self.heater.turn_on())
+            await await_or_reraise(self.dev.turn_on())
         elif hvac_mode == HVACMode.OFF:
-            await await_or_reraise(self.heater.turn_off())
+            await await_or_reraise(self.dev.turn_off())
         else:
             _LOGGER.warning("Unknown operation mode: %s", hvac_mode)
 
     @property
     def temperature_unit(self) -> str:
         """Return the unit of measurement."""
-        if self.dev.system.temp_unit == "F":
+        if self.dev.unit == "F":
             return TEMP_FAHRENHEIT
         return TEMP_CELSIUS
 
     @property
     def min_temp(self) -> int:
         """Return the minimum temperature supported by the thermostat."""
-        if self.temperature_unit == TEMP_FAHRENHEIT:
-            return AQUALINK_TEMP_FAHRENHEIT_LOW
-        return AQUALINK_TEMP_CELSIUS_LOW
+        return self.dev.min_temperature
 
     @property
     def max_temp(self) -> int:
         """Return the minimum temperature supported by the thermostat."""
-        if self.temperature_unit == TEMP_FAHRENHEIT:
-            return AQUALINK_TEMP_FAHRENHEIT_HIGH
-        return AQUALINK_TEMP_CELSIUS_HIGH
+        return self.dev.max_temperature
 
     @property
     def target_temperature(self) -> float:
@@ -110,21 +91,9 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
         """Set new target temperature."""
         await await_or_reraise(self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])))
 
-    @property
-    def sensor(self) -> AqualinkSensor:
-        """Return the sensor device for the current thermostat."""
-        sensor = f"{self.name.lower()}_temp"
-        return self.dev.system.devices[sensor]
-
     @property
     def current_temperature(self) -> float | None:
         """Return the current temperature."""
-        if self.sensor.state != "":
-            return float(self.sensor.state)
+        if self.dev.current_temperature != "":
+            return float(self.dev.current_temperature)
         return None
-
-    @property
-    def heater(self) -> AqualinkHeater:
-        """Return the heater device for the current thermostat."""
-        heater = f"{self.name.lower()}_heater"
-        return self.dev.system.devices[heater]
diff --git a/homeassistant/components/iaqualink/const.py b/homeassistant/components/iaqualink/const.py
index 189d7083b2d..7cabfa2b4f6 100644
--- a/homeassistant/components/iaqualink/const.py
+++ b/homeassistant/components/iaqualink/const.py
@@ -2,4 +2,4 @@
 from datetime import timedelta
 
 DOMAIN = "iaqualink"
-UPDATE_INTERVAL = timedelta(seconds=30)
+UPDATE_INTERVAL = timedelta(seconds=15)
diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py
index 3a8b1e0fb4a..91ca64a87e6 100644
--- a/homeassistant/components/iaqualink/light.py
+++ b/homeassistant/components/iaqualink/light.py
@@ -90,7 +90,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
     @property
     def color_mode(self) -> ColorMode:
         """Return the color mode of the light."""
-        if self.dev.is_dimmer:
+        if self.dev.supports_brightness:
             return ColorMode.BRIGHTNESS
         return ColorMode.ONOFF
 
@@ -102,7 +102,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
     @property
     def supported_features(self) -> int:
         """Return the list of features supported by the light."""
-        if self.dev.is_color:
+        if self.dev.supports_effect:
             return LightEntityFeature.EFFECT
 
         return 0
diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json
index 7c57744fd3b..d5b7d7de0d8 100644
--- a/homeassistant/components/iaqualink/manifest.json
+++ b/homeassistant/components/iaqualink/manifest.json
@@ -4,7 +4,7 @@
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/iaqualink/",
   "codeowners": ["@flz"],
-  "requirements": ["iaqualink==0.4.1"],
+  "requirements": ["iaqualink==0.5.0"],
   "iot_class": "cloud_polling",
   "loggers": ["iaqualink"]
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index 2013c7c7c54..816621b2bf0 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -901,7 +901,7 @@ hyperion-py==0.7.5
 iammeter==0.1.7
 
 # homeassistant.components.iaqualink
-iaqualink==0.4.1
+iaqualink==0.5.0
 
 # homeassistant.components.ibeacon
 ibeacon_ble==0.7.4
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 2fe4bb98109..f8cdd47286f 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -672,7 +672,7 @@ huawei-lte-api==1.6.3
 hyperion-py==0.7.5
 
 # homeassistant.components.iaqualink
-iaqualink==0.4.1
+iaqualink==0.5.0
 
 # homeassistant.components.ibeacon
 ibeacon_ble==0.7.4
diff --git a/tests/components/iaqualink/conftest.py b/tests/components/iaqualink/conftest.py
index 6a46e063501..b4db99dbe40 100644
--- a/tests/components/iaqualink/conftest.py
+++ b/tests/components/iaqualink/conftest.py
@@ -1,6 +1,6 @@
 """Configuration for iAqualink tests."""
 import random
-from unittest.mock import AsyncMock
+from unittest.mock import AsyncMock, PropertyMock, patch
 
 from iaqualink.client import AqualinkClient
 from iaqualink.device import AqualinkDevice
@@ -47,14 +47,31 @@ def get_aqualink_system(aqualink, cls=None, data=None):
     return cls(aqualink=aqualink, data=data)
 
 
-def get_aqualink_device(system, cls=None, data=None):
+def get_aqualink_device(system, name, cls=None, data=None):
     """Create aqualink device."""
     if cls is None:
         cls = AqualinkDevice
 
+        # AqualinkDevice doesn't implement some of the properties since it's left to
+        # sub-classes for them to do. Provide a basic implementation here for the
+        # benefits of the test suite.
+        attrs = {
+            "name": name,
+            "manufacturer": "Jandy",
+            "model": "Device",
+            "label": name.upper(),
+        }
+
+        for k, v in attrs.items():
+            patcher = patch.object(cls, k, new_callable=PropertyMock)
+            mock = patcher.start()
+            mock.return_value = v
+
     if data is None:
         data = {}
 
+    data["name"] = name
+
     return cls(system=system, data=data)
 
 
@@ -72,7 +89,7 @@ def config_fixture():
 
 @pytest.fixture(name="config_entry")
 def config_entry_fixture():
-    """Create a mock HEOS config entry."""
+    """Create a mock config entry."""
     return MockConfigEntry(
         domain=DOMAIN,
         data=MOCK_DATA,
diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py
index bd2e072d213..3f2b822da81 100644
--- a/tests/components/iaqualink/test_init.py
+++ b/tests/components/iaqualink/test_init.py
@@ -4,15 +4,15 @@ import asyncio
 import logging
 from unittest.mock import AsyncMock, patch
 
-from iaqualink.device import (
-    AqualinkAuxToggle,
-    AqualinkBinarySensor,
-    AqualinkDevice,
-    AqualinkLightToggle,
-    AqualinkSensor,
-    AqualinkThermostat,
-)
 from iaqualink.exception import AqualinkServiceException
+from iaqualink.systems.iaqua.device import (
+    IaquaAuxSwitch,
+    IaquaBinarySensor,
+    IaquaLightSwitch,
+    IaquaSensor,
+    IaquaThermostat,
+)
+from iaqualink.systems.iaqua.system import IaquaSystem
 
 from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
 from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
@@ -101,7 +101,7 @@ async def test_setup_devices_exception(hass, config_entry, client):
     """Test setup encountering an exception while retrieving devices."""
     config_entry.add_to_hass(hass)
 
-    system = get_aqualink_system(client)
+    system = get_aqualink_system(client, cls=IaquaSystem)
     systems = {system.serial: system}
 
     with patch(
@@ -124,10 +124,10 @@ async def test_setup_all_good_no_recognized_devices(hass, config_entry, client):
     """Test setup ending in no devices recognized."""
     config_entry.add_to_hass(hass)
 
-    system = get_aqualink_system(client)
+    system = get_aqualink_system(client, cls=IaquaSystem)
     systems = {system.serial: system}
 
-    device = get_aqualink_device(system, AqualinkDevice, data={"name": "dev_1"})
+    device = get_aqualink_device(system, name="dev_1")
     devices = {device.name: device}
 
     with patch(
@@ -161,19 +161,15 @@ async def test_setup_all_good_all_device_types(hass, config_entry, client):
     """Test setup ending in one device of each type recognized."""
     config_entry.add_to_hass(hass)
 
-    system = get_aqualink_system(client)
+    system = get_aqualink_system(client, cls=IaquaSystem)
     systems = {system.serial: system}
 
     devices = [
-        get_aqualink_device(system, AqualinkAuxToggle, data={"name": "aux_1"}),
-        get_aqualink_device(
-            system, AqualinkBinarySensor, data={"name": "freeze_protection"}
-        ),
-        get_aqualink_device(system, AqualinkLightToggle, data={"name": "aux_2"}),
-        get_aqualink_device(system, AqualinkSensor, data={"name": "ph"}),
-        get_aqualink_device(
-            system, AqualinkThermostat, data={"name": "pool_set_point"}
-        ),
+        get_aqualink_device(system, name="aux_1", cls=IaquaAuxSwitch),
+        get_aqualink_device(system, name="freeze_protection", cls=IaquaBinarySensor),
+        get_aqualink_device(system, name="aux_2", cls=IaquaLightSwitch),
+        get_aqualink_device(system, name="ph", cls=IaquaSensor),
+        get_aqualink_device(system, name="pool_set_point", cls=IaquaThermostat),
     ]
     devices = {d.name: d for d in devices}
 
@@ -207,7 +203,7 @@ async def test_multiple_updates(hass, config_entry, caplog, client):
     """Test all possible results of online status transition after update."""
     config_entry.add_to_hass(hass)
 
-    system = get_aqualink_system(client)
+    system = get_aqualink_system(client, cls=IaquaSystem)
     systems = {system.serial: system}
 
     system.get_devices = AsyncMock(return_value={})
@@ -269,7 +265,7 @@ async def test_multiple_updates(hass, config_entry, caplog, client):
     system.update.side_effect = set_online_to_true
     await _ffwd_next_update_interval(hass)
     assert len(caplog.records) == 1
-    assert "Reconnected" in caplog.text
+    assert "reconnected" in caplog.text
 
     # False -> None / ServiceException
     system.online = False
@@ -292,7 +288,7 @@ async def test_multiple_updates(hass, config_entry, caplog, client):
     system.update.side_effect = set_online_to_true
     await _ffwd_next_update_interval(hass)
     assert len(caplog.records) == 1
-    assert "Reconnected" in caplog.text
+    assert "reconnected" in caplog.text
 
     # None -> False
     system.online = None
@@ -311,11 +307,11 @@ async def test_entity_assumed_and_available(hass, config_entry, client):
     """Test assumed_state and_available properties for all values of online."""
     config_entry.add_to_hass(hass)
 
-    system = get_aqualink_system(client)
+    system = get_aqualink_system(client, cls=IaquaSystem)
     systems = {system.serial: system}
 
     light = get_aqualink_device(
-        system, AqualinkLightToggle, data={"name": "aux_1", "state": "1"}
+        system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
     )
     devices = {d.name: d for d in [light]}
     system.get_devices = AsyncMock(return_value=devices)
-- 
GitLab


From b6a59b282f9fd51fac585c132ff640afb155e3f2 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 17 Oct 2022 04:04:05 -0500
Subject: [PATCH 535/985] Bump qingping-ble to 0.8.0 (#80443)

Adds support for the Temp RH Pro E model
---
 homeassistant/components/qingping/manifest.json | 2 +-
 requirements_all.txt                            | 2 +-
 requirements_test_all.txt                       | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json
index 85df751bfc7..c0fc7347b13 100644
--- a/homeassistant/components/qingping/manifest.json
+++ b/homeassistant/components/qingping/manifest.json
@@ -11,7 +11,7 @@
       "connectable": false
     }
   ],
-  "requirements": ["qingping-ble==0.7.0"],
+  "requirements": ["qingping-ble==0.8.0"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco", "@skgsergio"],
   "iot_class": "local_push"
diff --git a/requirements_all.txt b/requirements_all.txt
index 816621b2bf0..aca8f43c2e5 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2123,7 +2123,7 @@ pyzbar==0.1.7
 pyzerproc==0.4.8
 
 # homeassistant.components.qingping
-qingping-ble==0.7.0
+qingping-ble==0.8.0
 
 # homeassistant.components.qnap
 qnapstats==0.4.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f8cdd47286f..9729431c1f8 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1471,7 +1471,7 @@ pyws66i==1.1
 pyzerproc==0.4.8
 
 # homeassistant.components.qingping
-qingping-ble==0.7.0
+qingping-ble==0.8.0
 
 # homeassistant.components.rachio
 rachiopy==1.0.3
-- 
GitLab


From 627bd827665ae7a28c6772ee2990f3a58972d189 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Mon, 17 Oct 2022 11:42:17 +0200
Subject: [PATCH 536/985] Simplify parsing of script and automation config
 (#80465)

---
 .../components/automation/__init__.py         | 64 +++++++++----------
 homeassistant/components/script/__init__.py   | 63 +++++++++---------
 2 files changed, 61 insertions(+), 66 deletions(-)

diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index fed941dea1a..234fcc97839 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -53,7 +53,7 @@ from homeassistant.exceptions import (
     ServiceNotFound,
     TemplateError,
 )
-from homeassistant.helpers import condition, extract_domain_configs
+from homeassistant.helpers import condition
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity import ToggleEntity
 from homeassistant.helpers.entity_component import EntityComponent
@@ -670,7 +670,6 @@ class AutomationEntityConfig:
     """Container for prepared automation entity configuration."""
 
     config_block: ConfigType
-    config_key: str
     list_no: int
     raw_blueprint_inputs: ConfigType | None
     raw_config: ConfigType | None
@@ -683,38 +682,37 @@ async def _prepare_automation_config(
     """Parse configuration and prepare automation entity configuration."""
     automation_configs: list[AutomationEntityConfig] = []
 
-    for config_key in extract_domain_configs(config, DOMAIN):
-        conf: list[ConfigType | blueprint.BlueprintInputs] = config[config_key]
+    conf: list[ConfigType | blueprint.BlueprintInputs] = config[DOMAIN]
 
-        for list_no, config_block in enumerate(conf):
-            raw_blueprint_inputs = None
-            raw_config = None
-            if isinstance(config_block, blueprint.BlueprintInputs):
-                blueprint_inputs = config_block
-                raw_blueprint_inputs = blueprint_inputs.config_with_inputs
+    for list_no, config_block in enumerate(conf):
+        raw_blueprint_inputs = None
+        raw_config = None
+        if isinstance(config_block, blueprint.BlueprintInputs):
+            blueprint_inputs = config_block
+            raw_blueprint_inputs = blueprint_inputs.config_with_inputs
 
-                try:
-                    raw_config = blueprint_inputs.async_substitute()
-                    config_block = cast(
-                        dict[str, Any],
-                        await async_validate_config_item(hass, raw_config),
-                    )
-                except vol.Invalid as err:
-                    LOGGER.error(
-                        "Blueprint %s generated invalid automation with inputs %s: %s",
-                        blueprint_inputs.blueprint.name,
-                        blueprint_inputs.inputs,
-                        humanize_error(config_block, err),
-                    )
-                    continue
-            else:
-                raw_config = cast(AutomationConfig, config_block).raw_config
-
-            automation_configs.append(
-                AutomationEntityConfig(
-                    config_block, config_key, list_no, raw_blueprint_inputs, raw_config
+            try:
+                raw_config = blueprint_inputs.async_substitute()
+                config_block = cast(
+                    dict[str, Any],
+                    await async_validate_config_item(hass, raw_config),
+                )
+            except vol.Invalid as err:
+                LOGGER.error(
+                    "Blueprint %s generated invalid automation with inputs %s: %s",
+                    blueprint_inputs.blueprint.name,
+                    blueprint_inputs.inputs,
+                    humanize_error(config_block, err),
                 )
+                continue
+        else:
+            raw_config = cast(AutomationConfig, config_block).raw_config
+
+        automation_configs.append(
+            AutomationEntityConfig(
+                config_block, list_no, raw_blueprint_inputs, raw_config
             )
+        )
 
     return automation_configs
 
@@ -722,9 +720,8 @@ async def _prepare_automation_config(
 def _automation_name(automation_config: AutomationEntityConfig) -> str:
     """Return the configured name of an automation."""
     config_block = automation_config.config_block
-    config_key = automation_config.config_key
     list_no = automation_config.list_no
-    return config_block.get(CONF_ALIAS) or f"{config_key} {list_no}"
+    return config_block.get(CONF_ALIAS) or f"{DOMAIN} {list_no}"
 
 
 async def _create_automation_entities(
@@ -855,8 +852,7 @@ async def _async_process_config(
         if idx not in config_matches
     ]
     entities = await _create_automation_entities(hass, updated_automation_configs)
-    if entities:
-        await component.async_add_entities(entities)
+    await component.async_add_entities(entities)
 
     return
 
diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py
index 105309268d1..a1c683faba7 100644
--- a/homeassistant/components/script/__init__.py
+++ b/homeassistant/components/script/__init__.py
@@ -29,7 +29,7 @@ from homeassistant.const import (
     STATE_ON,
 )
 from homeassistant.core import HomeAssistant, ServiceCall, callback
-from homeassistant.helpers import entity_registry as er, extract_domain_configs
+from homeassistant.helpers import entity_registry as er
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.config_validation import make_entity_service_schema
 from homeassistant.helpers.entity import ToggleEntity
@@ -238,37 +238,36 @@ async def _async_process_config(hass, config, component) -> None:
     """
     entities = []
 
-    for config_key in extract_domain_configs(config, DOMAIN):
-        conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key]
-
-        for key, config_block in conf.items():
-            raw_blueprint_inputs = None
-            raw_config = None
-
-            if isinstance(config_block, BlueprintInputs):
-                blueprint_inputs = config_block
-                raw_blueprint_inputs = blueprint_inputs.config_with_inputs
-
-                try:
-                    raw_config = blueprint_inputs.async_substitute()
-                    config_block = cast(
-                        dict[str, Any],
-                        await async_validate_config_item(hass, raw_config),
-                    )
-                except vol.Invalid as err:
-                    LOGGER.error(
-                        "Blueprint %s generated invalid script with input %s: %s",
-                        blueprint_inputs.blueprint.name,
-                        blueprint_inputs.inputs,
-                        humanize_error(config_block, err),
-                    )
-                    continue
-            else:
-                raw_config = cast(ScriptConfig, config_block).raw_config
-
-            entities.append(
-                ScriptEntity(hass, key, config_block, raw_config, raw_blueprint_inputs)
-            )
+    conf: dict[str, dict[str, Any] | BlueprintInputs] = config[DOMAIN]
+
+    for key, config_block in conf.items():
+        raw_blueprint_inputs = None
+        raw_config = None
+
+        if isinstance(config_block, BlueprintInputs):
+            blueprint_inputs = config_block
+            raw_blueprint_inputs = blueprint_inputs.config_with_inputs
+
+            try:
+                raw_config = blueprint_inputs.async_substitute()
+                config_block = cast(
+                    dict[str, Any],
+                    await async_validate_config_item(hass, raw_config),
+                )
+            except vol.Invalid as err:
+                LOGGER.error(
+                    "Blueprint %s generated invalid script with input %s: %s",
+                    blueprint_inputs.blueprint.name,
+                    blueprint_inputs.inputs,
+                    humanize_error(config_block, err),
+                )
+                continue
+        else:
+            raw_config = cast(ScriptConfig, config_block).raw_config
+
+        entities.append(
+            ScriptEntity(hass, key, config_block, raw_config, raw_blueprint_inputs)
+        )
 
     await component.async_add_entities(entities)
 
-- 
GitLab


From 9680cd267fba47f8895b379e1d5da76511a4403f Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 17 Oct 2022 13:17:51 +0200
Subject: [PATCH 537/985] Cleanup sensor private attributes (#80463)

---
 homeassistant/components/sensor/__init__.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index 9ebf225c637..f7fbecea224 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -418,12 +418,8 @@ class SensorEntity(Entity):
         None  # Subclasses of SensorEntity should not set this
     )
     _last_reset_reported = False
-    _temperature_conversion_reported = False
     _sensor_option_unit_of_measurement: str | None = None
 
-    # Temporary private attribute to track if deprecation has been logged.
-    __datetime_as_string_deprecation_logged = False
-
     async def async_internal_added_to_hass(self) -> None:
         """Call when the sensor entity is added to hass."""
         await super().async_internal_added_to_hass()
-- 
GitLab


From a26b3e7a34fbb6ac522e11ca186845795c23a6c2 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 17 Oct 2022 13:59:04 +0200
Subject: [PATCH 538/985] Refactor access to ConfigStore (#80467)

* Refactore access to ConfigStore

* Make tests async

* Reset config during test
---
 homeassistant/core.py | 13 ++++++-------
 tests/test_core.py    | 16 +++++++++-------
 2 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/homeassistant/core.py b/homeassistant/core.py
index 089e7e3b298..73fc18fd3b7 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -1789,6 +1789,8 @@ class Config:
         """Initialize a new config object."""
         self.hass = hass
 
+        self._store = self._ConfigStore(self.hass)
+
         self.latitude: float = 0
         self.longitude: float = 0
         self.elevation: int = 0
@@ -1957,14 +1959,12 @@ class Config:
     async def async_update(self, **kwargs: Any) -> None:
         """Update the configuration from a dictionary."""
         self._update(source=ConfigSource.STORAGE, **kwargs)
-        await self.async_store()
+        await self._async_store()
         self.hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, kwargs)
 
     async def async_load(self) -> None:
         """Load [homeassistant] core config."""
-        store = self._ConfigStore(self.hass)
-
-        if not (data := await store.async_load()):
+        if not (data := await self._store.async_load()):
             return
 
         # In 2021.9 we fixed validation to disallow a path (because that's never correct)
@@ -1994,7 +1994,7 @@ class Config:
             currency=data.get("currency"),
         )
 
-    async def async_store(self) -> None:
+    async def _async_store(self) -> None:
         """Store [homeassistant] core config."""
         data = {
             "latitude": self.latitude,
@@ -2010,8 +2010,7 @@ class Config:
             "currency": self.currency,
         }
 
-        store = self._ConfigStore(self.hass)
-        await store.async_save(data)
+        await self._store.async_save(data)
 
     # Circular dependency prevents us from generating the class at top level
     # pylint: disable-next=import-outside-toplevel
diff --git a/tests/test_core.py b/tests/test_core.py
index 67513ea8b17..017c8b3b607 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -925,7 +925,7 @@ async def test_serviceregistry_callback_service_raise_exception(hass):
     await hass.async_block_till_done()
 
 
-def test_config_defaults():
+async def test_config_defaults():
     """Test config defaults."""
     hass = Mock()
     config = ha.Config(hass)
@@ -950,21 +950,21 @@ def test_config_defaults():
     assert config.currency == "EUR"
 
 
-def test_config_path_with_file():
+async def test_config_path_with_file():
     """Test get_config_path method."""
     config = ha.Config(None)
     config.config_dir = "/test/ha-config"
     assert config.path("test.conf") == "/test/ha-config/test.conf"
 
 
-def test_config_path_with_dir_and_file():
+async def test_config_path_with_dir_and_file():
     """Test get_config_path method."""
     config = ha.Config(None)
     config.config_dir = "/test/ha-config"
     assert config.path("dir", "test.conf") == "/test/ha-config/dir/test.conf"
 
 
-def test_config_as_dict():
+async def test_config_as_dict():
     """Test as dict."""
     config = ha.Config(None)
     config.config_dir = "/test/ha-config"
@@ -994,7 +994,7 @@ def test_config_as_dict():
     assert expected == config.as_dict()
 
 
-def test_config_is_allowed_path():
+async def test_config_is_allowed_path():
     """Test is_allowed_path method."""
     config = ha.Config(None)
     with TemporaryDirectory() as tmp_dir:
@@ -1026,7 +1026,7 @@ def test_config_is_allowed_path():
             config.is_allowed_path(None)
 
 
-def test_config_is_allowed_external_url():
+async def test_config_is_allowed_external_url():
     """Test is_allowed_external_url method."""
     config = ha.Config(None)
     config.allowlist_external_urls = [
@@ -1273,7 +1273,7 @@ async def test_additional_data_in_core_config(hass, hass_storage):
 
 
 async def test_incorrect_internal_external_url(hass, hass_storage, caplog):
-    """Test that we warn when detecting invalid internal/extenral url."""
+    """Test that we warn when detecting invalid internal/external url."""
     config = ha.Config(hass)
 
     hass_storage[ha.CORE_STORAGE_KEY] = {
@@ -1287,6 +1287,8 @@ async def test_incorrect_internal_external_url(hass, hass_storage, caplog):
     assert "Invalid external_url set" not in caplog.text
     assert "Invalid internal_url set" not in caplog.text
 
+    config = ha.Config(hass)
+
     hass_storage[ha.CORE_STORAGE_KEY] = {
         "version": 1,
         "data": {
-- 
GitLab


From c0fc23f794b12879bdc54756bceddf31ec30cc11 Mon Sep 17 00:00:00 2001
From: "Marcus R. Brown" <contact@marcusrbrown.com>
Date: Mon, 17 Oct 2022 06:36:01 -0700
Subject: [PATCH 539/985] Update pychannels to 1.2.3 (#80409)

---
 homeassistant/components/channels/manifest.json | 2 +-
 requirements_all.txt                            | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json
index d167d6b4207..54e06fdc3ab 100644
--- a/homeassistant/components/channels/manifest.json
+++ b/homeassistant/components/channels/manifest.json
@@ -2,7 +2,7 @@
   "domain": "channels",
   "name": "Channels",
   "documentation": "https://www.home-assistant.io/integrations/channels",
-  "requirements": ["pychannels==1.0.0"],
+  "requirements": ["pychannels==1.2.3"],
   "codeowners": [],
   "iot_class": "local_polling",
   "loggers": ["pychannels"]
diff --git a/requirements_all.txt b/requirements_all.txt
index aca8f43c2e5..ed595c08d48 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1478,7 +1478,7 @@ pycarwings2==2.13
 pycfdns==1.2.2
 
 # homeassistant.components.channels
-pychannels==1.0.0
+pychannels==1.2.3
 
 # homeassistant.components.cast
 pychromecast==12.1.4
-- 
GitLab


From f730f96024eb466ee55c33cf6ffad56f0281600f Mon Sep 17 00:00:00 2001
From: Jose Ramirez <jramirez857@users.noreply.github.com>
Date: Mon, 17 Oct 2022 10:09:37 -0400
Subject: [PATCH 540/985] Convert darksky unittest tests to pytest (#79868)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 tests/components/darksky/test_sensor.py  | 175 +++++++++--------------
 tests/components/darksky/test_weather.py |  73 ++++------
 2 files changed, 94 insertions(+), 154 deletions(-)

diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py
index 02bc392cd68..3d1f8fb26bc 100644
--- a/tests/components/darksky/test_sensor.py
+++ b/tests/components/darksky/test_sensor.py
@@ -1,17 +1,14 @@
 """The tests for the Dark Sky platform."""
 from datetime import timedelta
 import re
-import unittest
-from unittest.mock import MagicMock, patch
+from unittest.mock import patch
 
 import forecastio
-from requests.exceptions import HTTPError
-import requests_mock
+from requests.exceptions import ConnectionError as ConnectError
 
-from homeassistant.components.darksky import sensor as darksky
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
 
-from tests.common import get_test_home_assistant, load_fixture
+from tests.common import load_fixture
 
 VALID_CONFIG_MINIMAL = {
     "sensor": {
@@ -69,140 +66,100 @@ INVALID_CONFIG_LANG = {
     }
 }
 
-VALID_CONFIG_ALERTS = {
-    "sensor": {
-        "platform": "darksky",
-        "api_key": "foo",
-        "forecast": [1, 2],
-        "hourly_forecast": [1, 2],
-        "monitored_conditions": ["summary", "icon", "temperature_high", "alerts"],
-        "scan_interval": timedelta(seconds=120),
-    }
-}
 
+async def test_setup_with_config(hass, requests_mock):
+    """Test the platform setup with configuration."""
+    with patch("homeassistant.components.darksky.sensor.forecastio.load_forecast"):
+        assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL)
+        await hass.async_block_till_done()
 
-def load_forecastMock(key, lat, lon, units, lang):  # pylint: disable=invalid-name
-    """Mock darksky forecast loading."""
-    return ""
+        state = hass.states.get("sensor.dark_sky_summary")
+        assert state is not None
 
 
-class TestDarkSkySetup(unittest.TestCase):
-    """Test the Dark Sky platform."""
+async def test_setup_with_invalid_config(hass):
+    """Test the platform setup with invalid configuration."""
+    assert await async_setup_component(hass, "sensor", INVALID_CONFIG_MINIMAL)
+    await hass.async_block_till_done()
 
-    def add_entities(self, new_entities, update_before_add=False):
-        """Mock add entities."""
-        if update_before_add:
-            for entity in new_entities:
-                entity.update()
+    state = hass.states.get("sensor.dark_sky_summary")
+    assert state is None
 
-        for entity in new_entities:
-            self.entities.append(entity)
 
-    def setUp(self):
-        """Initialize values for this testcase class."""
-        self.hass = get_test_home_assistant()
-        self.key = "foo"
-        self.lat = self.hass.config.latitude = 37.8267
-        self.lon = self.hass.config.longitude = -122.423
-        self.entities = []
-        self.addCleanup(self.tear_down_cleanup)
+async def test_setup_with_language_config(hass):
+    """Test the platform setup with language configuration."""
+    with patch("homeassistant.components.darksky.sensor.forecastio.load_forecast"):
+        assert await async_setup_component(hass, "sensor", VALID_CONFIG_LANG_DE)
+        await hass.async_block_till_done()
 
-    def tear_down_cleanup(self):
-        """Stop everything that was started."""
-        self.hass.stop()
+        state = hass.states.get("sensor.dark_sky_summary")
+        assert state is not None
 
-    @patch(
-        "homeassistant.components.darksky.sensor.forecastio.load_forecast",
-        new=load_forecastMock,
-    )
-    def test_setup_with_config(self):
-        """Test the platform setup with configuration."""
-        setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL)
-        self.hass.block_till_done()
 
-        state = self.hass.states.get("sensor.dark_sky_summary")
-        assert state is not None
+async def test_setup_with_invalid_language_config(hass):
+    """Test the platform setup with language configuration."""
+    assert await async_setup_component(hass, "sensor", INVALID_CONFIG_LANG)
+    await hass.async_block_till_done()
 
-    def test_setup_with_invalid_config(self):
-        """Test the platform setup with invalid configuration."""
-        setup_component(self.hass, "sensor", INVALID_CONFIG_MINIMAL)
-        self.hass.block_till_done()
+    state = hass.states.get("sensor.dark_sky_summary")
+    assert state is None
 
-        state = self.hass.states.get("sensor.dark_sky_summary")
-        assert state is None
 
-    @patch(
-        "homeassistant.components.darksky.sensor.forecastio.load_forecast",
-        new=load_forecastMock,
+async def test_setup_bad_api_key(hass, requests_mock):
+    """Test for handling a bad API key."""
+    # The Dark Sky API wrapper that we use raises an HTTP error
+    # when you try to use a bad (or no) API key.
+    url = "https://api.darksky.net/forecast/{}/{},{}?units=auto".format(
+        "foo", str(hass.config.latitude), str(hass.config.longitude)
     )
-    def test_setup_with_language_config(self):
-        """Test the platform setup with language configuration."""
-        setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE)
-        self.hass.block_till_done()
+    msg = f"400 Client Error: Bad Request for url: {url}"
+    requests_mock.get(url, text=msg, status_code=400)
 
-        state = self.hass.states.get("sensor.dark_sky_summary")
-        assert state is not None
+    assert await async_setup_component(
+        hass, "sensor", {"sensor": {"platform": "darksky", "api_key": "foo"}}
+    )
+    await hass.async_block_till_done()
 
-    def test_setup_with_invalid_language_config(self):
-        """Test the platform setup with language configuration."""
-        setup_component(self.hass, "sensor", INVALID_CONFIG_LANG)
-        self.hass.block_till_done()
+    assert hass.states.get("sensor.dark_sky_summary") is None
 
-        state = self.hass.states.get("sensor.dark_sky_summary")
-        assert state is None
 
-    @patch("forecastio.api.get_forecast")
-    def test_setup_bad_api_key(self, mock_get_forecast):
-        """Test for handling a bad API key."""
-        # The Dark Sky API wrapper that we use raises an HTTP error
-        # when you try to use a bad (or no) API key.
-        url = "https://api.darksky.net/forecast/{}/{},{}?units=auto".format(
-            self.key, str(self.lat), str(self.lon)
-        )
-        msg = f"400 Client Error: Bad Request for url: {url}"
-        mock_get_forecast.side_effect = HTTPError(msg)
+async def test_connection_error(hass):
+    """Test setting up with a connection error."""
+    with patch(
+        "homeassistant.components.darksky.sensor.forecastio.load_forecast",
+        side_effect=ConnectError(),
+    ):
+        await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL)
+        await hass.async_block_till_done()
 
-        response = darksky.setup_platform(
-            self.hass, VALID_CONFIG_MINIMAL["sensor"], MagicMock()
-        )
-        assert not response
+        state = hass.states.get("sensor.dark_sky_summary")
+        assert state is None
 
-    @patch(
-        "homeassistant.components.darksky.sensor.forecastio.load_forecast",
-        new=load_forecastMock,
-    )
-    def test_setup_with_alerts_config(self):
-        """Test the platform setup with alert configuration."""
-        setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS)
-        self.hass.block_till_done()
-
-        state = self.hass.states.get("sensor.dark_sky_alerts")
-        assert state.state == "0"
-
-    @requests_mock.Mocker()
-    @patch("forecastio.api.get_forecast", wraps=forecastio.api.get_forecast)
-    def test_setup(self, mock_req, mock_get_forecast):
-        """Test for successfully setting up the forecast.io platform."""
+
+async def test_setup(hass, requests_mock):
+    """Test for successfully setting up the forecast.io platform."""
+    with patch(
+        "forecastio.api.get_forecast", wraps=forecastio.api.get_forecast
+    ) as mock_get_forecast:
         uri = (
             r"https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/"
             r"(-?\d+\.?\d*),(-?\d+\.?\d*)"
         )
-        mock_req.get(re.compile(uri), text=load_fixture("darksky.json"))
+        requests_mock.get(re.compile(uri), text=load_fixture("darksky.json"))
 
-        assert setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL)
-        self.hass.block_till_done()
+        assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL)
+        await hass.async_block_till_done()
 
-        assert mock_get_forecast.called
         assert mock_get_forecast.call_count == 1
-        assert len(self.hass.states.entity_ids()) == 13
+        assert len(hass.states.async_entity_ids()) == 13
 
-        state = self.hass.states.get("sensor.dark_sky_summary")
+        state = hass.states.get("sensor.dark_sky_summary")
         assert state is not None
         assert state.state == "Clear"
         assert state.attributes.get("friendly_name") == "Dark Sky Summary"
-        state = self.hass.states.get("sensor.dark_sky_alerts")
+        state = hass.states.get("sensor.dark_sky_alerts")
         assert state.state == "2"
 
-        state = self.hass.states.get("sensor.dark_sky_daytime_high_temperature_1d")
+        state = hass.states.get("sensor.dark_sky_daytime_high_temperature_1d")
         assert state is not None
         assert state.attributes.get("device_class") == "temperature"
diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py
index 9a1b3912b87..4a982577d48 100644
--- a/tests/components/darksky/test_weather.py
+++ b/tests/components/darksky/test_weather.py
@@ -1,67 +1,50 @@
 """The tests for the Dark Sky weather component."""
 import re
-import unittest
 from unittest.mock import patch
 
 import forecastio
-from requests.exceptions import ConnectionError
-import requests_mock
+from requests.exceptions import ConnectionError as ConnectError
 
 from homeassistant.components import weather
-from homeassistant.setup import setup_component
-from homeassistant.util.unit_system import METRIC_SYSTEM
-
-from tests.common import get_test_home_assistant, load_fixture
-
-
-class TestDarkSky(unittest.TestCase):
-    """Test the Dark Sky weather component."""
-
-    def setUp(self):
-        """Set up things to be run when tests are started."""
-        self.hass = get_test_home_assistant()
-        self.hass.config.units = METRIC_SYSTEM
-        self.lat = self.hass.config.latitude = 37.8267
-        self.lon = self.hass.config.longitude = -122.423
-        self.addCleanup(self.tear_down_cleanup)
-
-    def tear_down_cleanup(self):
-        """Stop down everything that was started."""
-        self.hass.stop()
-
-    @requests_mock.Mocker()
-    @patch("forecastio.api.get_forecast", wraps=forecastio.api.get_forecast)
-    def test_setup(self, mock_req, mock_get_forecast):
-        """Test for successfully setting up the forecast.io platform."""
-        uri = (
-            r"https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/"
-            r"(-?\d+\.?\d*),(-?\d+\.?\d*)"
+from homeassistant.setup import async_setup_component
+
+from tests.common import load_fixture
+
+
+async def test_setup(hass, requests_mock):
+    """Test for successfully setting up the forecast.io platform."""
+    with patch(
+        "forecastio.api.get_forecast", wraps=forecastio.api.get_forecast
+    ) as mock_get_forecast:
+        requests_mock.get(
+            re.compile(
+                r"https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/"
+                r"(-?\d+\.?\d*),(-?\d+\.?\d*)"
+            ),
+            text=load_fixture("darksky.json"),
         )
-        mock_req.get(re.compile(uri), text=load_fixture("darksky.json"))
 
-        assert setup_component(
-            self.hass,
+        assert await async_setup_component(
+            hass,
             weather.DOMAIN,
             {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}},
         )
-        self.hass.block_till_done()
+        await hass.async_block_till_done()
 
-        assert mock_get_forecast.called
         assert mock_get_forecast.call_count == 1
-
-        state = self.hass.states.get("weather.test")
+        state = hass.states.get("weather.test")
         assert state.state == "sunny"
 
-    @patch("forecastio.load_forecast", side_effect=ConnectionError())
-    def test_failed_setup(self, mock_load_forecast):
-        """Test to ensure that a network error does not break component state."""
 
-        assert setup_component(
-            self.hass,
+async def test_failed_setup(hass):
+    """Test to ensure that a network error does not break component state."""
+    with patch("forecastio.load_forecast", side_effect=ConnectError()):
+        assert await async_setup_component(
+            hass,
             weather.DOMAIN,
             {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}},
         )
-        self.hass.block_till_done()
+        await hass.async_block_till_done()
 
-        state = self.hass.states.get("weather.test")
+        state = hass.states.get("weather.test")
         assert state.state == "unavailable"
-- 
GitLab


From 326a1b21ca204fd3c590d0e467a2be2caa57971f Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Mon, 17 Oct 2022 16:14:20 +0200
Subject: [PATCH 541/985] Add message service to LaMetric (#80448)

---
 homeassistant/components/lametric/__init__.py |   4 +-
 homeassistant/components/lametric/const.py    |   3 +
 homeassistant/components/lametric/helpers.py  |  28 +++
 homeassistant/components/lametric/services.py |  96 ++++++++++
 .../components/lametric/services.yaml         | 174 ++++++++++++++++++
 tests/components/lametric/test_helpers.py     |  38 ++++
 tests/components/lametric/test_services.py    | 120 ++++++++++++
 7 files changed, 462 insertions(+), 1 deletion(-)
 create mode 100644 homeassistant/components/lametric/services.py
 create mode 100644 homeassistant/components/lametric/services.yaml
 create mode 100644 tests/components/lametric/test_helpers.py
 create mode 100644 tests/components/lametric/test_services.py

diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py
index 8b7c8b2dca2..5fd531234b8 100644
--- a/homeassistant/components/lametric/__init__.py
+++ b/homeassistant/components/lametric/__init__.py
@@ -12,6 +12,7 @@ from homeassistant.helpers.typing import ConfigType
 
 from .const import DOMAIN, PLATFORMS
 from .coordinator import LaMetricDataUpdateCoordinator
+from .services import async_setup_services
 
 CONFIG_SCHEMA = vol.Schema(
     vol.All(
@@ -31,6 +32,7 @@ CONFIG_SCHEMA = vol.Schema(
 
 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     """Set up the LaMetric integration."""
+    async_setup_services(hass)
     hass.data[DOMAIN] = {"hass_config": config}
     if DOMAIN in config:
         async_create_issue(
@@ -51,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     coordinator = LaMetricDataUpdateCoordinator(hass, entry)
     await coordinator.async_config_entry_first_refresh()
 
-    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
+    hass.data[DOMAIN][entry.entry_id] = coordinator
     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
 
     # Set up notify platform, no entry support for notify component yet,
diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py
index ecaed7b833c..4c6ea64c835 100644
--- a/homeassistant/components/lametric/const.py
+++ b/homeassistant/components/lametric/const.py
@@ -23,3 +23,6 @@ CONF_ICON_TYPE: Final = "icon_type"
 CONF_LIFETIME: Final = "lifetime"
 CONF_PRIORITY: Final = "priority"
 CONF_SOUND: Final = "sound"
+CONF_MESSAGE: Final = "message"
+
+SERVICE_MESSAGE: Final = "message"
diff --git a/homeassistant/components/lametric/helpers.py b/homeassistant/components/lametric/helpers.py
index e9b1b941dae..6ca3157be0c 100644
--- a/homeassistant/components/lametric/helpers.py
+++ b/homeassistant/components/lametric/helpers.py
@@ -7,8 +7,12 @@ from typing import Any, TypeVar
 from demetriek import LaMetricConnectionError, LaMetricError
 from typing_extensions import Concatenate, ParamSpec
 
+from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import device_registry as dr
 
+from .const import DOMAIN
+from .coordinator import LaMetricDataUpdateCoordinator
 from .entity import LaMetricEntity
 
 _LaMetricEntityT = TypeVar("_LaMetricEntityT", bound=LaMetricEntity)
@@ -44,3 +48,27 @@ def lametric_exception_handler(
             ) from error
 
     return handler
+
+
+@callback
+def async_get_coordinator_by_device_id(
+    hass: HomeAssistant, device_id: str
+) -> LaMetricDataUpdateCoordinator:
+    """Get the LaMetric coordinator for this device ID."""
+    device_registry = dr.async_get(hass)
+
+    if (device_entry := device_registry.async_get(device_id)) is None:
+        raise ValueError(f"Unknown LaMetric device ID: {device_id}")
+
+    for entry_id in device_entry.config_entries:
+        if (
+            (entry := hass.config_entries.async_get_entry(entry_id))
+            and entry.domain == DOMAIN
+            and entry.entry_id in hass.data[DOMAIN]
+        ):
+            coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][
+                entry.entry_id
+            ]
+            return coordinator
+
+    raise ValueError(f"No coordinator for device ID: {device_id}")
diff --git a/homeassistant/components/lametric/services.py b/homeassistant/components/lametric/services.py
new file mode 100644
index 00000000000..be8dea5d7bf
--- /dev/null
+++ b/homeassistant/components/lametric/services.py
@@ -0,0 +1,96 @@
+"""Support for LaMetric time services."""
+from demetriek import (
+    AlarmSound,
+    LaMetricError,
+    Model,
+    Notification,
+    NotificationIconType,
+    NotificationPriority,
+    NotificationSound,
+    Simple,
+    Sound,
+)
+import voluptuous as vol
+
+from homeassistant.const import CONF_DEVICE_ID, CONF_ICON
+from homeassistant.core import HomeAssistant, ServiceCall, callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import config_validation as cv
+
+from .const import (
+    CONF_CYCLES,
+    CONF_ICON_TYPE,
+    CONF_MESSAGE,
+    CONF_PRIORITY,
+    CONF_SOUND,
+    DOMAIN,
+    SERVICE_MESSAGE,
+)
+from .coordinator import LaMetricDataUpdateCoordinator
+from .helpers import async_get_coordinator_by_device_id
+
+SERVICE_MESSAGE_SCHEMA = vol.Schema(
+    {
+        vol.Required(CONF_DEVICE_ID): cv.string,
+        vol.Required(CONF_MESSAGE): cv.string,
+        vol.Optional(CONF_CYCLES, default=1): cv.positive_int,
+        vol.Optional(CONF_ICON_TYPE, default=NotificationIconType.NONE): vol.Coerce(
+            NotificationIconType
+        ),
+        vol.Optional(CONF_PRIORITY, default=NotificationPriority.INFO): vol.Coerce(
+            NotificationPriority
+        ),
+        vol.Optional(CONF_SOUND): vol.Any(
+            vol.Coerce(AlarmSound), vol.Coerce(NotificationSound)
+        ),
+        vol.Optional(CONF_ICON): cv.string,
+    }
+)
+
+
+@callback
+def async_setup_services(hass: HomeAssistant) -> None:
+    """Set up services for the LaMetric integration."""
+
+    async def _async_service_text(call: ServiceCall) -> None:
+        """Send a message to a LaMetric device."""
+        coordinator = async_get_coordinator_by_device_id(
+            hass, call.data[CONF_DEVICE_ID]
+        )
+        await async_service_text(coordinator, call)
+
+    hass.services.async_register(
+        DOMAIN,
+        SERVICE_MESSAGE,
+        _async_service_text,
+        schema=SERVICE_MESSAGE_SCHEMA,
+    )
+
+
+async def async_service_text(
+    coordinator: LaMetricDataUpdateCoordinator, call: ServiceCall
+) -> None:
+    """Send a message to an LaMetric device."""
+    sound = None
+    if CONF_SOUND in call.data:
+        sound = Sound(id=call.data[CONF_SOUND], category=None)
+
+    notification = Notification(
+        icon_type=NotificationIconType(call.data[CONF_ICON_TYPE]),
+        priority=NotificationPriority(call.data.get(CONF_PRIORITY)),
+        model=Model(
+            frames=[
+                Simple(
+                    icon=call.data.get(CONF_ICON),
+                    text=call.data[CONF_MESSAGE],
+                )
+            ],
+            cycles=call.data[CONF_CYCLES],
+            sound=sound,
+        ),
+    )
+
+    try:
+        await coordinator.lametric.notify(notification=notification)
+    except LaMetricError as ex:
+        raise HomeAssistantError("Could not send LaMetric notification") from ex
diff --git a/homeassistant/components/lametric/services.yaml b/homeassistant/components/lametric/services.yaml
new file mode 100644
index 00000000000..28cf982b4cd
--- /dev/null
+++ b/homeassistant/components/lametric/services.yaml
@@ -0,0 +1,174 @@
+message:
+  name: Display a message
+  description: Display a message with an optional icon on a LaMetric device.
+  fields:
+    device_id:
+      name: Device
+      description: The LaMetric device to display the message on.
+      required: true
+      selector:
+        device:
+          integration: lametric
+    message:
+      name: Message
+      description: The message to display.
+      required: true
+      selector:
+        text:
+    icon:
+      name: Icon
+      description: >-
+        The ID number of the icon or animation to display. List of all icons
+        and their IDs can be found at: https://developer.lametric.com/icons
+      required: false
+      selector:
+        text:
+    sound:
+      name: Sound
+      description: The notification sound to play.
+      required: false
+      selector:
+        select:
+          options:
+            - label: "Alarm 1"
+              value: "alarm1"
+            - label: "Alarm 2"
+              value: "alarm2"
+            - label: "Alarm 3"
+              value: "alarm3"
+            - label: "Alarm 4"
+              value: "alarm4"
+            - label: "Alarm 5"
+              value: "alarm5"
+            - label: "Alarm 6"
+              value: "alarm6"
+            - label: "Alarm 7"
+              value: "alarm7"
+            - label: "Alarm 8"
+              value: "alarm8"
+            - label: "Alarm 9"
+              value: "alarm9"
+            - label: "Alarm 10"
+              value: "alarm10"
+            - label: "Alarm 11"
+              value: "alarm11"
+            - label: "Alarm 12"
+              value: "alarm12"
+            - label: "Alarm 13"
+              value: "alarm13"
+            - label: "Bicycle"
+              value: "bicycle"
+            - label: "Car"
+              value: "car"
+            - label: "Cash"
+              value: "cash"
+            - label: "Cat"
+              value: "cat"
+            - label: "Dog 1"
+              value: "dog"
+            - label: "Dog 2"
+              value: "dog2"
+            - label: "Energy"
+              value: "energy"
+            - label: "Knock knock"
+              value: "knock-knock"
+            - label: "Letter email"
+              value: "letter_email"
+            - label: "Lose 1"
+              value: "lose1"
+            - label: "Lose 2"
+              value: "lose2"
+            - label: "Negative 1"
+              value: "negative1"
+            - label: "Negative 2"
+              value: "negative2"
+            - label: "Negative 3"
+              value: "negative3"
+            - label: "Negative 4"
+              value: "negative4"
+            - label: "Negative 5"
+              value: "negative5"
+            - label: "Notification 1"
+              value: "notification"
+            - label: "Notification 2"
+              value: "notification2"
+            - label: "Notification 3"
+              value: "notification3"
+            - label: "Notification 4"
+              value: "notification4"
+            - label: "Open door"
+              value: "open_door"
+            - label: "Positive 1"
+              value: "positive1"
+            - label: "Positive 2"
+              value: "positive2"
+            - label: "Positive 3"
+              value: "positive3"
+            - label: "Positive 4"
+              value: "positive4"
+            - label: "Positive 5"
+              value: "positive5"
+            - label: "Positive 6"
+              value: "positive6"
+            - label: "Static"
+              value: "static"
+            - label: "Thunder"
+              value: "thunder"
+            - label: "Water 1"
+              value: "water1"
+            - label: "Water 2"
+              value: "water2"
+            - label: "Win 1"
+              value: "win"
+            - label: "Win 2"
+              value: "win2"
+            - label: "Wind"
+              value: "wind"
+            - label: "Wind short"
+              value: "wind_short"
+    cycles:
+      name: Cycles
+      description: >-
+        The number of times to display the message. When set to 0, the message
+        will be displayed until dismissed.
+      required: false
+      default: 1
+      selector:
+        number:
+          min: 0
+          max: 10
+          mode: slider
+    icon_type:
+      name: Icon type
+      description: >-
+        The type of icon to display, indicating the nature of the notification.
+      required: false
+      default: "none"
+      selector:
+        select:
+          mode: dropdown
+          options:
+            - label: "None"
+              value: "none"
+            - label: "Info"
+              value: "info"
+            - label: "Alert"
+              value: "alert"
+    priority:
+      name: Priority
+      description: >-
+        The priority of the notification. When the device is running in
+        screensaver or kiosk mode, only critical priority notifications
+        will be accepted.
+      required: false
+      default: "info"
+      selector:
+        select:
+          mode: dropdown
+          options:
+            - label: "Info"
+              value: "info"
+            - label: "Warning"
+              value: "warning"
+            - label: "Critical"
+              value: "critical"
diff --git a/tests/components/lametric/test_helpers.py b/tests/components/lametric/test_helpers.py
new file mode 100644
index 00000000000..9a03a4d52cf
--- /dev/null
+++ b/tests/components/lametric/test_helpers.py
@@ -0,0 +1,38 @@
+"""Tests for the LaMetric helpers."""
+from unittest.mock import MagicMock
+
+import pytest
+
+from homeassistant.components.lametric.helpers import async_get_coordinator_by_device_id
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from tests.common import MockConfigEntry
+
+
+async def test_get_coordinator_by_device_id(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_lametric: MagicMock,
+) -> None:
+    """Test get LaMetric coordinator by device ID ."""
+    entity_registry = er.async_get(hass)
+
+    with pytest.raises(ValueError, match="Unknown LaMetric device ID: bla"):
+        async_get_coordinator_by_device_id(hass, "bla")
+
+    entry = entity_registry.async_get("button.frenck_s_lametric_next_app")
+    assert entry
+    assert entry.device_id
+
+    coordinator = async_get_coordinator_by_device_id(hass, entry.device_id)
+    assert coordinator.data == mock_lametric.device.return_value
+
+    # Unload entry
+    await hass.config_entries.async_unload(init_integration.entry_id)
+    await hass.async_block_till_done()
+
+    with pytest.raises(
+        ValueError, match=f"No coordinator for device ID: {entry.device_id}"
+    ):
+        async_get_coordinator_by_device_id(hass, entry.device_id)
diff --git a/tests/components/lametric/test_services.py b/tests/components/lametric/test_services.py
new file mode 100644
index 00000000000..50427755769
--- /dev/null
+++ b/tests/components/lametric/test_services.py
@@ -0,0 +1,120 @@
+"""Tests for the LaMetric services."""
+from unittest.mock import MagicMock
+
+from demetriek import (
+    LaMetricError,
+    Notification,
+    NotificationIconType,
+    NotificationPriority,
+    NotificationSound,
+    NotificationSoundCategory,
+    Simple,
+)
+import pytest
+
+from homeassistant.components.lametric.const import (
+    CONF_CYCLES,
+    CONF_ICON_TYPE,
+    CONF_MESSAGE,
+    CONF_PRIORITY,
+    CONF_SOUND,
+    DOMAIN,
+    SERVICE_MESSAGE,
+)
+from homeassistant.const import CONF_DEVICE_ID, CONF_ICON
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import entity_registry as er
+
+from tests.common import MockConfigEntry
+
+
+async def test_service_message(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_lametric: MagicMock,
+) -> None:
+    """Test the LaMetric text service."""
+    entity_registry = er.async_get(hass)
+
+    entry = entity_registry.async_get("button.frenck_s_lametric_next_app")
+    assert entry
+    assert entry.device_id
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_MESSAGE,
+        {
+            CONF_DEVICE_ID: entry.device_id,
+            CONF_MESSAGE: "Hi!",
+        },
+        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 is None
+    assert frame.text == "Hi!"
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_MESSAGE,
+        {
+            CONF_DEVICE_ID: entry.device_id,
+            CONF_MESSAGE: "Meow!",
+            CONF_CYCLES: 3,
+            CONF_ICON_TYPE: "info",
+            CONF_PRIORITY: "critical",
+            CONF_SOUND: "cat",
+            CONF_ICON: "6916",
+        },
+        blocking=True,
+    )
+
+    assert len(mock_lametric.notify.mock_calls) == 2
+
+    notification: Notification = mock_lametric.notify.mock_calls[1][2]["notification"]
+    assert notification.icon_type is NotificationIconType.INFO
+    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.CAT
+    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 == 6916
+    assert frame.text == "Meow!"
+
+    mock_lametric.notify.side_effect = LaMetricError
+    with pytest.raises(
+        HomeAssistantError, match="Could not send LaMetric notification"
+    ):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_MESSAGE,
+            {
+                CONF_DEVICE_ID: entry.device_id,
+                CONF_MESSAGE: "Epic failure!",
+            },
+            blocking=True,
+        )
+
+    assert len(mock_lametric.notify.mock_calls) == 3
-- 
GitLab


From 5ead3b26056857bfa9a95cdb9e0657969386431d Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
Date: Tue, 18 Oct 2022 03:30:33 +1100
Subject: [PATCH 542/985] Powerview Add Battery Option Selection (#80166)

Co-authored-by: J. Nick Koston <nick@koston.org>
---
 .coveragerc                                   |   1 +
 .../hunterdouglas_powerview/__init__.py       |   8 +-
 .../hunterdouglas_powerview/const.py          |   7 +
 .../hunterdouglas_powerview/select.py         | 121 ++++++++++++++++++
 4 files changed, 136 insertions(+), 1 deletion(-)
 create mode 100644 homeassistant/components/hunterdouglas_powerview/select.py

diff --git a/.coveragerc b/.coveragerc
index ecfa407013d..636d506de14 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -538,6 +538,7 @@ omit =
     homeassistant/components/hunterdouglas_powerview/entity.py
     homeassistant/components/hunterdouglas_powerview/model.py
     homeassistant/components/hunterdouglas_powerview/scene.py
+    homeassistant/components/hunterdouglas_powerview/select.py
     homeassistant/components/hunterdouglas_powerview/sensor.py
     homeassistant/components/hunterdouglas_powerview/shade_data.py
     homeassistant/components/hunterdouglas_powerview/util.py
diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py
index f7c8cd1eb4b..4b0d666d2ae 100644
--- a/homeassistant/components/hunterdouglas_powerview/__init__.py
+++ b/homeassistant/components/hunterdouglas_powerview/__init__.py
@@ -42,7 +42,13 @@ PARALLEL_UPDATES = 1
 
 CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
 
-PLATFORMS = [Platform.BUTTON, Platform.COVER, Platform.SCENE, Platform.SENSOR]
+PLATFORMS = [
+    Platform.BUTTON,
+    Platform.COVER,
+    Platform.SCENE,
+    Platform.SELECT,
+    Platform.SENSOR,
+]
 _LOGGER = logging.getLogger(__name__)
 
 
diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py
index 5dd59fba39c..7dd4c229c48 100644
--- a/homeassistant/components/hunterdouglas_powerview/const.py
+++ b/homeassistant/components/hunterdouglas_powerview/const.py
@@ -83,3 +83,10 @@ ATTR_BATTERY_KIND = "batteryKind"
 BATTERY_KIND_HARDWIRED = 1
 BATTERY_KIND_BATTERY = 2
 BATTERY_KIND_RECHARGABLE = 3
+
+POWER_SUPPLY_TYPE_MAP = {
+    BATTERY_KIND_HARDWIRED: "Hardwired Power Supply",
+    BATTERY_KIND_BATTERY: "Battery Wand",
+    BATTERY_KIND_RECHARGABLE: "Rechargeable Battery",
+}
+POWER_SUPPLY_TYPE_REVERSE_MAP = {v: k for k, v in POWER_SUPPLY_TYPE_MAP.items()}
diff --git a/homeassistant/components/hunterdouglas_powerview/select.py b/homeassistant/components/hunterdouglas_powerview/select.py
new file mode 100644
index 00000000000..e440d0b7d27
--- /dev/null
+++ b/homeassistant/components/hunterdouglas_powerview/select.py
@@ -0,0 +1,121 @@
+"""Support for hunterdouglass_powerview settings."""
+from __future__ import annotations
+
+from collections.abc import Callable, Coroutine
+from dataclasses import dataclass
+from typing import Any, Final
+
+from aiopvapi.resources.shade import BaseShade, factory as PvShade
+
+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 (
+    ATTR_BATTERY_KIND,
+    DOMAIN,
+    POWER_SUPPLY_TYPE_MAP,
+    POWER_SUPPLY_TYPE_REVERSE_MAP,
+    ROOM_ID_IN_SHADE,
+    ROOM_NAME_UNICODE,
+    SHADE_BATTERY_LEVEL,
+)
+from .coordinator import PowerviewShadeUpdateCoordinator
+from .entity import ShadeEntity
+from .model import PowerviewDeviceInfo, PowerviewEntryData
+
+
+@dataclass
+class PowerviewSelectDescriptionMixin:
+    """Mixin to describe a select entity."""
+
+    current_fn: Callable[[BaseShade], Any]
+    select_fn: Callable[[BaseShade, str], Coroutine[Any, Any, bool]]
+
+
+@dataclass
+class PowerviewSelectDescription(
+    SelectEntityDescription, PowerviewSelectDescriptionMixin
+):
+    """A class that describes select entities."""
+
+    entity_category: EntityCategory = EntityCategory.CONFIG
+
+
+DROPDOWNS: Final = [
+    PowerviewSelectDescription(
+        key="powersource",
+        name="Power Source",
+        icon="mdi:power-plug-outline",
+        current_fn=lambda shade: POWER_SUPPLY_TYPE_MAP.get(
+            shade.raw_data.get(ATTR_BATTERY_KIND), None
+        ),
+        options=list(POWER_SUPPLY_TYPE_MAP.values()),
+        select_fn=lambda shade, option: shade.set_power_source(
+            POWER_SUPPLY_TYPE_REVERSE_MAP.get(option)
+        ),
+    ),
+]
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+    """Set up the hunter douglas select entities."""
+
+    pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
+
+    entities = []
+    for raw_shade in pv_entry.shade_data.values():
+        shade: BaseShade = PvShade(raw_shade, pv_entry.api)
+        if SHADE_BATTERY_LEVEL not in shade.raw_data:
+            continue
+        name_before_refresh = shade.name
+        room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
+        room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
+
+        for description in DROPDOWNS:
+            entities.append(
+                PowerViewSelect(
+                    pv_entry.coordinator,
+                    pv_entry.device_info,
+                    room_name,
+                    shade,
+                    name_before_refresh,
+                    description,
+                )
+            )
+
+    async_add_entities(entities)
+
+
+class PowerViewSelect(ShadeEntity, SelectEntity):
+    """Representation of a select entity."""
+
+    def __init__(
+        self,
+        coordinator: PowerviewShadeUpdateCoordinator,
+        device_info: PowerviewDeviceInfo,
+        room_name: str,
+        shade: BaseShade,
+        name: str,
+        description: PowerviewSelectDescription,
+    ) -> None:
+        """Initialize the select entity."""
+        super().__init__(coordinator, device_info, room_name, shade, name)
+        self.entity_description: PowerviewSelectDescription = description
+        self._attr_name = f"{self._shade_name} {description.name}"
+        self._attr_unique_id = f"{self._attr_unique_id}_{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._shade)
+
+    async def async_select_option(self, option: str) -> None:
+        """Change the selected option."""
+        await self.entity_description.select_fn(self._shade, option)
+        await self._shade.refresh()  # force update data to ensure new info is in coordinator
+        self.async_write_ha_state()
-- 
GitLab


From 72f4665d33e96e19d634510bd593dc97ff44e6d9 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Mon, 17 Oct 2022 11:29:10 -0600
Subject: [PATCH 543/985] Streamline Enphase Envoy config flow tests (#79914)

* Streamline Enphase Envoy config flow tests

* Don't test data results using constants

* Fix data issues

* Fixtures

* Simplify mock creation

* Docstrings
---
 tests/components/enphase_envoy/conftest.py    | 110 +++++
 .../enphase_envoy/fixtures/__init__.py        |   1 +
 .../enphase_envoy/fixtures/data.json          |  10 +
 .../fixtures/inverters_production.json        |  18 +
 .../enphase_envoy/test_config_flow.py         | 406 ++++++------------
 5 files changed, 278 insertions(+), 267 deletions(-)
 create mode 100644 tests/components/enphase_envoy/conftest.py
 create mode 100644 tests/components/enphase_envoy/fixtures/__init__.py
 create mode 100644 tests/components/enphase_envoy/fixtures/data.json
 create mode 100644 tests/components/enphase_envoy/fixtures/inverters_production.json

diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py
new file mode 100644
index 00000000000..cb01e8a81e4
--- /dev/null
+++ b/tests/components/enphase_envoy/conftest.py
@@ -0,0 +1,110 @@
+"""Define test fixtures for Enphase Envoy."""
+import json
+from unittest.mock import AsyncMock, Mock, patch
+
+import pytest
+
+from homeassistant.components.enphase_envoy import DOMAIN
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockConfigEntry, load_fixture
+
+
+@pytest.fixture(name="config_entry")
+def config_entry_fixture(hass: HomeAssistant, config, serial_number):
+    """Define a config entry fixture."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        title=f"Envoy {serial_number}" if serial_number else "Envoy",
+        unique_id=serial_number,
+        data=config,
+    )
+    entry.add_to_hass(hass)
+    return entry
+
+
+@pytest.fixture(name="config")
+def config_fixture():
+    """Define a config entry data fixture."""
+    return {
+        CONF_HOST: "1.1.1.1",
+        CONF_NAME: "Envoy 1234",
+        CONF_USERNAME: "test-username",
+        CONF_PASSWORD: "test-password",
+    }
+
+
+@pytest.fixture(name="gateway_data", scope="session")
+def gateway_data_fixture():
+    """Define a fixture to return gateway data."""
+    return json.loads(load_fixture("data.json", "enphase_envoy"))
+
+
+@pytest.fixture(name="inverters_production_data", scope="session")
+def inverters_production_data_fixture():
+    """Define a fixture to return inverter production data."""
+    return json.loads(load_fixture("inverters_production.json", "enphase_envoy"))
+
+
+@pytest.fixture(name="mock_envoy_reader")
+def mock_envoy_reader_fixture(
+    gateway_data,
+    mock_get_data,
+    mock_get_full_serial_number,
+    mock_inverters_production,
+    serial_number,
+):
+    """Define a mocked EnvoyReader fixture."""
+    mock_envoy_reader = Mock(
+        getData=mock_get_data,
+        get_full_serial_number=mock_get_full_serial_number,
+        inverters_production=mock_inverters_production,
+    )
+
+    for key, value in gateway_data.items():
+        setattr(mock_envoy_reader, key, AsyncMock(return_value=value))
+
+    return mock_envoy_reader
+
+
+@pytest.fixture(name="mock_get_full_serial_number")
+def mock_get_full_serial_number_fixture(serial_number):
+    """Define a mocked EnvoyReader.get_full_serial_number fixture."""
+    return AsyncMock(return_value=serial_number)
+
+
+@pytest.fixture(name="mock_get_data")
+def mock_get_data_fixture():
+    """Define a mocked EnvoyReader.getData fixture."""
+    return AsyncMock()
+
+
+@pytest.fixture(name="mock_inverters_production")
+def mock_inverters_production_fixture(inverters_production_data):
+    """Define a mocked EnvoyReader.inverters_production fixture."""
+    return AsyncMock(return_value=inverters_production_data)
+
+
+@pytest.fixture(name="setup_enphase_envoy")
+async def setup_enphase_envoy_fixture(hass, config, mock_envoy_reader):
+    """Define a fixture to set up Enphase Envoy."""
+    with patch(
+        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader",
+        return_value=mock_envoy_reader,
+    ), patch(
+        "homeassistant.components.enphase_envoy.EnvoyReader",
+        return_value=mock_envoy_reader,
+    ), patch(
+        "homeassistant.components.enphase_envoy.PLATFORMS", []
+    ):
+        assert await async_setup_component(hass, DOMAIN, config)
+        await hass.async_block_till_done()
+        yield
+
+
+@pytest.fixture(name="serial_number")
+def serial_number_fixture():
+    """Define a serial number fixture."""
+    return "1234"
diff --git a/tests/components/enphase_envoy/fixtures/__init__.py b/tests/components/enphase_envoy/fixtures/__init__.py
new file mode 100644
index 00000000000..b3ef7db17a3
--- /dev/null
+++ b/tests/components/enphase_envoy/fixtures/__init__.py
@@ -0,0 +1 @@
+"""Define data fixtures for Enphase Envoy."""
diff --git a/tests/components/enphase_envoy/fixtures/data.json b/tests/components/enphase_envoy/fixtures/data.json
new file mode 100644
index 00000000000..d6868a6dbf7
--- /dev/null
+++ b/tests/components/enphase_envoy/fixtures/data.json
@@ -0,0 +1,10 @@
+{
+  "production": 1840,
+  "daily_production": 28223,
+  "seven_days_production": 174482,
+  "lifetime_production": 5924391,
+  "consumption": 1840,
+  "daily_consumption": 5923857,
+  "seven_days_consumption": 5923857,
+  "lifetime_consumption": 5923857
+}
diff --git a/tests/components/enphase_envoy/fixtures/inverters_production.json b/tests/components/enphase_envoy/fixtures/inverters_production.json
new file mode 100644
index 00000000000..14891f2d278
--- /dev/null
+++ b/tests/components/enphase_envoy/fixtures/inverters_production.json
@@ -0,0 +1,18 @@
+{
+  "202140024014": [136, "2022-10-08 16:43:36"],
+  "202140023294": [163, "2022-10-08 16:43:41"],
+  "202140013819": [130, "2022-10-08 16:43:31"],
+  "202140023794": [139, "2022-10-08 16:43:38"],
+  "202140023381": [130, "2022-10-08 16:43:47"],
+  "202140024176": [54, "2022-10-08 16:43:59"],
+  "202140003284": [132, "2022-10-08 16:43:55"],
+  "202140019854": [129, "2022-10-08 16:43:58"],
+  "202140020743": [131, "2022-10-08 16:43:49"],
+  "202140023531": [28, "2022-10-08 16:43:53"],
+  "202140024241": [164, "2022-10-08 16:43:33"],
+  "202140022963": [164, "2022-10-08 16:43:41"],
+  "202140023149": [118, "2022-10-08 16:43:47"],
+  "202140024828": [129, "2022-10-08 16:43:36"],
+  "202140023269": [133, "2022-10-08 16:43:43"],
+  "202140024157": [112, "2022-10-08 16:43:52"]
+}
diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py
index caba2296927..fac5b01c60e 100644
--- a/tests/components/enphase_envoy/test_config_flow.py
+++ b/tests/components/enphase_envoy/test_config_flow.py
@@ -1,46 +1,31 @@
 """Test the Enphase Envoy config flow."""
-from unittest.mock import MagicMock, patch
+from unittest.mock import AsyncMock, MagicMock
 
 import httpx
+import pytest
 
 from homeassistant import config_entries
 from homeassistant.components import zeroconf
 from homeassistant.components.enphase_envoy.const import DOMAIN
-from homeassistant.const import CONF_HOST
 from homeassistant.core import HomeAssistant
 
-from tests.common import MockConfigEntry
 
-
-async def test_form(hass: HomeAssistant) -> None:
+async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None:
     """Test we get the 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.enphase_envoy.config_flow.EnvoyReader.getData",
-        return_value=True,
-    ), patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number",
-        return_value="1234",
-    ), patch(
-        "homeassistant.components.enphase_envoy.async_setup_entry",
-        return_value=True,
-    ) as mock_setup_entry:
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-        await hass.async_block_till_done()
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "create_entry"
     assert result2["title"] == "Envoy 1234"
     assert result2["data"] == {
@@ -49,38 +34,27 @@ async def test_form(hass: HomeAssistant) -> None:
         "username": "test-username",
         "password": "test-password",
     }
-    assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_user_no_serial_number(hass: HomeAssistant) -> None:
+@pytest.mark.parametrize("serial_number", [None])
+async def test_user_no_serial_number(
+    hass: HomeAssistant, config, setup_enphase_envoy
+) -> None:
     """Test user setup without a serial number."""
-
     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.enphase_envoy.config_flow.EnvoyReader.getData",
-        return_value=True,
-    ), patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number",
-        return_value=None,
-    ), patch(
-        "homeassistant.components.enphase_envoy.async_setup_entry",
-        return_value=True,
-    ) as mock_setup_entry:
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-        await hass.async_block_till_done()
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "create_entry"
     assert result2["title"] == "Envoy"
     assert result2["data"] == {
@@ -89,40 +63,36 @@ async def test_user_no_serial_number(hass: HomeAssistant) -> None:
         "username": "test-username",
         "password": "test-password",
     }
-    assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_user_fetching_serial_fails(hass: HomeAssistant) -> None:
+@pytest.mark.parametrize(
+    "mock_get_full_serial_number",
+    [
+        AsyncMock(
+            side_effect=httpx.HTTPStatusError(
+                "any", request=MagicMock(), response=MagicMock()
+            )
+        )
+    ],
+)
+async def test_user_fetching_serial_fails(
+    hass: HomeAssistant, setup_enphase_envoy
+) -> None:
     """Test user setup without a serial number."""
-
     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.enphase_envoy.config_flow.EnvoyReader.getData",
-        return_value=True,
-    ), patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.get_full_serial_number",
-        side_effect=httpx.HTTPStatusError(
-            "any", request=MagicMock(), response=MagicMock()
-        ),
-    ), patch(
-        "homeassistant.components.enphase_envoy.async_setup_entry",
-        return_value=True,
-    ) as mock_setup_entry:
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-        await hass.async_block_till_done()
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "create_entry"
     assert result2["title"] == "Envoy"
     assert result2["data"] == {
@@ -131,83 +101,75 @@ async def test_user_fetching_serial_fails(hass: HomeAssistant) -> None:
         "username": "test-username",
         "password": "test-password",
     }
-    assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_form_invalid_auth(hass: HomeAssistant) -> None:
+@pytest.mark.parametrize(
+    "mock_get_data",
+    [
+        AsyncMock(
+            side_effect=httpx.HTTPStatusError(
+                "any", request=MagicMock(), response=MagicMock()
+            )
+        )
+    ],
+)
+async def test_form_invalid_auth(hass: HomeAssistant, setup_enphase_envoy) -> None:
     """Test we handle invalid auth."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": config_entries.SOURCE_USER}
     )
-
-    with patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
-        side_effect=httpx.HTTPStatusError(
-            "any", request=MagicMock(), response=MagicMock()
-        ),
-    ):
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "form"
     assert result2["errors"] == {"base": "invalid_auth"}
 
 
-async def test_form_cannot_connect(hass: HomeAssistant) -> None:
+@pytest.mark.parametrize(
+    "mock_get_data", [AsyncMock(side_effect=httpx.HTTPError("any"))]
+)
+async def test_form_cannot_connect(hass: HomeAssistant, setup_enphase_envoy) -> None:
     """Test we handle cannot connect error."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": config_entries.SOURCE_USER}
     )
-
-    with patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
-        side_effect=httpx.HTTPError("any"),
-    ):
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "form"
     assert result2["errors"] == {"base": "cannot_connect"}
 
 
-async def test_form_unknown_error(hass: HomeAssistant) -> None:
+@pytest.mark.parametrize("mock_get_data", [AsyncMock(side_effect=ValueError)])
+async def test_form_unknown_error(hass: HomeAssistant, setup_enphase_envoy) -> None:
     """Test we handle unknown error."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": config_entries.SOURCE_USER}
     )
-
-    with patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
-        side_effect=ValueError,
-    ):
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "form"
     assert result2["errors"] == {"base": "unknown"}
 
 
-async def test_zeroconf(hass: HomeAssistant) -> None:
+async def test_zeroconf(hass: HomeAssistant, setup_enphase_envoy) -> None:
     """Test we can setup from zeroconf."""
-
     result = await hass.config_entries.flow.async_init(
         DOMAIN,
         context={"source": config_entries.SOURCE_ZEROCONF},
@@ -221,28 +183,17 @@ async def test_zeroconf(hass: HomeAssistant) -> None:
             type="mock_type",
         ),
     )
-    await hass.async_block_till_done()
-
     assert result["type"] == "form"
     assert result["step_id"] == "user"
 
-    with patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
-        return_value=True,
-    ), patch(
-        "homeassistant.components.enphase_envoy.async_setup_entry",
-        return_value=True,
-    ) as mock_setup_entry:
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-        await hass.async_block_till_done()
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "create_entry"
     assert result2["title"] == "Envoy 1234"
     assert result2["result"].unique_id == "1234"
@@ -252,63 +203,34 @@ async def test_zeroconf(hass: HomeAssistant) -> None:
         "username": "test-username",
         "password": "test-password",
     }
-    assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_form_host_already_exists(hass: HomeAssistant) -> None:
+async def test_form_host_already_exists(
+    hass: HomeAssistant, config_entry, setup_enphase_envoy
+) -> None:
     """Test host already exists."""
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        data={
-            "host": "1.1.1.1",
-            "name": "Envoy",
-            "username": "test-username",
-            "password": "test-password",
-        },
-        title="Envoy",
-    )
-    config_entry.add_to_hass(hass)
     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.enphase_envoy.config_flow.EnvoyReader.getData",
-        return_value=True,
-    ):
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-        await hass.async_block_till_done()
-
-    assert result2["type"] == "abort"
-    assert result2["reason"] == "already_configured"
-
-
-async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None:
-    """Test serial number already exists from zeroconf."""
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        data={
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
             "host": "1.1.1.1",
-            "name": "Envoy",
             "username": "test-username",
             "password": "test-password",
         },
-        unique_id="1234",
-        title="Envoy",
     )
-    config_entry.add_to_hass(hass)
+    assert result2["type"] == "abort"
+    assert result2["reason"] == "already_configured"
 
+
+async def test_zeroconf_serial_already_exists(
+    hass: HomeAssistant, config_entry, setup_enphase_envoy
+) -> None:
+    """Test serial number already exists from zeroconf."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN,
         context={"source": config_entries.SOURCE_ZEROCONF},
@@ -322,28 +244,16 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None:
             type="mock_type",
         ),
     )
-
     assert result["type"] == "abort"
     assert result["reason"] == "already_configured"
-    assert config_entry.data[CONF_HOST] == "4.4.4.4"
 
+    assert config_entry.data["host"] == "4.4.4.4"
 
-async def test_zeroconf_serial_already_exists_ignores_ipv6(hass: HomeAssistant) -> None:
-    """Test serial number already exists from zeroconf but the discovery is ipv6."""
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        data={
-            "host": "1.1.1.1",
-            "name": "Envoy",
-            "username": "test-username",
-            "password": "test-password",
-        },
-        unique_id="1234",
-        title="Envoy",
-    )
-    config_entry.add_to_hass(hass)
 
+async def test_zeroconf_serial_already_exists_ignores_ipv6(
+    hass: HomeAssistant, config_entry, setup_enphase_envoy
+) -> None:
+    """Test serial number already exists from zeroconf but the discovery is ipv6."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN,
         context={"source": config_entries.SOURCE_ZEROCONF},
@@ -357,71 +267,39 @@ async def test_zeroconf_serial_already_exists_ignores_ipv6(hass: HomeAssistant)
             type="mock_type",
         ),
     )
-
     assert result["type"] == "abort"
     assert result["reason"] == "not_ipv4_address"
-    assert config_entry.data[CONF_HOST] == "1.1.1.1"
 
+    assert config_entry.data["host"] == "1.1.1.1"
 
-async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None:
-    """Test hosts already exists from zeroconf."""
 
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        data={
-            "host": "1.1.1.1",
-            "name": "Envoy",
-            "username": "test-username",
-            "password": "test-password",
-        },
-        title="Envoy",
+@pytest.mark.parametrize("serial_number", [None])
+async def test_zeroconf_host_already_exists(
+    hass: HomeAssistant, config_entry, setup_enphase_envoy
+) -> None:
+    """Test hosts already exists from zeroconf."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": config_entries.SOURCE_ZEROCONF},
+        data=zeroconf.ZeroconfServiceInfo(
+            host="1.1.1.1",
+            addresses=["1.1.1.1"],
+            hostname="mock_hostname",
+            name="mock_name",
+            port=None,
+            properties={"serialnum": "1234"},
+            type="mock_type",
+        ),
     )
-    config_entry.add_to_hass(hass)
-
-    with patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
-        return_value=True,
-    ), patch(
-        "homeassistant.components.enphase_envoy.async_setup_entry",
-        return_value=True,
-    ) as mock_setup_entry:
-        result = await hass.config_entries.flow.async_init(
-            DOMAIN,
-            context={"source": config_entries.SOURCE_ZEROCONF},
-            data=zeroconf.ZeroconfServiceInfo(
-                host="1.1.1.1",
-                addresses=["1.1.1.1"],
-                hostname="mock_hostname",
-                name="mock_name",
-                port=None,
-                properties={"serialnum": "1234"},
-                type="mock_type",
-            ),
-        )
-        await hass.async_block_till_done()
-
     assert result["type"] == "abort"
     assert result["reason"] == "already_configured"
 
     assert config_entry.unique_id == "1234"
     assert config_entry.title == "Envoy 1234"
-    assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_reauth(hass: HomeAssistant) -> None:
+async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> None:
     """Test we reauth auth."""
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        data={
-            "host": "1.1.1.1",
-            "name": "Envoy",
-            "username": "test-username",
-            "password": "test-password",
-        },
-        title="Envoy",
-    )
-    config_entry.add_to_hass(hass)
     result = await hass.config_entries.flow.async_init(
         DOMAIN,
         context={
@@ -430,19 +308,13 @@ async def test_reauth(hass: HomeAssistant) -> None:
             "entry_id": config_entry.entry_id,
         },
     )
-
-    with patch(
-        "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData",
-        return_value=True,
-    ):
-        result2 = await hass.config_entries.flow.async_configure(
-            result["flow_id"],
-            {
-                "host": "1.1.1.1",
-                "username": "test-username",
-                "password": "test-password",
-            },
-        )
-
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "host": "1.1.1.1",
+            "username": "test-username",
+            "password": "test-password",
+        },
+    )
     assert result2["type"] == "abort"
     assert result2["reason"] == "reauth_successful"
-- 
GitLab


From d3f39e776b79bfb5bf3d10cd1f01d4357eff0f30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B6ren?= <mail@pattyland.de>
Date: Mon, 17 Oct 2022 20:27:08 +0200
Subject: [PATCH 544/985] Fix typo in LED BLE documentation URL (#80479)

---
 homeassistant/components/led_ble/manifest.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json
index 6e20e0fa9cb..6802eea9bc7 100644
--- a/homeassistant/components/led_ble/manifest.json
+++ b/homeassistant/components/led_ble/manifest.json
@@ -2,7 +2,7 @@
   "domain": "led_ble",
   "name": "LED BLE",
   "config_flow": true,
-  "documentation": "https://www.home-assistant.io/integrations/ble_ble",
+  "documentation": "https://www.home-assistant.io/integrations/led_ble/",
   "requirements": ["led-ble==1.0.0"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco"],
-- 
GitLab


From ebf73f41baafd73435d3103766cc2aa7638c83f1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 17 Oct 2022 13:41:57 -0500
Subject: [PATCH 545/985] Bump qingping-ble to 0.8.1 (#80473)

---
 homeassistant/components/qingping/manifest.json | 2 +-
 requirements_all.txt                            | 2 +-
 requirements_test_all.txt                       | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json
index c0fc7347b13..a30795fb42d 100644
--- a/homeassistant/components/qingping/manifest.json
+++ b/homeassistant/components/qingping/manifest.json
@@ -11,7 +11,7 @@
       "connectable": false
     }
   ],
-  "requirements": ["qingping-ble==0.8.0"],
+  "requirements": ["qingping-ble==0.8.1"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco", "@skgsergio"],
   "iot_class": "local_push"
diff --git a/requirements_all.txt b/requirements_all.txt
index ed595c08d48..6aab8532cfe 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2123,7 +2123,7 @@ pyzbar==0.1.7
 pyzerproc==0.4.8
 
 # homeassistant.components.qingping
-qingping-ble==0.8.0
+qingping-ble==0.8.1
 
 # homeassistant.components.qnap
 qnapstats==0.4.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 9729431c1f8..14af4610950 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1471,7 +1471,7 @@ pyws66i==1.1
 pyzerproc==0.4.8
 
 # homeassistant.components.qingping
-qingping-ble==0.8.0
+qingping-ble==0.8.1
 
 # homeassistant.components.rachio
 rachiopy==1.0.3
-- 
GitLab


From a8f1cc0d330372f60e50954b7cf049fcf9a18762 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Mon, 17 Oct 2022 21:11:58 +0200
Subject: [PATCH 546/985] Remove unneeded guards from (async_)add_entities call
 (#80471)

---
 homeassistant/components/aemet/weather.py            |  3 +--
 homeassistant/components/asuswrt/device_tracker.py   |  3 +--
 homeassistant/components/bond/button.py              |  3 +--
 homeassistant/components/bosch_shc/binary_sensor.py  |  3 +--
 homeassistant/components/bosch_shc/cover.py          |  3 +--
 homeassistant/components/bosch_shc/sensor.py         |  3 +--
 homeassistant/components/bosch_shc/switch.py         |  3 +--
 homeassistant/components/daikin/switch.py            |  3 +--
 .../components/devolo_home_network/device_tracker.py |  6 ++----
 homeassistant/components/dominos/__init__.py         |  3 +--
 homeassistant/components/dynalite/dynalitebase.py    |  3 +--
 homeassistant/components/enocean/sensor.py           |  3 +--
 homeassistant/components/firmata/binary_sensor.py    |  3 +--
 homeassistant/components/firmata/light.py            |  3 +--
 homeassistant/components/firmata/sensor.py           |  3 +--
 homeassistant/components/firmata/switch.py           |  3 +--
 homeassistant/components/flume/binary_sensor.py      |  3 +--
 homeassistant/components/flume/sensor.py             |  3 +--
 homeassistant/components/flux_led/number.py          |  3 +--
 homeassistant/components/flux_led/select.py          |  3 +--
 homeassistant/components/flux_led/switch.py          |  3 +--
 homeassistant/components/freebox/device_tracker.py   |  3 +--
 homeassistant/components/fritz/device_tracker.py     |  3 +--
 homeassistant/components/fronius/coordinator.py      |  3 +--
 .../components/homematicip_cloud/binary_sensor.py    |  3 +--
 .../components/homematicip_cloud/climate.py          |  3 +--
 homeassistant/components/homematicip_cloud/cover.py  |  3 +--
 homeassistant/components/homematicip_cloud/light.py  |  3 +--
 homeassistant/components/homematicip_cloud/sensor.py |  3 +--
 homeassistant/components/homematicip_cloud/switch.py |  3 +--
 .../components/homematicip_cloud/weather.py          |  3 +--
 homeassistant/components/icloud/device_tracker.py    |  3 +--
 homeassistant/components/icloud/sensor.py            |  3 +--
 homeassistant/components/insteon/utils.py            |  3 +--
 homeassistant/components/iotawatt/sensor.py          |  3 +--
 .../components/keenetic_ndms2/device_tracker.py      |  6 ++----
 homeassistant/components/life360/device_tracker.py   |  3 +--
 homeassistant/components/lutron_caseta/button.py     |  3 +--
 homeassistant/components/maxcube/binary_sensor.py    |  3 +--
 homeassistant/components/maxcube/climate.py          |  3 +--
 homeassistant/components/mikrotik/device_tracker.py  |  3 +--
 .../components/nederlandse_spoorwegen/sensor.py      |  3 +--
 homeassistant/components/netatmo/sensor.py           |  3 +--
 homeassistant/components/netgear/device_tracker.py   |  3 +--
 homeassistant/components/netgear/sensor.py           |  3 +--
 homeassistant/components/netgear/switch.py           |  3 +--
 homeassistant/components/octoprint/sensor.py         |  3 +--
 homeassistant/components/owntracks/device_tracker.py |  3 +--
 homeassistant/components/plum_lightpad/light.py      |  3 +--
 .../components/ruckus_unleashed/device_tracker.py    |  6 ++----
 homeassistant/components/tado/binary_sensor.py       |  3 +--
 homeassistant/components/tado/climate.py             |  3 +--
 homeassistant/components/tado/sensor.py              |  3 +--
 homeassistant/components/tado/water_heater.py        |  3 +--
 homeassistant/components/unifi/device_tracker.py     |  6 ++----
 homeassistant/components/unifi/sensor.py             |  6 ++----
 homeassistant/components/unifi/switch.py             | 12 ++++--------
 homeassistant/components/unifi/update.py             |  3 +--
 homeassistant/components/wemo/light.py               |  3 +--
 homeassistant/components/wled/light.py               |  3 +--
 homeassistant/components/wled/number.py              |  3 +--
 homeassistant/components/wled/select.py              |  3 +--
 homeassistant/components/wled/switch.py              |  3 +--
 homeassistant/components/xbox/binary_sensor.py       |  3 +--
 homeassistant/components/xbox/sensor.py              |  3 +--
 homeassistant/components/xbox_live/sensor.py         |  3 +--
 66 files changed, 74 insertions(+), 148 deletions(-)

diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py
index a7ff3630e78..695ae283a47 100644
--- a/homeassistant/components/aemet/weather.py
+++ b/homeassistant/components/aemet/weather.py
@@ -84,8 +84,7 @@ async def async_setup_entry(
         unique_id = f"{config_entry.unique_id} {mode}"
         entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
 
-    if entities:
-        async_add_entities(entities, False)
+    async_add_entities(entities, False)
 
 
 class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py
index fc2d4ede26d..efdf4993927 100644
--- a/homeassistant/components/asuswrt/device_tracker.py
+++ b/homeassistant/components/asuswrt/device_tracker.py
@@ -47,8 +47,7 @@ def add_entities(
         new_tracked.append(AsusWrtDevice(router, device))
         tracked.add(mac)
 
-    if new_tracked:
-        async_add_entities(new_tracked)
+    async_add_entities(new_tracked)
 
 
 class AsusWrtDevice(ScannerEntity):
diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py
index 6f57a23b079..32b76c6fcae 100644
--- a/homeassistant/components/bond/button.py
+++ b/homeassistant/components/bond/button.py
@@ -274,8 +274,7 @@ async def async_setup_entry(
             )
         entities.extend(device_entities)
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class BondButtonEntity(BondEntity, ButtonEntity):
diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py
index e76e562f9ef..25ab320a4c4 100644
--- a/homeassistant/components/bosch_shc/binary_sensor.py
+++ b/homeassistant/components/bosch_shc/binary_sensor.py
@@ -53,8 +53,7 @@ async def async_setup_entry(
                 )
             )
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class ShutterContactSensor(SHCEntity, BinarySensorEntity):
diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py
index 91dc361a23d..3f1a9eccb93 100644
--- a/homeassistant/components/bosch_shc/cover.py
+++ b/homeassistant/components/bosch_shc/cover.py
@@ -36,8 +36,7 @@ async def async_setup_entry(
             )
         )
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class ShutterControlCover(SHCEntity, CoverEntity):
diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py
index cb8272e92cf..90fc44710a0 100644
--- a/homeassistant/components/bosch_shc/sensor.py
+++ b/homeassistant/components/bosch_shc/sensor.py
@@ -157,8 +157,7 @@ async def async_setup_entry(
             )
         )
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class TemperatureSensor(SHCEntity, SensorEntity):
diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py
index 28b68ac091b..1bc430c8d4a 100644
--- a/homeassistant/components/bosch_shc/switch.py
+++ b/homeassistant/components/bosch_shc/switch.py
@@ -156,8 +156,7 @@ async def async_setup_entry(
             )
         )
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class SHCSwitch(SHCEntity, SwitchEntity):
diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py
index 0f885e63bf7..23b4b526f9a 100644
--- a/homeassistant/components/daikin/switch.py
+++ b/homeassistant/components/daikin/switch.py
@@ -50,8 +50,7 @@ async def async_setup_entry(
         # device supports the streamer, so assume so if it does support
         # advanced modes.
         switches.append(DaikinStreamerSwitch(daikin_api))
-    if switches:
-        async_add_entities(switches)
+    async_add_entities(switches)
 
 
 class DaikinZoneSwitch(SwitchEntity):
diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py
index 0e3e47d9320..f535136680a 100644
--- a/homeassistant/components/devolo_home_network/device_tracker.py
+++ b/homeassistant/components/devolo_home_network/device_tracker.py
@@ -55,8 +55,7 @@ async def async_setup_entry(
                 )
             )
             tracked.add(station[MAC_ADDRESS])
-            if new_entities:
-                async_add_entities(new_entities)
+            async_add_entities(new_entities)
 
     @callback
     def restore_entities() -> None:
@@ -82,8 +81,7 @@ async def async_setup_entry(
                 )
                 tracked.add(mac_address)
 
-        if missing:
-            async_add_entities(missing)
+        async_add_entities(missing)
 
     if device.device and "wifi1" in device.device.features:
         restore_entities()
diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py
index ecfe7b65a7d..37344ed9d4b 100644
--- a/homeassistant/components/dominos/__init__.py
+++ b/homeassistant/components/dominos/__init__.py
@@ -90,8 +90,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
         order = DominosOrder(order_info, dominos)
         entities.append(order)
 
-    if entities:
-        component.add_entities(entities)
+    component.add_entities(entities)
 
     # Return boolean to indicate that initialization was successfully.
     return True
diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py
index c1814307d1c..b4b8285cbb0 100644
--- a/homeassistant/components/dynalite/dynalitebase.py
+++ b/homeassistant/components/dynalite/dynalitebase.py
@@ -31,8 +31,7 @@ def async_setup_entry_base(
         added_entities = []
         for device in devices:
             added_entities.append(entity_from_device(device, bridge))
-        if added_entities:
-            async_add_entities(added_entities)
+        async_add_entities(added_entities)
 
     bridge.register_add_devices(platform, async_add_entities_platform)
 
diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py
index 84237852e80..ae75dfe25c1 100644
--- a/homeassistant/components/enocean/sensor.py
+++ b/homeassistant/components/enocean/sensor.py
@@ -148,8 +148,7 @@ def setup_platform(
     elif sensor_type == SENSOR_TYPE_WINDOWHANDLE:
         entities = [EnOceanWindowHandle(dev_id, dev_name, SENSOR_DESC_WINDOWHANDLE)]
 
-    if entities:
-        add_entities(entities)
+    add_entities(entities)
 
 
 class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity):
diff --git a/homeassistant/components/firmata/binary_sensor.py b/homeassistant/components/firmata/binary_sensor.py
index 97b6581c420..0cb38d1df8b 100644
--- a/homeassistant/components/firmata/binary_sensor.py
+++ b/homeassistant/components/firmata/binary_sensor.py
@@ -40,8 +40,7 @@ async def async_setup_entry(
         binary_sensor_entity = FirmataBinarySensor(api, config_entry, name, pin)
         new_entities.append(binary_sensor_entity)
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
 
 
 class FirmataBinarySensor(FirmataPinEntity, BinarySensorEntity):
diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py
index 59be2484d66..29504f704bf 100644
--- a/homeassistant/components/firmata/light.py
+++ b/homeassistant/components/firmata/light.py
@@ -46,8 +46,7 @@ async def async_setup_entry(
         light_entity = FirmataLight(api, config_entry, name, pin)
         new_entities.append(light_entity)
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
 
 
 class FirmataLight(FirmataPinEntity, LightEntity):
diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py
index 359090fae1c..3497aec9e88 100644
--- a/homeassistant/components/firmata/sensor.py
+++ b/homeassistant/components/firmata/sensor.py
@@ -40,8 +40,7 @@ async def async_setup_entry(
         sensor_entity = FirmataSensor(api, config_entry, name, pin)
         new_entities.append(sensor_entity)
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
 
 
 class FirmataSensor(FirmataPinEntity, SensorEntity):
diff --git a/homeassistant/components/firmata/switch.py b/homeassistant/components/firmata/switch.py
index 10037bb6a7a..f52300b6db3 100644
--- a/homeassistant/components/firmata/switch.py
+++ b/homeassistant/components/firmata/switch.py
@@ -42,8 +42,7 @@ async def async_setup_entry(
         switch_entity = FirmataSwitch(api, config_entry, name, pin)
         new_entities.append(switch_entity)
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
 
 
 class FirmataSwitch(FirmataPinEntity, SwitchEntity):
diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py
index f6b1f63a26c..2f5ea4323d9 100644
--- a/homeassistant/components/flume/binary_sensor.py
+++ b/homeassistant/components/flume/binary_sensor.py
@@ -122,8 +122,7 @@ async def async_setup_entry(
             ]
         )
 
-    if flume_entity_list:
-        async_add_entities(flume_entity_list)
+    async_add_entities(flume_entity_list)
 
 
 class FlumeNotificationBinarySensor(FlumeEntity, BinarySensorEntity):
diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py
index 09f65f7d891..703744c248e 100644
--- a/homeassistant/components/flume/sensor.py
+++ b/homeassistant/components/flume/sensor.py
@@ -120,8 +120,7 @@ async def async_setup_entry(
             ]
         )
 
-    if flume_entity_list:
-        async_add_entities(flume_entity_list)
+    async_add_entities(flume_entity_list)
 
 
 class FlumeSensor(FlumeEntity, SensorEntity):
diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py
index 18237c97e94..4fe0e250db6 100644
--- a/homeassistant/components/flux_led/number.py
+++ b/homeassistant/components/flux_led/number.py
@@ -89,8 +89,7 @@ async def async_setup_entry(
             FluxSpeedNumber(coordinator, base_unique_id, f"{name} Effect Speed", None)
         )
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class FluxSpeedNumber(
diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py
index 63929740020..9e24cd2cd7d 100644
--- a/homeassistant/components/flux_led/select.py
+++ b/homeassistant/components/flux_led/select.py
@@ -81,8 +81,7 @@ async def async_setup_entry(
     if FLUX_COLOR_MODE_RGBW in device.color_modes:
         entities.append(FluxWhiteChannelSelect(coordinator.device, entry))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class FluxConfigAtStartSelect(FluxBaseEntity, SelectEntity):
diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py
index 18b079beff9..168d16268aa 100644
--- a/homeassistant/components/flux_led/switch.py
+++ b/homeassistant/components/flux_led/switch.py
@@ -48,8 +48,7 @@ async def async_setup_entry(
             FluxMusicSwitch(coordinator, base_unique_id, f"{name} Music", "music")
         )
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class FluxSwitch(
diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py
index bd71588aea0..0b48d08170a 100644
--- a/homeassistant/components/freebox/device_tracker.py
+++ b/homeassistant/components/freebox/device_tracker.py
@@ -48,8 +48,7 @@ def add_entities(
         new_tracked.append(FreeboxDevice(router, device))
         tracked.add(mac)
 
-    if new_tracked:
-        async_add_entities(new_tracked, True)
+    async_add_entities(new_tracked, True)
 
 
 class FreeboxDevice(ScannerEntity):
diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py
index 9591fec156b..998eab2ede7 100644
--- a/homeassistant/components/fritz/device_tracker.py
+++ b/homeassistant/components/fritz/device_tracker.py
@@ -65,8 +65,7 @@ def _async_add_entities(
         new_tracked.append(FritzBoxTracker(avm_wrapper, device))
         data_fritz.tracked[avm_wrapper.unique_id].add(mac)
 
-    if new_tracked:
-        async_add_entities(new_tracked)
+    async_add_entities(new_tracked)
 
 
 class FritzBoxTracker(FritzDeviceBase, ScannerEntity):
diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py
index c1090dab1b1..08779b9b673 100644
--- a/homeassistant/components/fronius/coordinator.py
+++ b/homeassistant/components/fronius/coordinator.py
@@ -104,8 +104,7 @@ class FroniusCoordinatorBase(
                         continue
                     new_entities.append(entity_constructor(self, key, solar_net_id))
                     self.unregistered_keys[solar_net_id].remove(key)
-            if new_entities:
-                async_add_entities(new_entities)
+            async_add_entities(new_entities)
 
         _add_entities_for_unregistered_keys()
         self.solar_net.cleanup_callbacks.append(
diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py
index 6d0fe0c1b96..db35a5d3ee5 100644
--- a/homeassistant/components/homematicip_cloud/binary_sensor.py
+++ b/homeassistant/components/homematicip_cloud/binary_sensor.py
@@ -142,8 +142,7 @@ async def async_setup_entry(
         elif isinstance(group, AsyncSecurityZoneGroup):
             entities.append(HomematicipSecurityZoneSensorGroup(hap, device=group))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEntity):
diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py
index b3d2236f05a..2fc9f8fd12d 100644
--- a/homeassistant/components/homematicip_cloud/climate.py
+++ b/homeassistant/components/homematicip_cloud/climate.py
@@ -51,8 +51,7 @@ async def async_setup_entry(
         if isinstance(device, AsyncHeatingGroup):
             entities.append(HomematicipHeatingGroup(hap, device))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py
index 31faea875a4..7038c423df0 100644
--- a/homeassistant/components/homematicip_cloud/cover.py
+++ b/homeassistant/components/homematicip_cloud/cover.py
@@ -62,8 +62,7 @@ async def async_setup_entry(
         if isinstance(group, AsyncExtendedLinkedShutterGroup):
             entities.append(HomematicipCoverShutterGroup(hap, group))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity):
diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py
index c43ac510023..09a5cd7ec34 100644
--- a/homeassistant/components/homematicip_cloud/light.py
+++ b/homeassistant/components/homematicip_cloud/light.py
@@ -62,8 +62,7 @@ async def async_setup_entry(
         ):
             entities.append(HomematicipDimmer(hap, device))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class HomematicipLight(HomematicipGenericEntity, LightEntity):
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index bb9dd8021ed..03aaa7626b7 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -130,8 +130,7 @@ async def async_setup_entry(
             entities.append(HomematicpTemperatureExternalSensorCh2(hap, device))
             entities.append(HomematicpTemperatureExternalSensorDelta(hap, device))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity):
diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py
index 1b39633bc15..770687ef50d 100644
--- a/homeassistant/components/homematicip_cloud/switch.py
+++ b/homeassistant/components/homematicip_cloud/switch.py
@@ -82,8 +82,7 @@ async def async_setup_entry(
         if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)):
             entities.append(HomematicipGroupSwitch(hap, group))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity):
diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py
index 3acbae95441..a52e99bfa4e 100644
--- a/homeassistant/components/homematicip_cloud/weather.py
+++ b/homeassistant/components/homematicip_cloud/weather.py
@@ -64,8 +64,7 @@ async def async_setup_entry(
 
     entities.append(HomematicipHomeWeather(hap))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py
index 44297a21112..fc1de213a69 100644
--- a/homeassistant/components/icloud/device_tracker.py
+++ b/homeassistant/components/icloud/device_tracker.py
@@ -51,8 +51,7 @@ def add_entities(account: IcloudAccount, async_add_entities, tracked):
         new_tracked.append(IcloudTrackerEntity(account, device))
         tracked.add(dev_id)
 
-    if new_tracked:
-        async_add_entities(new_tracked, True)
+    async_add_entities(new_tracked, True)
 
 
 class IcloudTrackerEntity(TrackerEntity):
diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py
index c3b23057ebc..e7c982607cb 100644
--- a/homeassistant/components/icloud/sensor.py
+++ b/homeassistant/components/icloud/sensor.py
@@ -47,8 +47,7 @@ def add_entities(account, async_add_entities, tracked):
         new_tracked.append(IcloudDeviceBatterySensor(account, device))
         tracked.add(dev_id)
 
-    if new_tracked:
-        async_add_entities(new_tracked, True)
+    async_add_entities(new_tracked, True)
 
 
 class IcloudDeviceBatterySensor(SensorEntity):
diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py
index 09375e7827a..c5dbba9c25b 100644
--- a/homeassistant/components/insteon/utils.py
+++ b/homeassistant/components/insteon/utils.py
@@ -392,5 +392,4 @@ def async_add_insteon_entities(
         groups = get_platform_groups(device, platform)
         for group in groups:
             new_entities.append(entity_type(device, group))
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py
index 8bcf5bfae9a..dd04a32cce4 100644
--- a/homeassistant/components/iotawatt/sensor.py
+++ b/homeassistant/components/iotawatt/sensor.py
@@ -154,8 +154,7 @@ async def async_setup_entry(
             for key in coordinator.data["sensors"]
             if key not in created
         ]
-        if entities:
-            async_add_entities(entities)
+        async_add_entities(entities)
 
     coordinator.async_add_listener(new_data_received)
 
diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py
index a2b01040a5a..4491b644b27 100644
--- a/homeassistant/components/keenetic_ndms2/device_tracker.py
+++ b/homeassistant/components/keenetic_ndms2/device_tracker.py
@@ -64,8 +64,7 @@ async def async_setup_entry(
                     )
                 )
 
-    if restored:
-        async_add_entities(restored)
+    async_add_entities(restored)
 
     async_dispatcher_connect(hass, router.signal_update, update_from_router)
 
@@ -79,8 +78,7 @@ def update_items(router: KeeneticRouter, async_add_entities, tracked: set[str]):
             tracked.add(mac)
             new_tracked.append(KeeneticTracker(device, router))
 
-    if new_tracked:
-        async_add_entities(new_tracked)
+    async_add_entities(new_tracked)
 
 
 class KeeneticTracker(ScannerEntity):
diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py
index 2c05b944a27..a6ca0a16aa3 100644
--- a/homeassistant/components/life360/device_tracker.py
+++ b/homeassistant/components/life360/device_tracker.py
@@ -87,8 +87,7 @@ async def async_setup_entry(
                 and not new_members_only
             ):
                 new_entities.append(Life360DeviceTracker(coordinator, member_id))
-        if new_entities:
-            async_add_entities(new_entities)
+        async_add_entities(new_entities)
 
     process_data(new_members_only=False)
     entry.async_on_unload(coordinator.async_add_listener(process_data))
diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py
index 713c711f675..b8cbb23774a 100644
--- a/homeassistant/components/lutron_caseta/button.py
+++ b/homeassistant/components/lutron_caseta/button.py
@@ -67,8 +67,7 @@ async def async_setup_entry(
             ),
         )
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class LutronCasetaButton(LutronCasetaDevice, ButtonEntity):
diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py
index c6c9ba8ebb0..051ef02ed25 100644
--- a/homeassistant/components/maxcube/binary_sensor.py
+++ b/homeassistant/components/maxcube/binary_sensor.py
@@ -28,8 +28,7 @@ def setup_platform(
             if device.is_windowshutter():
                 devices.append(MaxCubeShutter(handler, device))
 
-    if devices:
-        add_entities(devices)
+    add_entities(devices)
 
 
 class MaxCubeBinarySensorBase(BinarySensorEntity):
diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py
index 6361600518b..733736606f6 100644
--- a/homeassistant/components/maxcube/climate.py
+++ b/homeassistant/components/maxcube/climate.py
@@ -60,8 +60,7 @@ def setup_platform(
             if device.is_thermostat() or device.is_wallthermostat():
                 devices.append(MaxCubeClimate(handler, device))
 
-    if devices:
-        add_entities(devices)
+    add_entities(devices)
 
 
 class MaxCubeClimate(ClimateEntity):
diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py
index 9529322affd..cfa2e216d1d 100644
--- a/homeassistant/components/mikrotik/device_tracker.py
+++ b/homeassistant/components/mikrotik/device_tracker.py
@@ -68,8 +68,7 @@ def update_items(
             tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, coordinator)
             new_tracked.append(tracked[mac])
 
-    if new_tracked:
-        async_add_entities(new_tracked)
+    async_add_entities(new_tracked)
 
 
 class MikrotikDataUpdateCoordinatorTracker(
diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py
index 063fd12f5e0..913ea3800f0 100644
--- a/homeassistant/components/nederlandse_spoorwegen/sensor.py
+++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py
@@ -88,8 +88,7 @@ def setup_platform(
                 departure.get(CONF_TIME),
             )
         )
-    if sensors:
-        add_entities(sensors, True)
+    add_entities(sensors, True)
 
 
 def valid_stations(stations, given_stations):
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index 65ac610ef5d..82f6c95b699 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -399,8 +399,7 @@ async def async_setup_entry(
         for device_id in entities.values():
             device_registry.async_remove_device(device_id)
 
-        if new_entities:
-            async_add_entities(new_entities)
+        async_add_entities(new_entities)
 
     async_dispatcher_connect(
         hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities
diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py
index cd648990e05..5feff521efa 100644
--- a/homeassistant/components/netgear/device_tracker.py
+++ b/homeassistant/components/netgear/device_tracker.py
@@ -39,8 +39,7 @@ async def async_setup_entry(
             new_entities.append(NetgearScannerEntity(coordinator, router, device))
             tracked.add(mac)
 
-        if new_entities:
-            async_add_entities(new_entities)
+        async_add_entities(new_entities)
 
     entry.async_on_unload(coordinator.async_add_listener(new_device_callback))
 
diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py
index c38142a3dc5..dc857e1377a 100644
--- a/homeassistant/components/netgear/sensor.py
+++ b/homeassistant/components/netgear/sensor.py
@@ -338,8 +338,7 @@ async def async_setup_entry(
             )
             tracked.add(mac)
 
-        if new_entities:
-            async_add_entities(new_entities)
+        async_add_entities(new_entities)
 
     entry.async_on_unload(coordinator.async_add_listener(new_device_callback))
 
diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py
index 6491ecf0abe..4318ae7598d 100644
--- a/homeassistant/components/netgear/switch.py
+++ b/homeassistant/components/netgear/switch.py
@@ -143,8 +143,7 @@ async def async_setup_entry(
             )
             tracked.add(mac)
 
-        if new_entities:
-            async_add_entities(new_entities)
+        async_add_entities(new_entities)
 
     entry.async_on_unload(coordinator.async_add_listener(new_device_callback))
 
diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py
index ad493022dfc..fd140d5c00e 100644
--- a/homeassistant/components/octoprint/sensor.py
+++ b/homeassistant/components/octoprint/sensor.py
@@ -71,8 +71,7 @@ async def async_setup_entry(
                         device_id,
                     )
                 )
-        if new_tools:
-            async_add_entities(new_tools)
+        async_add_entities(new_tools)
 
     config_entry.async_on_unload(coordinator.async_add_listener(async_add_tool_sensors))
 
diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py
index 79efee6bd2c..b65ac9ccbe7 100644
--- a/homeassistant/components/owntracks/device_tracker.py
+++ b/homeassistant/components/owntracks/device_tracker.py
@@ -49,8 +49,7 @@ async def async_setup_entry(
 
     hass.data[OT_DOMAIN]["context"].set_async_see(_receive_data)
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class OwnTracksEntity(TrackerEntity, RestoreEntity):
diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py
index f990efc3fcc..770570a3c39 100644
--- a/homeassistant/components/plum_lightpad/light.py
+++ b/homeassistant/components/plum_lightpad/light.py
@@ -42,8 +42,7 @@ async def async_setup_entry(
             logical_load = plum.get_load(device["llid"])
             entities.append(PlumLight(load=logical_load))
 
-        if entities:
-            async_add_entities(entities)
+        async_add_entities(entities)
 
     async def new_load(device):
         setup_entities(device)
diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py
index 8c3e78ae479..c9d9df6068e 100644
--- a/homeassistant/components/ruckus_unleashed/device_tracker.py
+++ b/homeassistant/components/ruckus_unleashed/device_tracker.py
@@ -55,8 +55,7 @@ def add_new_entities(coordinator, async_add_entities, tracked):
         new_tracked.append(RuckusUnleashedDevice(coordinator, mac, device[API_NAME]))
         tracked.add(mac)
 
-    if new_tracked:
-        async_add_entities(new_tracked)
+    async_add_entities(new_tracked)
 
 
 @callback
@@ -77,8 +76,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked):
             )
             tracked.add(entity.unique_id)
 
-    if missing:
-        async_add_entities(missing)
+    async_add_entities(missing)
 
 
 class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity):
diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py
index 0eff510051d..7f009c278fe 100644
--- a/homeassistant/components/tado/binary_sensor.py
+++ b/homeassistant/components/tado/binary_sensor.py
@@ -90,8 +90,7 @@ async def async_setup_entry(
             ]
         )
 
-    if entities:
-        async_add_entities(entities, True)
+    async_add_entities(entities, True)
 
 
 class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity):
diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py
index 16a433b1d12..5f58203e9e8 100644
--- a/homeassistant/components/tado/climate.py
+++ b/homeassistant/components/tado/climate.py
@@ -104,8 +104,7 @@ async def async_setup_entry(
         "set_temp_offset",
     )
 
-    if entities:
-        async_add_entities(entities, True)
+    async_add_entities(entities, True)
 
 
 def _generate_entities(tado):
diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py
index 22f4608f670..6d11e15a9fd 100644
--- a/homeassistant/components/tado/sensor.py
+++ b/homeassistant/components/tado/sensor.py
@@ -82,8 +82,7 @@ async def async_setup_entry(
             ]
         )
 
-    if entities:
-        async_add_entities(entities, True)
+    async_add_entities(entities, True)
 
 
 class TadoHomeSensor(TadoHomeEntity, SensorEntity):
diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py
index 3bd3ae48026..92551df8109 100644
--- a/homeassistant/components/tado/water_heater.py
+++ b/homeassistant/components/tado/water_heater.py
@@ -75,8 +75,7 @@ async def async_setup_entry(
         "set_timer",
     )
 
-    if entities:
-        async_add_entities(entities, True)
+    async_add_entities(entities, True)
 
 
 def _generate_entities(tado):
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index 2ae7f2c394f..f184663291d 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -111,8 +111,7 @@ def add_client_entities(controller, async_add_entities, clients):
 
         trackers.append(UniFiClientTracker(client, controller))
 
-    if trackers:
-        async_add_entities(trackers)
+    async_add_entities(trackers)
 
 
 @callback
@@ -127,8 +126,7 @@ def add_device_entities(controller, async_add_entities, devices):
         device = controller.api.devices[mac]
         trackers.append(UniFiDeviceTracker(device, controller))
 
-    if trackers:
-        async_add_entities(trackers)
+    async_add_entities(trackers)
 
 
 class UniFiClientTracker(UniFiClientBase, ScannerEntity):
diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py
index 165507c0498..ab750f6b33e 100644
--- a/homeassistant/components/unifi/sensor.py
+++ b/homeassistant/components/unifi/sensor.py
@@ -67,8 +67,7 @@ def add_bandwidth_entities(controller, async_add_entities, clients):
             client = controller.api.clients[mac]
             sensors.append(sensor_class(client, controller))
 
-    if sensors:
-        async_add_entities(sensors)
+    async_add_entities(sensors)
 
 
 @callback
@@ -83,8 +82,7 @@ def add_uptime_entities(controller, async_add_entities, clients):
         client = controller.api.clients[mac]
         sensors.append(UniFiUpTimeSensor(client, controller))
 
-    if sensors:
-        async_add_entities(sensors)
+    async_add_entities(sensors)
 
 
 class UniFiBandwidthSensor(UniFiClient, SensorEntity):
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index 72083cdc219..fefcfcc56c6 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -124,8 +124,7 @@ def add_block_entities(controller, async_add_entities, clients):
         client = controller.api.clients[mac]
         switches.append(UniFiBlockClientSwitch(client, controller))
 
-    if switches:
-        async_add_entities(switches)
+    async_add_entities(switches)
 
 
 @callback
@@ -175,8 +174,7 @@ def add_poe_entities(controller, async_add_entities, clients, known_poe_clients)
 
         switches.append(UniFiPOEClientSwitch(client, controller))
 
-    if switches:
-        async_add_entities(switches)
+    async_add_entities(switches)
 
 
 @callback
@@ -193,8 +191,7 @@ def add_dpi_entities(controller, async_add_entities, dpi_groups):
 
         switches.append(UniFiDPIRestrictionSwitch(dpi_groups[group], controller))
 
-    if switches:
-        async_add_entities(switches)
+    async_add_entities(switches)
 
 
 @callback
@@ -213,8 +210,7 @@ def add_outlet_entities(controller, async_add_entities, devices):
             if outlet.has_relay:
                 switches.append(UniFiOutletSwitch(device, controller, outlet.index))
 
-    if switches:
-        async_add_entities(switches)
+    async_add_entities(switches)
 
 
 class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity):
diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py
index ecfbe3549bf..76965c17dad 100644
--- a/homeassistant/components/unifi/update.py
+++ b/homeassistant/components/unifi/update.py
@@ -64,8 +64,7 @@ def add_device_update_entities(controller, async_add_entities, devices):
         device = controller.api.devices[mac]
         entities.append(UniFiDeviceUpdateEntity(device, controller))
 
-    if entities:
-        async_add_entities(entities)
+    async_add_entities(entities)
 
 
 class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity):
diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py
index 3ff0f115a04..2767d44032c 100644
--- a/homeassistant/components/wemo/light.py
+++ b/homeassistant/components/wemo/light.py
@@ -76,8 +76,7 @@ def async_setup_bridge(
                 known_light_ids.add(light_id)
                 new_lights.append(WemoLight(coordinator, light))
 
-        if new_lights:
-            async_add_entities(new_lights)
+        async_add_entities(new_lights)
 
     async_update_lights()
     config_entry.async_on_unload(coordinator.async_add_listener(async_update_lights))
diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py
index 74af6cc0793..2c684013765 100644
--- a/homeassistant/components/wled/light.py
+++ b/homeassistant/components/wled/light.py
@@ -270,5 +270,4 @@ def async_update_segments(
         current_ids.add(segment_id)
         new_entities.append(WLEDSegmentLight(coordinator, segment_id))
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py
index 05c517192a0..eb029a07db7 100644
--- a/homeassistant/components/wled/number.py
+++ b/homeassistant/components/wled/number.py
@@ -147,5 +147,4 @@ def async_update_segments(
         for desc in NUMBERS:
             new_entities.append(WLEDNumber(coordinator, segment_id, desc))
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py
index 5b0de05370c..badde5515d4 100644
--- a/homeassistant/components/wled/select.py
+++ b/homeassistant/components/wled/select.py
@@ -195,5 +195,4 @@ def async_update_segments(
         current_ids.add(segment_id)
         new_entities.append(WLEDPaletteSelect(coordinator, segment_id))
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py
index 20b5a618726..9f241756e90 100644
--- a/homeassistant/components/wled/switch.py
+++ b/homeassistant/components/wled/switch.py
@@ -215,5 +215,4 @@ def async_update_segments(
         current_ids.add(segment_id)
         new_entities.append(WLEDReverseSwitch(coordinator, segment_id))
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py
index ac97d502c55..3c262bce82e 100644
--- a/homeassistant/components/xbox/binary_sensor.py
+++ b/homeassistant/components/xbox/binary_sensor.py
@@ -62,8 +62,7 @@ def async_update_friends(
         ]
         new_entities = new_entities + current[xuid]
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
 
     # Process deleted favorites, remove them from Home Assistant
     for xuid in current_ids - new_ids:
diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py
index 9cba49d1dcb..77d52719c88 100644
--- a/homeassistant/components/xbox/sensor.py
+++ b/homeassistant/components/xbox/sensor.py
@@ -64,8 +64,7 @@ def async_update_friends(
         ]
         new_entities = new_entities + current[xuid]
 
-    if new_entities:
-        async_add_entities(new_entities)
+    async_add_entities(new_entities)
 
     # Process deleted favorites, remove them from Home Assistant
     for xuid in current_ids - new_ids:
diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py
index 3081f334821..07adcbeb5cc 100644
--- a/homeassistant/components/xbox_live/sensor.py
+++ b/homeassistant/components/xbox_live/sensor.py
@@ -60,8 +60,7 @@ def setup_platform(
             continue
         entities.append(XboxSensor(api, xuid, gamercard, interval))
 
-    if entities:
-        add_entities(entities, True)
+    add_entities(entities, True)
 
 
 def get_user_gamercard(api, xuid):
-- 
GitLab


From 416aad32cc6607271b00e210360ae75ee339dc04 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Mon, 17 Oct 2022 22:17:08 +0200
Subject: [PATCH 547/985] Don't mock out migration in recorder tests (#80480)

---
 tests/common.py   | 4 +---
 tests/conftest.py | 4 +---
 2 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/tests/common.py b/tests/common.py
index 4b357fe7033..4ff65049b74 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -933,9 +933,7 @@ def init_recorder_component(hass, add_config=None):
         if recorder.CONF_COMMIT_INTERVAL not in config:
             config[recorder.CONF_COMMIT_INTERVAL] = 0
 
-    with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
-        "homeassistant.components.recorder.migration.migrate_schema"
-    ):
+    with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True):
         if recorder.DOMAIN not in hass.data:
             recorder_helper.async_initialize_recorder(hass)
         assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config})
diff --git a/tests/conftest.py b/tests/conftest.py
index dacd9d09c91..8ce266f6b26 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -897,9 +897,7 @@ async def _async_init_recorder_component(hass, add_config=None):
         if recorder.CONF_COMMIT_INTERVAL not in config:
             config[recorder.CONF_COMMIT_INTERVAL] = 0
 
-    with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch(
-        "homeassistant.components.recorder.migration.migrate_schema"
-    ):
+    with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True):
         if recorder.DOMAIN not in hass.data:
             recorder_helper.async_initialize_recorder(hass)
         assert await async_setup_component(
-- 
GitLab


From 6323021bfc256951d9ab6498a38eedcfeb798818 Mon Sep 17 00:00:00 2001
From: "David F. Mulcahey" <david.mulcahey@me.com>
Date: Mon, 17 Oct 2022 16:54:37 -0400
Subject: [PATCH 548/985] Bump ZHA quirks to 0.0.83 (#80489)

---
 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 7067491a12a..5796fc99f3f 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -7,7 +7,7 @@
     "bellows==0.34.2",
     "pyserial==3.5",
     "pyserial-asyncio==0.6",
-    "zha-quirks==0.0.82",
+    "zha-quirks==0.0.83",
     "zigpy-deconz==0.19.0",
     "zigpy==0.51.3",
     "zigpy-xbee==0.16.2",
diff --git a/requirements_all.txt b/requirements_all.txt
index 6aab8532cfe..3ace01237bf 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2601,7 +2601,7 @@ zengge==0.2
 zeroconf==0.39.1
 
 # homeassistant.components.zha
-zha-quirks==0.0.82
+zha-quirks==0.0.83
 
 # homeassistant.components.zhong_hong
 zhong_hong_hvac==1.0.9
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 14af4610950..0c7cf34900a 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1802,7 +1802,7 @@ youless-api==0.16
 zeroconf==0.39.1
 
 # homeassistant.components.zha
-zha-quirks==0.0.82
+zha-quirks==0.0.83
 
 # homeassistant.components.zha
 zigpy-deconz==0.19.0
-- 
GitLab


From 352014fc957876182be5df055cff5c7647c3ff17 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Tue, 18 Oct 2022 00:02:04 +0200
Subject: [PATCH 549/985] Bump deconz dependency to v105 (#80492)

---
 homeassistant/components/deconz/manifest.json | 2 +-
 requirements_all.txt                          | 2 +-
 requirements_test_all.txt                     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 38c78d849da..81bdc974f25 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -3,7 +3,7 @@
   "name": "deCONZ",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/deconz",
-  "requirements": ["pydeconz==104"],
+  "requirements": ["pydeconz==105"],
   "ssdp": [
     {
       "manufacturer": "Royal Philips Electronics",
diff --git a/requirements_all.txt b/requirements_all.txt
index 3ace01237bf..898632164f5 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1511,7 +1511,7 @@ pydaikin==2.7.2
 pydanfossair==0.1.0
 
 # homeassistant.components.deconz
-pydeconz==104
+pydeconz==105
 
 # homeassistant.components.delijn
 pydelijn==1.0.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 0c7cf34900a..915c013e592 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1066,7 +1066,7 @@ pycoolmasternet-async==0.1.2
 pydaikin==2.7.2
 
 # homeassistant.components.deconz
-pydeconz==104
+pydeconz==105
 
 # homeassistant.components.dexcom
 pydexcom==0.2.3
-- 
GitLab


From 8d3de5359275352ce13a38c28e49d3f5ccb04787 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Tue, 18 Oct 2022 00:36:30 +0000
Subject: [PATCH 550/985] [ci skip] Translation update

---
 .../components/accuweather/translations/bg.json          | 2 +-
 homeassistant/components/coinbase/translations/bg.json   | 6 ++++++
 .../components/dsmr_reader/translations/bg.json          | 5 +++++
 .../components/google_travel_time/translations/bg.json   | 3 ++-
 .../components/google_travel_time/translations/ca.json   | 3 ++-
 .../components/google_travel_time/translations/de.json   | 3 ++-
 .../google_travel_time/translations/pt-BR.json           | 3 ++-
 .../components/google_travel_time/translations/ru.json   | 3 ++-
 .../google_travel_time/translations/zh-Hant.json         | 3 ++-
 homeassistant/components/lametric/translations/ca.json   | 2 ++
 .../components/openweathermap/translations/bg.json       | 4 ++--
 homeassistant/components/overkiz/translations/bg.json    | 3 ++-
 homeassistant/components/radarr/translations/bg.json     | 9 +++++++++
 .../trafikverket_weatherstation/translations/bg.json     | 2 +-
 homeassistant/components/zwave_js/translations/bg.json   | 3 +++
 15 files changed, 43 insertions(+), 11 deletions(-)

diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json
index 6cd4cdde80e..8435bea00df 100644
--- a/homeassistant/components/accuweather/translations/bg.json
+++ b/homeassistant/components/accuweather/translations/bg.json
@@ -4,7 +4,7 @@
             "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\u0445 \u043f\u0440\u0438 \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_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447"
         },
         "step": {
diff --git a/homeassistant/components/coinbase/translations/bg.json b/homeassistant/components/coinbase/translations/bg.json
index eb72ab1d10d..cce4b6f5c2a 100644
--- a/homeassistant/components/coinbase/translations/bg.json
+++ b/homeassistant/components/coinbase/translations/bg.json
@@ -16,6 +16,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Coinbase \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 Coinbase \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "\u0415\u0434\u043d\u043e \u0438\u043b\u0438 \u043f\u043e\u0432\u0435\u0447\u0435 \u043e\u0442 \u0438\u0441\u043a\u0430\u043d\u0438\u0442\u0435 \u0432\u0430\u043b\u0443\u0442\u043d\u0438 \u0441\u0430\u043b\u0434\u0430 \u043d\u0435 \u0441\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0442 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f Coinbase API.",
diff --git a/homeassistant/components/dsmr_reader/translations/bg.json b/homeassistant/components/dsmr_reader/translations/bg.json
index 1c6120581b0..68c75f114a6 100644
--- a/homeassistant/components/dsmr_reader/translations/bg.json
+++ b/homeassistant/components/dsmr_reader/translations/bg.json
@@ -3,5 +3,10 @@
         "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."
         }
+    },
+    "issues": {
+        "deprecated_yaml": {
+            "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 DSMR Reader \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430"
+        }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json
index 2530cf7a4ce..3965f9c706c 100644
--- a/homeassistant/components/google_travel_time/translations/bg.json
+++ b/homeassistant/components/google_travel_time/translations/bg.json
@@ -4,7 +4,8 @@
             "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"
+            "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\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/ca.json b/homeassistant/components/google_travel_time/translations/ca.json
index 41ebdd99bc9..cfd24ef02a8 100644
--- a/homeassistant/components/google_travel_time/translations/ca.json
+++ b/homeassistant/components/google_travel_time/translations/ca.json
@@ -4,7 +4,8 @@
             "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada"
         },
         "error": {
-            "cannot_connect": "Ha fallat la connexi\u00f3"
+            "cannot_connect": "Ha fallat la connexi\u00f3",
+            "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json
index 701935f53fe..24bf9799ee0 100644
--- a/homeassistant/components/google_travel_time/translations/de.json
+++ b/homeassistant/components/google_travel_time/translations/de.json
@@ -4,7 +4,8 @@
             "already_configured": "Standort ist bereits konfiguriert"
         },
         "error": {
-            "cannot_connect": "Verbindung fehlgeschlagen"
+            "cannot_connect": "Verbindung fehlgeschlagen",
+            "invalid_auth": "Ung\u00fcltige Authentifizierung"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/pt-BR.json b/homeassistant/components/google_travel_time/translations/pt-BR.json
index b0896991097..9b8249b1b51 100644
--- a/homeassistant/components/google_travel_time/translations/pt-BR.json
+++ b/homeassistant/components/google_travel_time/translations/pt-BR.json
@@ -4,7 +4,8 @@
             "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada"
         },
         "error": {
-            "cannot_connect": "Falha ao conectar"
+            "cannot_connect": "Falha ao conectar",
+            "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/ru.json b/homeassistant/components/google_travel_time/translations/ru.json
index 1e2706de347..d506ed4ca5e 100644
--- a/homeassistant/components/google_travel_time/translations/ru.json
+++ b/homeassistant/components/google_travel_time/translations/ru.json
@@ -4,7 +4,8 @@
             "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
         },
         "error": {
-            "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+            "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+            "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json
index f8ac86306a2..29bf50f3376 100644
--- a/homeassistant/components/google_travel_time/translations/zh-Hant.json
+++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json
@@ -4,7 +4,8 @@
             "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
         },
         "error": {
-            "cannot_connect": "\u9023\u7dda\u5931\u6557"
+            "cannot_connect": "\u9023\u7dda\u5931\u6557",
+            "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/lametric/translations/ca.json b/homeassistant/components/lametric/translations/ca.json
index 0c7a30099e5..b5f3d609052 100644
--- a/homeassistant/components/lametric/translations/ca.json
+++ b/homeassistant/components/lametric/translations/ca.json
@@ -8,6 +8,8 @@
             "missing_configuration": "La integraci\u00f3 LaMetric no est\u00e0 configurada. Consulta la documentaci\u00f3.",
             "no_devices": "L'usuari autoritzat no t\u00e9 cap dispositiu LaMetric",
             "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})",
+            "reauth_device_not_found": "El dispositiu que est\u00e0s intentant tornar a autenticar no es troba en aquest compte de LaMetric",
+            "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament",
             "unknown": "Error inesperat"
         },
         "error": {
diff --git a/homeassistant/components/openweathermap/translations/bg.json b/homeassistant/components/openweathermap/translations/bg.json
index a46653b9fed..844dacd9cd2 100644
--- a/homeassistant/components/openweathermap/translations/bg.json
+++ b/homeassistant/components/openweathermap/translations/bg.json
@@ -4,7 +4,7 @@
             "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\u0445 \u043f\u0440\u0438 \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_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447"
         },
         "step": {
@@ -15,7 +15,7 @@
                     "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",
                     "mode": "\u0420\u0435\u0436\u0438\u043c",
-                    "name": "\u0418\u043c\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430"
+                    "name": "\u0418\u043c\u0435"
                 }
             }
         }
diff --git a/homeassistant/components/overkiz/translations/bg.json b/homeassistant/components/overkiz/translations/bg.json
index ff6e8f03030..99fe944f9cb 100644
--- a/homeassistant/components/overkiz/translations/bg.json
+++ b/homeassistant/components/overkiz/translations/bg.json
@@ -11,7 +11,8 @@
             "server_in_maintenance": "\u0421\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u0441\u043f\u0440\u044f\u043d \u0437\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430",
             "too_many_requests": "\u0422\u0432\u044a\u0440\u0434\u0435 \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u044f\u0432\u043a\u0438, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.",
             "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430",
-            "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b. \u0410\u043a\u0430\u0443\u043d\u0442\u0438\u0442\u0435 \u043d\u0430 Somfy Protect \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f."
+            "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b. \u0410\u043a\u0430\u0443\u043d\u0442\u0438\u0442\u0435 \u043d\u0430 Somfy Protect \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.",
+            "unsupported_hardware": "\u0412\u0430\u0448\u0438\u044f\u0442 \u0445\u0430\u0440\u0434\u0443\u0435\u0440 {unsupported_device} \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u043e\u0442 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f."
         },
         "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}",
         "step": {
diff --git a/homeassistant/components/radarr/translations/bg.json b/homeassistant/components/radarr/translations/bg.json
index e735a1995eb..562883a2f23 100644
--- a/homeassistant/components/radarr/translations/bg.json
+++ b/homeassistant/components/radarr/translations/bg.json
@@ -22,6 +22,15 @@
             }
         }
     },
+    "issues": {
+        "deprecated_yaml": {
+            "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Radarr \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\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 \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\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 \u043d\u0430 Radarr \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 Radarr \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430"
+        },
+        "removed_attributes": {
+            "title": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 \u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Radarr"
+        }
+    },
     "options": {
         "step": {
             "init": {
diff --git a/homeassistant/components/trafikverket_weatherstation/translations/bg.json b/homeassistant/components/trafikverket_weatherstation/translations/bg.json
index 07c54468c03..c4f6c0a2f55 100644
--- a/homeassistant/components/trafikverket_weatherstation/translations/bg.json
+++ b/homeassistant/components/trafikverket_weatherstation/translations/bg.json
@@ -5,7 +5,7 @@
         },
         "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"
+            "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json
index 7bf8bfcc764..0ed3ce16d2f 100644
--- a/homeassistant/components/zwave_js/translations/bg.json
+++ b/homeassistant/components/zwave_js/translations/bg.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "abort": {
+            "not_zwave_js_addon": "\u041e\u0442\u043a\u0440\u0438\u0442\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u043d\u0435 \u0435 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u043d\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u043d\u0430 Z-Wave JS."
+        },
         "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"
-- 
GitLab


From d38d21ab3a3f629b491799b61d1d4a5e4a24ac97 Mon Sep 17 00:00:00 2001
From: uvjustin <46082645+uvjustin@users.noreply.github.com>
Date: Tue, 18 Oct 2022 09:12:45 +0800
Subject: [PATCH 551/985] Fix stream recorder with orientation transforms
 (#80370)

Find moov instead of using fixed location
---
 homeassistant/components/stream/fmp4utils.py | 22 +++++++++++++++++---
 tests/components/stream/test_hls.py          |  2 +-
 tests/components/stream/test_ll_hls.py       |  2 +-
 tests/components/stream/test_worker.py       |  2 +-
 4 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py
index ed9dd6a9724..35d32d5b0e3 100644
--- a/homeassistant/components/stream/fmp4utils.py
+++ b/homeassistant/components/stream/fmp4utils.py
@@ -4,6 +4,8 @@ from __future__ import annotations
 from collections.abc import Generator
 from typing import TYPE_CHECKING
 
+from homeassistant.exceptions import HomeAssistantError
+
 if TYPE_CHECKING:
     from io import BufferedIOBase
 
@@ -11,7 +13,7 @@ if TYPE_CHECKING:
 def find_box(
     mp4_bytes: bytes, target_type: bytes, box_start: int = 0
 ) -> Generator[int, None, None]:
-    """Find location of first box (or sub_box if box_start provided) of given type."""
+    """Find location of first box (or sub box if box_start provided) of given type."""
     if box_start == 0:
         index = 0
         box_end = len(mp4_bytes)
@@ -141,12 +143,26 @@ def get_codec_string(mp4_bytes: bytes) -> str:
     return ",".join(codecs)
 
 
+def find_moov(mp4_io: BufferedIOBase) -> int:
+    """Find location of moov atom in a BufferedIOBase mp4."""
+    index = 0
+    while 1:
+        mp4_io.seek(index)
+        box_header = mp4_io.read(8)
+        if len(box_header) != 8:
+            raise HomeAssistantError("moov atom not found")
+        if box_header[4:8] == b"moov":
+            return index
+        index += int.from_bytes(box_header[0:4], byteorder="big")
+
+
 def read_init(bytes_io: BufferedIOBase) -> bytes:
     """Read the init from a mp4 file."""
-    bytes_io.seek(24)
+    moov_loc = find_moov(bytes_io)
+    bytes_io.seek(moov_loc)
     moov_len = int.from_bytes(bytes_io.read(4), byteorder="big")
     bytes_io.seek(0)
-    return bytes_io.read(24 + moov_len)
+    return bytes_io.read(moov_loc + moov_len)
 
 
 ZERO32 = b"\x00\x00\x00\x00"
diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py
index 204b460b026..e9da793369f 100644
--- a/tests/components/stream/test_hls.py
+++ b/tests/components/stream/test_hls.py
@@ -29,7 +29,7 @@ from .common import (
 from tests.common import async_fire_time_changed
 
 STREAM_SOURCE = "some-stream-source"
-INIT_BYTES = b"init"
+INIT_BYTES = b"\x00\x00\x00\x08moov"
 FAKE_PAYLOAD = b"fake-payload"
 SEGMENT_DURATION = 10
 TEST_TIMEOUT = 5.0  # Lower than 9s home assistant timeout
diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py
index 5755617f393..baad3043547 100644
--- a/tests/components/stream/test_ll_hls.py
+++ b/tests/components/stream/test_ll_hls.py
@@ -30,7 +30,7 @@ TEST_PART_DURATION = 0.75
 NUM_PART_SEGMENTS = int(-(-SEGMENT_DURATION // TEST_PART_DURATION))
 PART_INDEPENDENT_PERIOD = int(1 / TEST_PART_DURATION) or 1
 BYTERANGE_LENGTH = 1
-INIT_BYTES = b"init"
+INIT_BYTES = b"\x00\x00\x00\x08moov"
 SEQUENCE_BYTES = bytearray(range(NUM_PART_SEGMENTS * BYTERANGE_LENGTH))
 ALT_SEQUENCE_BYTES = bytearray(range(20, 20 + NUM_PART_SEGMENTS * BYTERANGE_LENGTH))
 VERY_LARGE_LAST_BYTE_POS = 9007199254740991
diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py
index 00d735df74d..54400af65ab 100644
--- a/tests/components/stream/test_worker.py
+++ b/tests/components/stream/test_worker.py
@@ -242,7 +242,7 @@ class FakePyAvBuffer:
         # Forward to appropriate FakeStream
         packet.stream.mux(packet)
         # Make new init/part data available to the worker
-        self.memory_file.write(b"0")
+        self.memory_file.write(b"\x00\x00\x00\x00moov")
 
     def close(self):
         """Close the buffer."""
-- 
GitLab


From f70f972d883e4b0f249f3b4e820a23c754b14f0c Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 17 Oct 2022 20:13:26 -0500
Subject: [PATCH 552/985] Fix connectable Bluetooth devices not being seen if
 the nearest scanner is non-connectable (#80388)

If we saw the non-connectable scanner advertisement first we would reject
the connectable scanner advertisement because it had worse signal strength.

In this case we need to check both
---
 homeassistant/components/bluetooth/manager.py | 56 +++++++++++++++----
 .../components/bluetooth/test_diagnostics.py  | 27 ++++++++-
 tests/components/bluetooth/test_manager.py    | 48 ++++++++++++++++
 3 files changed, 118 insertions(+), 13 deletions(-)

diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index dbb165f7cd9..abdf73f0659 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -123,7 +123,6 @@ class BluetoothManager:
             tuple[AdvertisementDataCallback, dict[str, set[str]]]
         ] = []
         self._all_history: dict[str, BluetoothServiceInfoBleak] = {}
-        self._non_connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
         self._connectable_history: dict[str, BluetoothServiceInfoBleak] = {}
         self._non_connectable_scanners: list[BaseHaScanner] = []
         self._connectable_scanners: list[BaseHaScanner] = []
@@ -157,9 +156,8 @@ class BluetoothManager:
                 service_info.as_dict()
                 for service_info in self._connectable_history.values()
             ],
-            "non_connectable_history": [
-                service_info.as_dict()
-                for service_info in self._non_connectable_history.values()
+            "all_history": [
+                service_info.as_dict() for service_info in self._all_history.values()
             ],
             "advertisement_tracker": self._advertisement_tracker.async_diagnostics(),
         }
@@ -256,7 +254,6 @@ class BluetoothManager:
         """Watch for unavailable devices and cleanup state history."""
         monotonic_now = MONOTONIC_TIME()
         connectable_history = self._connectable_history
-        non_connectable_history = self._non_connectable_history
         all_history = self._all_history
         tracker = self._advertisement_tracker
         intervals = tracker.intervals
@@ -280,8 +277,6 @@ class BluetoothManager:
                         if time_since_seen <= advertising_interval:
                             continue
 
-                    non_connectable_history.pop(address, None)
-
                     # The second loop (connectable=False) is responsible for removing
                     # the device from all the interval tracking since it is no longer
                     # available for both connectable and non-connectable
@@ -363,8 +358,24 @@ class BluetoothManager:
         device = service_info.device
         address = device.address
         all_history = self._all_history
+        connectable = service_info.connectable
+        connectable_history = self._connectable_history
 
         source = service_info.source
+        # This logic is complex due to the many combinations of scanners that are supported.
+        #
+        # We need to handle multiple connectable and non-connectable scanners
+        # and we need to handle the case where a device is connectable on one scanner
+        # but not on another.
+        #
+        # The device may also be connectable only by a scanner that has worse signal strength
+        # than a non-connectable scanner.
+        #
+        # all_history - the history of all advertisements from all scanners with the best
+        #               advertisement from each scanner
+        # connectable_history - the history of all connectable advertisements from all scanners
+        #                       with the best advertisement from each connectable scanner
+        #
         if (
             (old_service_info := all_history.get(address))
             and source != old_service_info.source
@@ -372,12 +383,35 @@ class BluetoothManager:
                 old_service_info, service_info
             )
         ):
+            # If we are rejecting the new advertisement and the device is connectable
+            # but not in the connectable history or the connectable source is the same
+            # as the new source, we need to add it to the connectable history
+            if connectable:
+                old_connectable_service_info = connectable_history.get(address)
+                if old_connectable_service_info and (
+                    # If its the same as the preferred source, we are done
+                    # as we know we prefer the old advertisement
+                    # from the check above
+                    (old_connectable_service_info is old_service_info)
+                    # If the old connectable source is different from the preferred
+                    # source, we need to check it as well to see if we prefer
+                    # the old connectable advertisement
+                    or (
+                        source != old_connectable_service_info.source
+                        and self._prefer_previous_adv_from_different_source(
+                            old_connectable_service_info, service_info
+                        )
+                    )
+                ):
+                    return
+
+                connectable_history[address] = service_info
+
             return
 
-        if connectable := service_info.connectable:
-            self._connectable_history[address] = service_info
-        else:
-            self._non_connectable_history[address] = service_info
+        if connectable:
+            connectable_history[address] = service_info
+
         all_history[address] = service_info
 
         # Track advertisement intervals to determine when we need to
diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py
index b053a439e00..a8d4d7aa142 100644
--- a/tests/components/bluetooth/test_diagnostics.py
+++ b/tests/components/bluetooth/test_diagnostics.py
@@ -116,7 +116,7 @@ async def test_diagnostics(
                     "timings": {},
                 },
                 "connectable_history": [],
-                "non_connectable_history": [],
+                "all_history": [],
                 "scanners": [
                     {
                         "adapter": "hci0",
@@ -239,7 +239,30 @@ async def test_diagnostics_macos(
                         "time": ANY,
                     }
                 ],
-                "non_connectable_history": [],
+                "all_history": [
+                    {
+                        "address": "44:44:33:11:23:45",
+                        "advertisement": [
+                            "wohand",
+                            {"1": {"__type": "<class " "'bytes'>", "repr": "b'\\x01'"}},
+                            {},
+                            [],
+                            -127,
+                            -127,
+                            [[]],
+                        ],
+                        "connectable": True,
+                        "manufacturer_data": {
+                            "1": {"__type": "<class " "'bytes'>", "repr": "b'\\x01'"}
+                        },
+                        "name": "wohand",
+                        "rssi": -127,
+                        "service_data": {},
+                        "service_uuids": [],
+                        "source": "local",
+                        "time": ANY,
+                    }
+                ],
                 "scanners": [
                     {
                         "adapter": "Core Bluetooth",
diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py
index 3f5fb56539c..c6a65046ef9 100644
--- a/tests/components/bluetooth/test_manager.py
+++ b/tests/components/bluetooth/test_manager.py
@@ -336,3 +336,51 @@ async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable(
         bluetooth.async_ble_device_from_address(hass, address, True)
         is switchbot_device_poor_signal
     )
+
+
+async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_connectable(
+    hass, enable_bluetooth
+):
+    """Test we can still get a connectable BLEDevice when the best path is non-connectable.
+
+    In this case the the device is closer to a non-connectable scanner, but the
+    at least one connectable scanner has the device in range.
+    """
+
+    address = "44:44:33:11:23:45"
+    now = time.monotonic()
+    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_time_and_source_connectable(
+        hass,
+        switchbot_device_good_signal,
+        switchbot_adv_good_signal,
+        now,
+        "hci1",
+        False,
+    )
+
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, False)
+        is switchbot_device_good_signal
+    )
+    assert bluetooth.async_ble_device_from_address(hass, address, True) is None
+
+    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_time_and_source_connectable(
+        hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, now, "hci0", True
+    )
+
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, False)
+        is switchbot_device_good_signal
+    )
+    assert (
+        bluetooth.async_ble_device_from_address(hass, address, True)
+        is switchbot_device_poor_signal
+    )
-- 
GitLab


From cb530e398cfb3373b4da112091cf6557574c4e1b Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 18 Oct 2022 05:30:21 +0200
Subject: [PATCH 553/985] Update sqlalchemy to 1.4.42 (#80495)

---
 homeassistant/components/recorder/manifest.json | 2 +-
 homeassistant/components/sql/manifest.json      | 2 +-
 homeassistant/package_constraints.txt           | 2 +-
 requirements_all.txt                            | 2 +-
 requirements_test_all.txt                       | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json
index 19a22b2a1e3..afdabfd6d01 100644
--- a/homeassistant/components/recorder/manifest.json
+++ b/homeassistant/components/recorder/manifest.json
@@ -2,7 +2,7 @@
   "domain": "recorder",
   "name": "Recorder",
   "documentation": "https://www.home-assistant.io/integrations/recorder",
-  "requirements": ["sqlalchemy==1.4.41", "fnvhash==0.1.0"],
+  "requirements": ["sqlalchemy==1.4.42", "fnvhash==0.1.0"],
   "codeowners": ["@home-assistant/core"],
   "quality_scale": "internal",
   "iot_class": "local_push",
diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json
index cb1556b3154..7484ca0feb7 100644
--- a/homeassistant/components/sql/manifest.json
+++ b/homeassistant/components/sql/manifest.json
@@ -2,7 +2,7 @@
   "domain": "sql",
   "name": "SQL",
   "documentation": "https://www.home-assistant.io/integrations/sql",
-  "requirements": ["sqlalchemy==1.4.41"],
+  "requirements": ["sqlalchemy==1.4.42"],
   "codeowners": ["@dgomes", "@gjohansson-ST"],
   "config_flow": true,
   "iot_class": "local_polling"
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 584f42a2b82..ee70296bd4a 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -37,7 +37,7 @@ pyudev==0.23.2
 pyyaml==6.0
 requests==2.28.1
 scapy==2.4.5
-sqlalchemy==1.4.41
+sqlalchemy==1.4.42
 typing-extensions>=4.4.0,<5.0
 voluptuous-serialize==2.5.0
 voluptuous==0.13.1
diff --git a/requirements_all.txt b/requirements_all.txt
index 898632164f5..2af7f71ddba 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2308,7 +2308,7 @@ spotipy==2.20.0
 
 # homeassistant.components.recorder
 # homeassistant.components.sql
-sqlalchemy==1.4.41
+sqlalchemy==1.4.42
 
 # homeassistant.components.srp_energy
 srpenergy==1.3.6
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 915c013e592..575e290ee47 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1593,7 +1593,7 @@ spotipy==2.20.0
 
 # homeassistant.components.recorder
 # homeassistant.components.sql
-sqlalchemy==1.4.41
+sqlalchemy==1.4.42
 
 # homeassistant.components.srp_energy
 srpenergy==1.3.6
-- 
GitLab


From c717fd19de01fc822d146cc5e353959dfa86d5f7 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 10:04:54 +0200
Subject: [PATCH 554/985] Move attribution to standalone attribute [e-g]
 (#80513)

---
 homeassistant/components/eafm/sensor.py              | 12 +++++-------
 .../components/entur_public_transport/sensor.py      |  6 ++----
 homeassistant/components/etherscan/sensor.py         | 11 +++--------
 homeassistant/components/fitbit/sensor.py            | 10 +++-------
 homeassistant/components/flick_electric/sensor.py    | 10 ++--------
 homeassistant/components/geonetnz_volcano/sensor.py  |  4 +---
 homeassistant/components/gios/sensor.py              |  3 +--
 .../components/google_travel_time/sensor.py          |  4 ++--
 homeassistant/components/gtfs/sensor.py              | 12 ++++++------
 9 files changed, 25 insertions(+), 47 deletions(-)

diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py
index ed663920a80..a695a38bb4b 100644
--- a/homeassistant/components/eafm/sensor.py
+++ b/homeassistant/components/eafm/sensor.py
@@ -7,7 +7,7 @@ import async_timeout
 
 from homeassistant.components.sensor import SensorEntity, SensorStateClass
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS
+from homeassistant.const import LENGTH_METERS
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.device_registry import DeviceEntryType
@@ -90,7 +90,10 @@ async def async_setup_entry(
 class Measurement(CoordinatorEntity, SensorEntity):
     """A gauge at a flood monitoring station."""
 
-    attribution = "This uses Environment Agency flood and river level data from the real-time data API"
+    _attr_attribution = (
+        "This uses Environment Agency flood and river level data "
+        "from the real-time data API"
+    )
     _attr_state_class = SensorStateClass.MEASUREMENT
 
     def __init__(self, coordinator, key):
@@ -166,11 +169,6 @@ class Measurement(CoordinatorEntity, SensorEntity):
             return None
         return UNIT_MAPPING.get(measure["unit"], measure["unitName"])
 
-    @property
-    def extra_state_attributes(self):
-        """Return the sensor specific state attributes."""
-        return {ATTR_ATTRIBUTION: self.attribution}
-
     @property
     def native_value(self):
         """Return the current sensor value."""
diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py
index e980ef0e396..fd470ff7c9f 100644
--- a/homeassistant/components/entur_public_transport/sensor.py
+++ b/homeassistant/components/entur_public_transport/sensor.py
@@ -8,7 +8,6 @@ import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     CONF_LATITUDE,
     CONF_LONGITUDE,
     CONF_NAME,
@@ -25,8 +24,6 @@ import homeassistant.util.dt as dt_util
 
 API_CLIENT_NAME = "homeassistant-homeassistant"
 
-ATTRIBUTION = "Data provided by entur.org under NLOD"
-
 CONF_STOP_IDS = "stop_ids"
 CONF_EXPAND_PLATFORMS = "expand_platforms"
 CONF_WHITELIST_LINES = "line_whitelist"
@@ -160,6 +157,8 @@ class EnturProxy:
 class EnturPublicTransportSensor(SensorEntity):
     """Implementation of a Entur public transport sensor."""
 
+    _attr_attribution = "Data provided by entur.org under NLOD"
+
     def __init__(
         self, api: EnturProxy, name: str, stop: str, show_on_map: bool
     ) -> None:
@@ -185,7 +184,6 @@ class EnturPublicTransportSensor(SensorEntity):
     @property
     def extra_state_attributes(self) -> dict:
         """Return the state attributes."""
-        self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION
         self._attributes[ATTR_STOP_ID] = self._stop
         return self._attributes
 
diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py
index 9f0c6f7eca9..d98ddcba23c 100644
--- a/homeassistant/components/etherscan/sensor.py
+++ b/homeassistant/components/etherscan/sensor.py
@@ -7,14 +7,12 @@ from pyetherscan import get_balance
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME, CONF_TOKEN
+from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TOKEN
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
-ATTRIBUTION = "Data provided by etherscan.io"
-
 CONF_TOKEN_ADDRESS = "token_address"
 
 SCAN_INTERVAL = timedelta(minutes=5)
@@ -54,6 +52,8 @@ def setup_platform(
 class EtherscanSensor(SensorEntity):
     """Representation of an Etherscan.io sensor."""
 
+    _attr_attribution = "Data provided by etherscan.io"
+
     def __init__(self, name, address, token, token_address):
         """Initialize the sensor."""
         self._name = name
@@ -78,11 +78,6 @@ class EtherscanSensor(SensorEntity):
         """Return the unit of measurement this sensor expresses itself in."""
         return self._unit_of_measurement
 
-    @property
-    def extra_state_attributes(self):
-        """Return the state attributes of the sensor."""
-        return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
     def update(self) -> None:
         """Get the latest state of the sensor."""
 
diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py
index c4e9970691d..78929307c02 100644
--- a/homeassistant/components/fitbit/sensor.py
+++ b/homeassistant/components/fitbit/sensor.py
@@ -19,12 +19,7 @@ from homeassistant.components.sensor import (
     PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
     SensorEntity,
 )
-from homeassistant.const import (
-    ATTR_ATTRIBUTION,
-    CONF_CLIENT_ID,
-    CONF_CLIENT_SECRET,
-    CONF_UNIT_SYSTEM,
-)
+from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_UNIT_SYSTEM
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -345,6 +340,7 @@ class FitbitSensor(SensorEntity):
     """Implementation of a Fitbit sensor."""
 
     entity_description: FitbitSensorEntityDescription
+    _attr_attribution = ATTRIBUTION
 
     def __init__(
         self,
@@ -396,7 +392,7 @@ class FitbitSensor(SensorEntity):
     @property
     def extra_state_attributes(self) -> dict[str, str | None]:
         """Return the state attributes."""
-        attrs: dict[str, str | None] = {ATTR_ATTRIBUTION: ATTRIBUTION}
+        attrs: dict[str, str | None] = {}
 
         if self.extra is not None:
             attrs["model"] = self.extra.get("deviceVersion")
diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py
index 215c9a88f6b..d0ff91d7094 100644
--- a/homeassistant/components/flick_electric/sensor.py
+++ b/homeassistant/components/flick_electric/sensor.py
@@ -8,12 +8,7 @@ from pyflick import FlickAPI, FlickPrice
 
 from homeassistant.components.sensor import SensorEntity
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    ATTR_ATTRIBUTION,
-    ATTR_FRIENDLY_NAME,
-    CURRENCY_CENT,
-    ENERGY_KILO_WATT_HOUR,
-)
+from homeassistant.const import ATTR_FRIENDLY_NAME, CURRENCY_CENT, ENERGY_KILO_WATT_HOUR
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.util.dt import utcnow
@@ -24,7 +19,6 @@ _LOGGER = logging.getLogger(__name__)
 
 SCAN_INTERVAL = timedelta(minutes=5)
 
-ATTRIBUTION = "Data provided by Flick Electric"
 FRIENDLY_NAME = "Flick Power Price"
 
 
@@ -40,6 +34,7 @@ async def async_setup_entry(
 class FlickPricingSensor(SensorEntity):
     """Entity object for Flick Electric sensor."""
 
+    _attr_attribution = "Data provided by Flick Electric"
     _attr_native_unit_of_measurement = f"{CURRENCY_CENT}/{ENERGY_KILO_WATT_HOUR}"
 
     def __init__(self, api: FlickAPI) -> None:
@@ -47,7 +42,6 @@ class FlickPricingSensor(SensorEntity):
         self._api: FlickAPI = api
         self._price: FlickPrice = None
         self._attributes: dict[str, Any] = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_FRIENDLY_NAME: FRIENDLY_NAME,
         }
 
diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py
index e48a4cec0cf..e78641f99e2 100644
--- a/homeassistant/components/geonetnz_volcano/sensor.py
+++ b/homeassistant/components/geonetnz_volcano/sensor.py
@@ -6,7 +6,6 @@ import logging
 from homeassistant.components.sensor import SensorEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     ATTR_LATITUDE,
     ATTR_LONGITUDE,
     LENGTH_KILOMETERS,
@@ -124,7 +123,7 @@ class GeonetnzVolcanoSensor(SensorEntity):
             self._distance = round(feed_entry.distance_to_home, 1)
         self._latitude = round(feed_entry.coordinates[0], 5)
         self._longitude = round(feed_entry.coordinates[1], 5)
-        self._attribution = feed_entry.attribution
+        self._attr_attribution = feed_entry.attribution
         self._alert_level = feed_entry.alert_level
         self._activity = feed_entry.activity
         self._hazards = feed_entry.hazards
@@ -159,7 +158,6 @@ class GeonetnzVolcanoSensor(SensorEntity):
         attributes = {}
         for key, value in (
             (ATTR_EXTERNAL_ID, self._external_id),
-            (ATTR_ATTRIBUTION, self._attribution),
             (ATTR_ACTIVITY, self._activity),
             (ATTR_HAZARDS, self._hazards),
             (ATTR_LONGITUDE, self._longitude),
diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py
index 2d32b8261f3..aa528f3d1d3 100644
--- a/homeassistant/components/gios/sensor.py
+++ b/homeassistant/components/gios/sensor.py
@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     ATTR_NAME,
     CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
     CONF_NAME,
@@ -154,6 +153,7 @@ async def async_setup_entry(
 class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity):
     """Define an GIOS sensor."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_has_entity_name = True
     entity_description: GiosSensorEntityDescription
 
@@ -174,7 +174,6 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity):
         )
         self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}"
         self._attrs: dict[str, Any] = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_STATION: self.coordinator.gios.station_name,
         }
         self.entity_description = description
diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py
index 524fae4a128..3ea38af1f27 100644
--- a/homeassistant/components/google_travel_time/sensor.py
+++ b/homeassistant/components/google_travel_time/sensor.py
@@ -10,7 +10,6 @@ from googlemaps.distance_matrix import distance_matrix
 from homeassistant.components.sensor import SensorEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     CONF_API_KEY,
     CONF_MODE,
     CONF_NAME,
@@ -104,6 +103,8 @@ async def async_setup_entry(
 class GoogleTravelTimeSensor(SensorEntity):
     """Representation of a Google travel time sensor."""
 
+    _attr_attribution = ATTRIBUTION
+
     def __init__(self, config_entry, name, api_key, origin, destination, client):
         """Initialize the sensor."""
         self._name = name
@@ -178,7 +179,6 @@ class GoogleTravelTimeSensor(SensorEntity):
             res["distance"] = _data["distance"]["text"]
         res["origin"] = self._resolved_origin
         res["destination"] = self._resolved_destination
-        res[ATTR_ATTRIBUTION] = ATTRIBUTION
         return res
 
     @property
diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py
index 2ea0c23f213..9b733d41502 100644
--- a/homeassistant/components/gtfs/sensor.py
+++ b/homeassistant/components/gtfs/sensor.py
@@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
 )
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET, STATE_UNKNOWN
+from homeassistant.const import CONF_NAME, CONF_OFFSET, STATE_UNKNOWN
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -648,6 +648,11 @@ class GTFSDepartureSensor(SensorEntity):
             # Assign attributes, icon and name
             self.update_attributes()
 
+            if self._agency:
+                self._attr_attribution = self._agency.agency_name
+            else:
+                self._attr_attribution = None
+
             if self._route:
                 self._icon = ICONS.get(self._route.route_type, ICON)
             else:
@@ -702,11 +707,6 @@ class GTFSDepartureSensor(SensorEntity):
         elif ATTR_INFO in self._attributes:
             del self._attributes[ATTR_INFO]
 
-        if self._agency:
-            self._attributes[ATTR_ATTRIBUTION] = self._agency.agency_name
-        elif ATTR_ATTRIBUTION in self._attributes:
-            del self._attributes[ATTR_ATTRIBUTION]
-
         # Add extra metadata
         key = "agency_id"
         if self._agency and key not in self._attributes:
-- 
GitLab


From d4c28e04e4793c328d1a1d096c2085ebbe7c5e7c Mon Sep 17 00:00:00 2001
From: Raman Gupta <7243222+raman325@users.noreply.github.com>
Date: Tue, 18 Oct 2022 04:06:29 -0400
Subject: [PATCH 555/985] Reduce missed coverage in zwave_js (#79571)

* Reduce missed coverage in zwave_js.climate and cover

* Add switch platform coverage

* Add select platform

* Add lock platform

* Remove one line of coverage from number platform

* update docstring
---
 homeassistant/components/zwave_js/climate.py | 10 +--
 homeassistant/components/zwave_js/cover.py   | 10 +--
 homeassistant/components/zwave_js/number.py  |  5 +-
 homeassistant/components/zwave_js/select.py  |  6 +-
 tests/components/zwave_js/test_cover.py      | 65 ++++++++++++++++++++
 tests/components/zwave_js/test_lock.py       | 33 +++++++++-
 tests/components/zwave_js/test_select.py     | 28 +++++++++
 tests/components/zwave_js/test_switch.py     | 31 +++++++++-
 8 files changed, 160 insertions(+), 28 deletions(-)

diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py
index e2bd69a1436..6cbb1ea3016 100644
--- a/homeassistant/components/zwave_js/climate.py
+++ b/homeassistant/components/zwave_js/climate.py
@@ -188,7 +188,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
         )
         self._set_modes_and_presets()
         self._attr_supported_features = 0
-        if len(self._hvac_presets) > 1:
+        if self._current_mode and len(self._hvac_presets) > 1:
             self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
         # If any setpoint value exists, we can assume temperature
         # can be set
@@ -428,9 +428,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
 
     async def async_set_fan_mode(self, fan_mode: str) -> None:
         """Set new target fan mode."""
-        if not self._fan_mode:
-            return
-
+        assert self._fan_mode is not None
         try:
             new_state = int(
                 next(
@@ -484,9 +482,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set new target preset mode."""
-        if self._current_mode is None:
-            # Thermostat(valve) has no support for setting a mode, so we make it a no-op
-            return
+        assert self._current_mode is not None
         if preset_mode == PRESET_NONE:
             # try to restore to the (translated) main hvac mode
             await self.async_set_hvac_mode(self.hvac_mode)
diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py
index 30364d127eb..b3f3aeaf1c0 100644
--- a/homeassistant/components/zwave_js/cover.py
+++ b/homeassistant/components/zwave_js/cover.py
@@ -27,7 +27,6 @@ from homeassistant.components.cover import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
@@ -138,8 +137,7 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
     async def async_set_cover_position(self, **kwargs: Any) -> None:
         """Move the cover to a specific position."""
         target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY)
-        if target_value is None:
-            raise HomeAssistantError("Missing target value on device.")
+        assert target_value is not None
         await self.info.node.async_set_value(
             target_value, percent_to_zwave_position(kwargs[ATTR_POSITION])
         )
@@ -147,15 +145,13 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
     async def async_open_cover(self, **kwargs: Any) -> None:
         """Open the cover."""
         target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY)
-        if target_value is None:
-            raise HomeAssistantError("Missing target value on device.")
+        assert target_value is not None
         await self.info.node.async_set_value(target_value, 99)
 
     async def async_close_cover(self, **kwargs: Any) -> None:
         """Close cover."""
         target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY)
-        if target_value is None:
-            raise HomeAssistantError("Missing target value on device.")
+        assert target_value is not None
         await self.info.node.async_set_value(target_value, 0)
 
     async def async_stop_cover(self, **kwargs: Any) -> None:
diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py
index f898170e308..7f7f5d65bb8 100644
--- a/homeassistant/components/zwave_js/number.py
+++ b/homeassistant/components/zwave_js/number.py
@@ -113,10 +113,7 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity):
         super().__init__(config_entry, driver, info)
         max_value = cast(int, self.info.primary_value.metadata.max)
         min_value = cast(int, self.info.primary_value.metadata.min)
-        self.correction_factor = max_value - min_value
-        # Fallback in case we can't properly calculate correction factor
-        if self.correction_factor == 0:
-            self.correction_factor = 1
+        self.correction_factor = (max_value - min_value) or 1
 
         # Entity class attributes
         self._attr_native_min_value = 0
diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py
index 0360b968173..adb5820657f 100644
--- a/homeassistant/components/zwave_js/select.py
+++ b/homeassistant/components/zwave_js/select.py
@@ -11,7 +11,6 @@ from zwave_js_server.model.driver import Driver
 from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -173,7 +172,6 @@ class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity):
 
     async def async_select_option(self, option: str) -> None:
         """Change the selected option."""
-        if (target_value := self._target_value) is None:
-            raise HomeAssistantError("Missing target value on device.")
+        assert self._target_value is not None
         key = next(key for key, val in self._lookup_map.items() if val == option)
-        await self.info.node.async_set_value(target_value, int(key))
+        await self.info.node.async_set_value(self._target_value, int(key))
diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py
index 54f71fa00d3..f26b0d29069 100644
--- a/tests/components/zwave_js/test_cover.py
+++ b/tests/components/zwave_js/test_cover.py
@@ -1,5 +1,11 @@
 """Test the Z-Wave JS cover platform."""
+from zwave_js_server.const import (
+    CURRENT_STATE_PROPERTY,
+    CURRENT_VALUE_PROPERTY,
+    CommandClass,
+)
 from zwave_js_server.event import Event
+from zwave_js_server.model.node import Node
 
 from homeassistant.components.cover import (
     ATTR_CURRENT_POSITION,
@@ -9,6 +15,7 @@ from homeassistant.components.cover import (
     SERVICE_OPEN_COVER,
     CoverDeviceClass,
 )
+from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
 from homeassistant.const import (
     ATTR_DEVICE_CLASS,
     STATE_CLOSED,
@@ -18,6 +25,8 @@ from homeassistant.const import (
     STATE_UNKNOWN,
 )
 
+from .common import replace_value_of_zwave_value
+
 WINDOW_COVER_ENTITY = "cover.zws_12"
 GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5"
 BLIND_COVER_ENTITY = "cover.window_blind_controller"
@@ -600,3 +609,59 @@ async def test_motor_barrier_cover(hass, client, gdc_zw062, integration):
 
     state = hass.states.get(GDC_COVER_ENTITY)
     assert state.state == STATE_UNKNOWN
+
+
+async def test_motor_barrier_cover_no_primary_value(
+    hass, client, gdc_zw062_state, integration
+):
+    """Test the cover entity where primary value value is None."""
+    node_state = replace_value_of_zwave_value(
+        gdc_zw062_state,
+        [
+            ZwaveValueMatcher(
+                property_=CURRENT_STATE_PROPERTY,
+                command_class=CommandClass.BARRIER_OPERATOR,
+            )
+        ],
+        None,
+    )
+    node = Node(client, node_state)
+    client.driver.controller.emit("node added", {"node": node})
+    await hass.async_block_till_done()
+
+    state = hass.states.get(GDC_COVER_ENTITY)
+    assert state
+    assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.GARAGE
+
+    assert state.state == STATE_UNKNOWN
+    assert ATTR_CURRENT_POSITION not in state.attributes
+
+
+async def test_fibaro_FGR222_shutter_cover_no_tilt(
+    hass, client, fibaro_fgr222_shutter_state, integration
+):
+    """Test tilt function of the Fibaro Shutter devices with tilt value is None."""
+    node_state = replace_value_of_zwave_value(
+        fibaro_fgr222_shutter_state,
+        [
+            ZwaveValueMatcher(
+                property_="fibaro",
+                command_class=CommandClass.MANUFACTURER_PROPRIETARY,
+                property_key="venetianBlindsTilt",
+            ),
+            ZwaveValueMatcher(
+                property_=CURRENT_VALUE_PROPERTY,
+                command_class=CommandClass.SWITCH_MULTILEVEL,
+            ),
+        ],
+        None,
+    )
+    node = Node(client, node_state)
+    client.driver.controller.emit("node added", {"node": node})
+    await hass.async_block_till_done()
+
+    state = hass.states.get(FIBARO_SHUTTER_COVER_ENTITY)
+    assert state
+    assert state.state == STATE_UNKNOWN
+    assert ATTR_CURRENT_POSITION not in state.attributes
+    assert ATTR_CURRENT_TILT_POSITION not in state.attributes
diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py
index 5f35a568f37..03ebd3b6453 100644
--- a/tests/components/zwave_js/test_lock.py
+++ b/tests/components/zwave_js/test_lock.py
@@ -1,7 +1,12 @@
 """Test the Z-Wave JS lock platform."""
-from zwave_js_server.const.command_class.lock import ATTR_CODE_SLOT, ATTR_USERCODE
+from zwave_js_server.const import CommandClass
+from zwave_js_server.const.command_class.lock import (
+    ATTR_CODE_SLOT,
+    ATTR_USERCODE,
+    CURRENT_MODE_PROPERTY,
+)
 from zwave_js_server.event import Event
-from zwave_js_server.model.node import NodeStatus
+from zwave_js_server.model.node import Node, NodeStatus
 
 from homeassistant.components.lock import (
     DOMAIN as LOCK_DOMAIN,
@@ -9,6 +14,7 @@ from homeassistant.components.lock import (
     SERVICE_UNLOCK,
 )
 from homeassistant.components.zwave_js.const import DOMAIN as ZWAVE_JS_DOMAIN
+from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
 from homeassistant.components.zwave_js.lock import (
     SERVICE_CLEAR_LOCK_USERCODE,
     SERVICE_SET_LOCK_USERCODE,
@@ -17,10 +23,11 @@ from homeassistant.const import (
     ATTR_ENTITY_ID,
     STATE_LOCKED,
     STATE_UNAVAILABLE,
+    STATE_UNKNOWN,
     STATE_UNLOCKED,
 )
 
-from .common import SCHLAGE_BE469_LOCK_ENTITY
+from .common import SCHLAGE_BE469_LOCK_ENTITY, replace_value_of_zwave_value
 
 
 async def test_door_lock(hass, client, lock_schlage_be469, integration):
@@ -160,3 +167,23 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration):
 async def test_only_one_lock(hass, client, lock_home_connect_620, integration):
     """Test node with both Door Lock and Lock CC values only gets one lock entity."""
     assert len(hass.states.async_entity_ids("lock")) == 1
+
+
+async def test_door_lock_no_value(hass, client, lock_schlage_be469_state, integration):
+    """Test a lock entity with door lock command class that has no value for mode."""
+    node_state = replace_value_of_zwave_value(
+        lock_schlage_be469_state,
+        [
+            ZwaveValueMatcher(
+                property_=CURRENT_MODE_PROPERTY,
+                command_class=CommandClass.DOOR_LOCK,
+            )
+        ],
+        None,
+    )
+    node = Node(client, node_state)
+    client.driver.controller.emit("node added", {"node": node})
+    await hass.async_block_till_done()
+    state = hass.states.get(SCHLAGE_BE469_LOCK_ENTITY)
+    assert state
+    assert state.state == STATE_UNKNOWN
diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py
index 1cf5fb54304..e278c11ca72 100644
--- a/tests/components/zwave_js/test_select.py
+++ b/tests/components/zwave_js/test_select.py
@@ -1,15 +1,19 @@
 """Test the Z-Wave JS number platform."""
 from unittest.mock import MagicMock
 
+from zwave_js_server.const import CURRENT_VALUE_PROPERTY, CommandClass
 from zwave_js_server.event import Event
 from zwave_js_server.model.node import Node
 
+from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import STATE_UNKNOWN
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity import EntityCategory
 import homeassistant.helpers.entity_registry as er
 
+from .common import replace_value_of_zwave_value
+
 DEFAULT_TONE_SELECT_ENTITY = "select.indoor_siren_6_default_tone_2"
 PROTECTION_SELECT_ENTITY = "select.family_room_combo_local_protection_state"
 MULTILEVEL_SWITCH_SELECT_ENTITY = "select.front_door_siren"
@@ -265,3 +269,27 @@ async def test_multilevel_switch_select(hass, client, fortrezz_ssa1_siren, integ
 
     state = hass.states.get(MULTILEVEL_SWITCH_SELECT_ENTITY)
     assert state.state == "Strobe ONLY"
+
+
+async def test_multilevel_switch_select_no_value(
+    hass, client, fortrezz_ssa1_siren_state, integration
+):
+    """Test Multilevel Switch CC based select entity with primary value is None."""
+    node_state = replace_value_of_zwave_value(
+        fortrezz_ssa1_siren_state,
+        [
+            ZwaveValueMatcher(
+                property_=CURRENT_VALUE_PROPERTY,
+                command_class=CommandClass.SWITCH_MULTILEVEL,
+            )
+        ],
+        None,
+    )
+    node = Node(client, node_state)
+    client.driver.controller.emit("node added", {"node": node})
+    await hass.async_block_till_done()
+
+    state = hass.states.get(MULTILEVEL_SWITCH_SELECT_ENTITY)
+
+    assert state
+    assert state.state == STATE_UNKNOWN
diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py
index b84ab32f618..d485c877cc4 100644
--- a/tests/components/zwave_js/test_switch.py
+++ b/tests/components/zwave_js/test_switch.py
@@ -1,11 +1,14 @@
 """Test the Z-Wave JS switch platform."""
 
+from zwave_js_server.const import CURRENT_VALUE_PROPERTY, CommandClass
 from zwave_js_server.event import Event
+from zwave_js_server.model.node import Node
 
 from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON
-from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher
+from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
 
-from .common import SWITCH_ENTITY
+from .common import SWITCH_ENTITY, replace_value_of_zwave_value
 
 
 async def test_switch(hass, hank_binary_switch, integration, client):
@@ -14,7 +17,7 @@ async def test_switch(hass, hank_binary_switch, integration, client):
     node = hank_binary_switch
 
     assert state
-    assert state.state == "off"
+    assert state.state == STATE_OFF
 
     # Test turning on
     await hass.services.async_call(
@@ -178,3 +181,25 @@ async def test_barrier_signaling_switch(hass, gdc_zw062, integration, client):
 
     state = hass.states.get(entity)
     assert state.state == STATE_ON
+
+
+async def test_switch_no_value(hass, hank_binary_switch_state, integration, client):
+    """Test the switch where primary value value is None."""
+    node_state = replace_value_of_zwave_value(
+        hank_binary_switch_state,
+        [
+            ZwaveValueMatcher(
+                property_=CURRENT_VALUE_PROPERTY,
+                command_class=CommandClass.SWITCH_BINARY,
+            )
+        ],
+        None,
+    )
+    node = Node(client, node_state)
+    client.driver.controller.emit("node added", {"node": node})
+    await hass.async_block_till_done()
+
+    state = hass.states.get(SWITCH_ENTITY)
+
+    assert state
+    assert state.state == STATE_UNKNOWN
-- 
GitLab


From 0e1fe4eba5fdc06dafecc572f607872cd78791c6 Mon Sep 17 00:00:00 2001
From: Nippey <matthias@dornuweb.de>
Date: Tue, 18 Oct 2022 10:31:08 +0200
Subject: [PATCH 556/985] Modbus: Add support for Holding Registers to Binary
 Sensor (#80460)

Update handling of binary sensors to support reading from holding registers (command 0x03).
---
 homeassistant/components/modbus/__init__.py      |  2 +-
 homeassistant/components/modbus/binary_sensor.py |  7 +++++--
 tests/components/modbus/test_binary_sensor.py    | 10 ++++++++++
 3 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py
index 2e855c7af8d..08834177e4e 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -268,7 +268,7 @@ BINARY_SENSOR_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
     {
         vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
         vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In(
-            [CALL_TYPE_COIL, CALL_TYPE_DISCRETE]
+            [CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING]
         ),
         vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int,
     }
diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py
index c432c102492..ed2f6fa0227 100644
--- a/homeassistant/components/modbus/binary_sensor.py
+++ b/homeassistant/components/modbus/binary_sensor.py
@@ -23,7 +23,7 @@ from homeassistant.helpers.update_coordinator import (
 
 from . import get_hub
 from .base_platform import BasePlatform
-from .const import CONF_SLAVE_COUNT
+from .const import CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CONF_SLAVE_COUNT
 from .modbus import ModbusHub
 
 _LOGGER = logging.getLogger(__name__)
@@ -109,9 +109,12 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
             self._result = None
         else:
             self._lazy_errors = self._lazy_error_count
-            self._attr_is_on = result.bits[0] & 1
             self._attr_available = True
             self._result = result
+            if self._input_type in (CALL_TYPE_COIL, CALL_TYPE_DISCRETE):
+                self._attr_is_on = bool(result.bits[0] & 1)
+            else:
+                self._attr_is_on = bool(result.registers[0] & 1)
 
         self.async_write_ha_state()
         if self._coordinator:
diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py
index bca63321597..777d284e20f 100644
--- a/tests/components/modbus/test_binary_sensor.py
+++ b/tests/components/modbus/test_binary_sensor.py
@@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN
 from homeassistant.components.modbus.const import (
     CALL_TYPE_COIL,
     CALL_TYPE_DISCRETE,
+    CALL_TYPE_REGISTER_HOLDING,
     CONF_INPUT_TYPE,
     CONF_LAZY_ERROR,
     CONF_SLAVE_COUNT,
@@ -81,6 +82,15 @@ async def test_config_binary_sensor(hass, mock_modbus):
                 },
             ],
         },
+        {
+            CONF_BINARY_SENSORS: [
+                {
+                    CONF_NAME: TEST_ENTITY_NAME,
+                    CONF_ADDRESS: 51,
+                    CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING,
+                },
+            ],
+        },
     ],
 )
 @pytest.mark.parametrize(
-- 
GitLab


From b09e95431ca5124fe6c65b6a13745e7bc4716e06 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 18 Oct 2022 10:36:32 +0200
Subject: [PATCH 557/985] Update sentry-sdk to 1.9.10 (#80474)

---
 homeassistant/components/sentry/manifest.json | 2 +-
 requirements_all.txt                          | 2 +-
 requirements_test_all.txt                     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json
index 9855c281ac4..a3c5664cd80 100644
--- a/homeassistant/components/sentry/manifest.json
+++ b/homeassistant/components/sentry/manifest.json
@@ -3,7 +3,7 @@
   "name": "Sentry",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/sentry",
-  "requirements": ["sentry-sdk==1.9.8"],
+  "requirements": ["sentry-sdk==1.9.10"],
   "codeowners": ["@dcramer", "@frenck"],
   "iot_class": "cloud_polling"
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index 2af7f71ddba..dd6261f61bf 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2238,7 +2238,7 @@ sensorpro-ble==0.5.0
 sensorpush-ble==1.5.2
 
 # homeassistant.components.sentry
-sentry-sdk==1.9.8
+sentry-sdk==1.9.10
 
 # homeassistant.components.sharkiq
 sharkiq==0.0.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 575e290ee47..969646c77e3 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1541,7 +1541,7 @@ sensorpro-ble==0.5.0
 sensorpush-ble==1.5.2
 
 # homeassistant.components.sentry
-sentry-sdk==1.9.8
+sentry-sdk==1.9.10
 
 # homeassistant.components.sharkiq
 sharkiq==0.0.1
-- 
GitLab


From cee8f2cabb3a01ac794cac6bcb7e18cc06b349e9 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Tue, 18 Oct 2022 02:40:49 -0600
Subject: [PATCH 558/985] Don't add RainMachine restriction switches if
 underlying data is missing (#80502)

---
 homeassistant/components/rainmachine/switch.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py
index 56ac814e2eb..db560c3c64c 100644
--- a/homeassistant/components/rainmachine/switch.py
+++ b/homeassistant/components/rainmachine/switch.py
@@ -34,7 +34,7 @@ from .model import (
     RainMachineEntityDescriptionMixinDataKey,
     RainMachineEntityDescriptionMixinUid,
 )
-from .util import RUN_STATE_MAP
+from .util import RUN_STATE_MAP, key_exists
 
 ATTR_AREA = "area"
 ATTR_CS_ON = "cs_on"
@@ -237,6 +237,8 @@ async def async_setup_entry(
 
     # Add switches to control restrictions:
     for description in RESTRICTIONS_SWITCH_DESCRIPTIONS:
+        if not key_exists(coordinator.data, description.data_key):
+            continue
         entities.append(RainMachineRestrictionSwitch(entry, data, description))
 
     async_add_entities(entities)
-- 
GitLab


From 503b765108ad9eb20f4af5e3fb0190a3bb42b505 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 18 Oct 2022 10:42:01 +0200
Subject: [PATCH 559/985] Update freezegun to 1.2.2 (#80498)

---
 requirements_test.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements_test.txt b/requirements_test.txt
index b09ba924fc5..82432d85072 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -10,7 +10,7 @@
 astroid==2.12.5
 codecov==2.1.12
 coverage==6.4.4
-freezegun==1.2.1
+freezegun==1.2.2
 mock-open==1.4.0
 mypy==0.982
 pre-commit==2.20.0
-- 
GitLab


From 086a1bdace89df68ec74e3f8406dcfd139c2d311 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 18 Oct 2022 10:42:57 +0200
Subject: [PATCH 560/985] Update yamllint to 1.28.0 (#80497)

---
 .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 ab4d048deb8..751c97ebbb4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -61,7 +61,7 @@ repos:
           - --branch=master
           - --branch=rc
   - repo: https://github.com/adrienverge/yamllint.git
-    rev: v1.27.1
+    rev: v1.28.0
     hooks:
       - id: yamllint
   - repo: https://github.com/pre-commit/mirrors-prettier
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index 87df8c733cb..ec6edeeea66 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -13,4 +13,4 @@ pycodestyle==2.8.0
 pydocstyle==6.1.1
 pyflakes==2.4.0
 pyupgrade==3.1.0
-yamllint==1.27.1
+yamllint==1.28.0
-- 
GitLab


From 9ff077e22506a84be09dbc450ba00c155d14fd9b Mon Sep 17 00:00:00 2001
From: Christopher Bailey <cbailey@mort.is>
Date: Tue, 18 Oct 2022 04:43:58 -0400
Subject: [PATCH 561/985] Bump pyunifiprotect to 4.3.4 (#80496)

---
 homeassistant/components/unifiprotect/manifest.json | 2 +-
 requirements_all.txt                                | 2 +-
 requirements_test_all.txt                           | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json
index abe881c6294..ae37360c5ee 100644
--- a/homeassistant/components/unifiprotect/manifest.json
+++ b/homeassistant/components/unifiprotect/manifest.json
@@ -3,7 +3,7 @@
   "name": "UniFi Protect",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/unifiprotect",
-  "requirements": ["pyunifiprotect==4.2.0", "unifi-discovery==1.1.7"],
+  "requirements": ["pyunifiprotect==4.3.4", "unifi-discovery==1.1.7"],
   "dependencies": ["http"],
   "codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
   "quality_scale": "platinum",
diff --git a/requirements_all.txt b/requirements_all.txt
index dd6261f61bf..5132420a59e 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2072,7 +2072,7 @@ pytrafikverket==0.2.1
 pyudev==0.23.2
 
 # homeassistant.components.unifiprotect
-pyunifiprotect==4.2.0
+pyunifiprotect==4.3.4
 
 # homeassistant.components.uptimerobot
 pyuptimerobot==22.2.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 969646c77e3..f4d6db00394 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1435,7 +1435,7 @@ pytrafikverket==0.2.1
 pyudev==0.23.2
 
 # homeassistant.components.unifiprotect
-pyunifiprotect==4.2.0
+pyunifiprotect==4.3.4
 
 # homeassistant.components.uptimerobot
 pyuptimerobot==22.2.0
-- 
GitLab


From 5d207f77aeb9e138ba6f81a5f04c6d0d1cb0d178 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 11:18:22 +0200
Subject: [PATCH 562/985] Move attribution to standalone attribute [h-l]
 (#80516)

---
 homeassistant/components/haveibeenpwned/sensor.py        | 8 ++++----
 homeassistant/components/hvv_departures/binary_sensor.py | 4 ++--
 homeassistant/components/hydrawise/__init__.py           | 8 ++++----
 homeassistant/components/icloud/account.py               | 7 +++----
 homeassistant/components/iperf3/sensor.py                | 6 ++----
 homeassistant/components/irish_rail_transport/sensor.py  | 6 +++---
 homeassistant/components/lastfm/sensor.py                | 6 +++---
 homeassistant/components/logi_circle/camera.py           | 3 +--
 homeassistant/components/logi_circle/sensor.py           | 4 ++--
 homeassistant/components/london_underground/sensor.py    | 9 +++------
 10 files changed, 27 insertions(+), 34 deletions(-)

diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py
index 400c280263a..199035b2713 100644
--- a/homeassistant/components/haveibeenpwned/sensor.py
+++ b/homeassistant/components/haveibeenpwned/sensor.py
@@ -10,7 +10,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_EMAIL
+from homeassistant.const import CONF_API_KEY, CONF_EMAIL
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -21,8 +21,6 @@ import homeassistant.util.dt as dt_util
 
 _LOGGER = logging.getLogger(__name__)
 
-ATTRIBUTION = "Data provided by Have I Been Pwned (HIBP)"
-
 DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
 
 HA_USER_AGENT = "Home Assistant HaveIBeenPwned Sensor Component"
@@ -61,6 +59,8 @@ def setup_platform(
 class HaveIBeenPwnedSensor(SensorEntity):
     """Implementation of a HaveIBeenPwned sensor."""
 
+    _attr_attribution = "Data provided by Have I Been Pwned (HIBP)"
+
     def __init__(self, data, email):
         """Initialize the HaveIBeenPwned sensor."""
         self._state = None
@@ -86,7 +86,7 @@ class HaveIBeenPwnedSensor(SensorEntity):
     @property
     def extra_state_attributes(self):
         """Return the attributes of the sensor."""
-        val = {ATTR_ATTRIBUTION: ATTRIBUTION}
+        val = {}
         if self._email not in self._data.data:
             return val
 
diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py
index 6a2f1467b19..106279cbb91 100644
--- a/homeassistant/components/hvv_departures/binary_sensor.py
+++ b/homeassistant/components/hvv_departures/binary_sensor.py
@@ -14,7 +14,6 @@ from homeassistant.components.binary_sensor import (
     BinarySensorEntity,
 )
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ATTRIBUTION
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -78,7 +77,6 @@ async def async_setup_entry(
                         "button_type": elevator.get("buttonType"),
                         "cause": elevator.get("cause"),
                         "lines": lines,
-                        ATTR_ATTRIBUTION: ATTRIBUTION,
                     },
                 }
         return elevators
@@ -126,6 +124,8 @@ async def async_setup_entry(
 class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity):
     """HVVDepartureBinarySensor class."""
 
+    _attr_attribution = ATTRIBUTION
+
     def __init__(self, coordinator, idx, config_entry):
         """Initialize."""
         super().__init__(coordinator)
diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py
index d92d785fadf..da413cce5ab 100644
--- a/homeassistant/components/hydrawise/__init__.py
+++ b/homeassistant/components/hydrawise/__init__.py
@@ -7,7 +7,7 @@ from requests.exceptions import ConnectTimeout, HTTPError
 import voluptuous as vol
 
 from homeassistant.components import persistent_notification
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL
+from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL
 from homeassistant.core import HomeAssistant, callback
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
@@ -19,8 +19,6 @@ _LOGGER = logging.getLogger(__name__)
 
 ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60]
 
-ATTRIBUTION = "Data provided by hydrawise.com"
-
 CONF_WATERING_TIME = "watering_minutes"
 
 NOTIFICATION_ID = "hydrawise_notification"
@@ -89,6 +87,8 @@ class HydrawiseHub:
 class HydrawiseEntity(Entity):
     """Entity class for Hydrawise devices."""
 
+    _attr_attribution = "Data provided by hydrawise.com"
+
     def __init__(self, data, description: EntityDescription):
         """Initialize the Hydrawise entity."""
         self.entity_description = description
@@ -111,4 +111,4 @@ class HydrawiseEntity(Entity):
     @property
     def extra_state_attributes(self):
         """Return the state attributes."""
-        return {ATTR_ATTRIBUTION: ATTRIBUTION, "identifier": self.data.get("relay")}
+        return {"identifier": self.data.get("relay")}
diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py
index c51f6a3ac26..7dc0e0ec830 100644
--- a/homeassistant/components/icloud/account.py
+++ b/homeassistant/components/icloud/account.py
@@ -16,7 +16,7 @@ from pyicloud.services.findmyiphone import AppleDevice
 
 from homeassistant.components.zone import async_active_zone
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME
+from homeassistant.const import CONF_USERNAME
 from homeassistant.core import CALLBACK_TYPE, HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
 from homeassistant.helpers.dispatcher import dispatcher_send
@@ -48,8 +48,6 @@ from .const import (
     DOMAIN,
 )
 
-ATTRIBUTION = "Data provided by Apple iCloud"
-
 # entity attributes
 ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval"
 ATTR_BATTERY = "battery"
@@ -368,6 +366,8 @@ class IcloudAccount:
 class IcloudDevice:
     """Representation of a iCloud device."""
 
+    _attr_attribution = "Data provided by Apple iCloud"
+
     def __init__(self, account: IcloudAccount, device: AppleDevice, status) -> None:
         """Initialize the iCloud device."""
         self._account = account
@@ -385,7 +385,6 @@ class IcloudDevice:
         self._location = None
 
         self._attrs = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval,
             ATTR_DEVICE_NAME: self._device_model,
             ATTR_DEVICE_STATUS: None,
diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py
index 2dffabeeb8c..3d13c302606 100644
--- a/homeassistant/components/iperf3/sensor.py
+++ b/homeassistant/components/iperf3/sensor.py
@@ -2,7 +2,7 @@
 from __future__ import annotations
 
 from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
+from homeassistant.const import CONF_MONITORED_CONDITIONS
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -11,8 +11,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 from . import ATTR_VERSION, DATA_UPDATED, DOMAIN as IPERF3_DOMAIN, SENSOR_TYPES
 
-ATTRIBUTION = "Data retrieved using Iperf3"
-
 ICON = "mdi:speedometer"
 
 ATTR_PROTOCOL = "Protocol"
@@ -42,6 +40,7 @@ async def async_setup_platform(
 class Iperf3Sensor(RestoreEntity, SensorEntity):
     """A Iperf3 sensor implementation."""
 
+    _attr_attribution = "Data retrieved using Iperf3"
     _attr_icon = ICON
     _attr_should_poll = False
 
@@ -55,7 +54,6 @@ class Iperf3Sensor(RestoreEntity, SensorEntity):
     def extra_state_attributes(self):
         """Return the state attributes."""
         return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_PROTOCOL: self._iperf3_data.protocol,
             ATTR_REMOTE_HOST: self._iperf3_data.host,
             ATTR_REMOTE_PORT: self._iperf3_data.port,
diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py
index 2035080b96d..d11a0b31809 100644
--- a/homeassistant/components/irish_rail_transport/sensor.py
+++ b/homeassistant/components/irish_rail_transport/sensor.py
@@ -7,7 +7,7 @@ from pyirishrail.pyirishrail import IrishRailRTPI
 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
@@ -23,7 +23,6 @@ ATTR_DUE_AT = "Due at"
 ATTR_EXPECT_AT = "Expected at"
 ATTR_NEXT_UP = "Later Train"
 ATTR_TRAIN_TYPE = "Train type"
-ATTRIBUTION = "Data provided by Irish Rail"
 
 CONF_STATION = "station"
 CONF_DESTINATION = "destination"
@@ -76,6 +75,8 @@ def setup_platform(
 class IrishRailTransportSensor(SensorEntity):
     """Implementation of an irish rail public transport sensor."""
 
+    _attr_attribution = "Data provided by Irish Rail"
+
     def __init__(self, data, station, direction, destination, stops_at, name):
         """Initialize the sensor."""
         self.data = data
@@ -110,7 +111,6 @@ class IrishRailTransportSensor(SensorEntity):
                 )
 
             return {
-                ATTR_ATTRIBUTION: ATTRIBUTION,
                 ATTR_STATION: self._station,
                 ATTR_ORIGIN: self._times[0][ATTR_ORIGIN],
                 ATTR_DESTINATION: self._times[0][ATTR_DESTINATION],
diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py
index 2675371f033..497ccf817bc 100644
--- a/homeassistant/components/lastfm/sensor.py
+++ b/homeassistant/components/lastfm/sensor.py
@@ -10,7 +10,7 @@ from pylast import WSError
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
+from homeassistant.const import CONF_API_KEY
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -21,7 +21,6 @@ _LOGGER = logging.getLogger(__name__)
 ATTR_LAST_PLAYED = "last_played"
 ATTR_PLAY_COUNT = "play_count"
 ATTR_TOP_PLAYED = "top_played"
-ATTRIBUTION = "Data provided by Last.fm"
 
 STATE_NOT_SCROBBLING = "Not Scrobbling"
 
@@ -64,6 +63,8 @@ def setup_platform(
 class LastfmSensor(SensorEntity):
     """A class for the Last.fm account."""
 
+    _attr_attribution = "Data provided by Last.fm"
+
     def __init__(self, user, lastfm_api):
         """Initialize the sensor."""
         self._unique_id = hashlib.sha256(user.encode("utf-8")).hexdigest()
@@ -117,7 +118,6 @@ class LastfmSensor(SensorEntity):
     def extra_state_attributes(self):
         """Return the state attributes."""
         return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_LAST_PLAYED: self._lastplayed,
             ATTR_PLAY_COUNT: self._playcount,
             ATTR_TOP_PLAYED: self._topplayed,
diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py
index 733e49ca0bf..4a8b36a3d55 100644
--- a/homeassistant/components/logi_circle/camera.py
+++ b/homeassistant/components/logi_circle/camera.py
@@ -8,7 +8,6 @@ from homeassistant.components.camera import Camera, CameraEntityFeature
 from homeassistant.components.ffmpeg import get_ffmpeg_manager
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     ATTR_BATTERY_CHARGING,
     ATTR_BATTERY_LEVEL,
     ATTR_ENTITY_ID,
@@ -62,6 +61,7 @@ async def async_setup_entry(
 class LogiCam(Camera):
     """An implementation of a Logi Circle camera."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_should_poll = True  # Cameras default to False
     _attr_supported_features = CameraEntityFeature.ON_OFF
 
@@ -141,7 +141,6 @@ class LogiCam(Camera):
     def extra_state_attributes(self):
         """Return the state attributes."""
         state = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             "battery_saving_mode": (
                 STATE_ON if self._camera.battery_saving else STATE_OFF
             ),
diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py
index baf6d933916..b31a7bda2b2 100644
--- a/homeassistant/components/logi_circle/sensor.py
+++ b/homeassistant/components/logi_circle/sensor.py
@@ -7,7 +7,6 @@ from typing import Any
 from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     ATTR_BATTERY_CHARGING,
     CONF_MONITORED_CONDITIONS,
     CONF_SENSORS,
@@ -58,6 +57,8 @@ async def async_setup_entry(
 class LogiSensor(SensorEntity):
     """A sensor implementation for a Logi Circle camera."""
 
+    _attr_attribution = ATTRIBUTION
+
     def __init__(self, camera, time_zone, description: SensorEntityDescription):
         """Initialize a sensor for Logi Circle camera."""
         self.entity_description = description
@@ -82,7 +83,6 @@ class LogiSensor(SensorEntity):
     def extra_state_attributes(self):
         """Return the state attributes."""
         state = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             "battery_saving_mode": (
                 STATE_ON if self._camera.battery_saving else STATE_OFF
             ),
diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py
index b111fb8be6c..2cad8e9a109 100644
--- a/homeassistant/components/london_underground/sensor.py
+++ b/homeassistant/components/london_underground/sensor.py
@@ -9,7 +9,6 @@ from london_tube_status import TubeData
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import PlatformNotReady
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -25,8 +24,6 @@ _LOGGER = logging.getLogger(__name__)
 
 DOMAIN = "london_underground"
 
-ATTRIBUTION = "Powered by TfL Open Data"
-
 CONF_LINE = "line"
 
 ICON = "mdi:subway"
@@ -102,11 +99,12 @@ class LondonTubeCoordinator(DataUpdateCoordinator):
 class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity):
     """Sensor that reads the status of a line from Tube Data."""
 
+    _attr_attribution = "Powered by TfL Open Data"
+
     def __init__(self, coordinator, name):
         """Initialize the London Underground sensor."""
         super().__init__(coordinator)
         self._name = name
-        self.attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
 
     @property
     def name(self):
@@ -126,5 +124,4 @@ class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity):
     @property
     def extra_state_attributes(self):
         """Return other details about the sensor state."""
-        self.attrs["Description"] = self.coordinator.data[self.name]["Description"]
-        return self.attrs
+        return {"Description": self.coordinator.data[self.name]["Description"]}
-- 
GitLab


From c1213857ced54041b883b8b8134ae1f58dea5273 Mon Sep 17 00:00:00 2001
From: jjlawren <jjlawren@users.noreply.github.com>
Date: Tue, 18 Oct 2022 04:51:43 -0500
Subject: [PATCH 563/985] Fix Plex reauth with multiple available servers
 (#80508)

---
 homeassistant/components/plex/config_flow.py |  8 ++-
 tests/components/plex/test_config_flow.py    | 56 ++++++++++++++++++++
 2 files changed, 63 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py
index e79b7e7ee04..1ebe439ff7c 100644
--- a/homeassistant/components/plex/config_flow.py
+++ b/homeassistant/components/plex/config_flow.py
@@ -106,6 +106,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         self.token = None
         self.client_id = None
         self._manual = False
+        self._reauth_config = None
 
     async def async_step_user(self, user_input=None, errors=None):
         """Handle a flow initialized by the user."""
@@ -178,6 +179,9 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
     async def async_step_server_validate(self, server_config):
         """Validate a provided configuration."""
+        if self._reauth_config:
+            server_config = {**self._reauth_config, **server_config}
+
         errors = {}
         self.current_login = server_config
 
@@ -336,7 +340,9 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
 
     async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
         """Handle a reauthorization flow request."""
-        self.current_login = dict(entry_data)
+        self._reauth_config = {
+            CONF_SERVER_IDENTIFIER: entry_data[CONF_SERVER_IDENTIFIER]
+        }
         return await self.async_step_user()
 
 
diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py
index fb5a0f06724..8c25baa8746 100644
--- a/tests/components/plex/test_config_flow.py
+++ b/tests/components/plex/test_config_flow.py
@@ -766,6 +766,62 @@ async def test_trigger_reauth(
     assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN"
 
 
+async def test_trigger_reauth_multiple_servers_available(
+    hass,
+    entry,
+    mock_plex_server,
+    mock_websocket,
+    current_request_with_host,
+    requests_mock,
+    plextv_resources_two_servers,
+):
+    """Test setup and reauthorization of a Plex token when multiple servers are available."""
+    assert entry.state is ConfigEntryState.LOADED
+
+    requests_mock.get(
+        "https://plex.tv/api/resources",
+        text=plextv_resources_two_servers,
+    )
+
+    with patch(
+        "plexapi.server.PlexServer.clients", side_effect=plexapi.exceptions.Unauthorized
+    ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized):
+        trigger_plex_update(mock_websocket)
+        await wait_for_debouncer(hass)
+
+    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+    assert entry.state is not ConfigEntryState.LOADED
+
+    flows = hass.config_entries.flow.async_progress()
+    assert len(flows) == 1
+    assert flows[0]["context"]["source"] == SOURCE_REAUTH
+
+    flow_id = flows[0]["flow_id"]
+
+    with patch("plexauth.PlexAuth.initiate_auth"), patch(
+        "plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"
+    ):
+        result = await hass.config_entries.flow.async_configure(flow_id, user_input={})
+        assert result["type"] == "external"
+
+        result = await hass.config_entries.flow.async_configure(result["flow_id"])
+        assert result["type"] == "external_done"
+
+        result = await hass.config_entries.flow.async_configure(result["flow_id"])
+        assert result["type"] == "abort"
+        assert result["flow_id"] == flow_id
+        assert result["reason"] == "reauth_successful"
+
+    assert len(hass.config_entries.flow.async_progress()) == 0
+    assert len(hass.config_entries.async_entries(DOMAIN)) == 1
+
+    assert entry.state is ConfigEntryState.LOADED
+    assert entry.data[CONF_SERVER] == mock_plex_server.friendly_name
+    assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier
+    assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == PLEX_DIRECT_URL
+    assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN"
+
+
 async def test_client_request_missing(hass):
     """Test when client headers are not set properly."""
     result = await hass.config_entries.flow.async_init(
-- 
GitLab


From 1fe397f7d7cc8237b86af54c03d6ff6e09debf88 Mon Sep 17 00:00:00 2001
From: Willem-Jan van Rootselaar <liudgervr@gmail.com>
Date: Tue, 18 Oct 2022 12:06:51 +0200
Subject: [PATCH 564/985] Update bsblan integration (#67399)

* Update bsblan integration

Update the integration to current standards

* removed unused code

update coverage

* some cleanup

* fix conflicts due upstream changes

* fix prettier json files

* fix remove comment code

* use dataclass instead of tuple

* fix spelling

* Set as class attribute

main entity doesn't need to give own name

* fix requirements
---
 .coveragerc                                   |   2 +
 homeassistant/components/bsblan/__init__.py   |  53 +++--
 homeassistant/components/bsblan/climate.py    | 193 +++++++++---------
 .../components/bsblan/config_flow.py          | 105 +++++-----
 homeassistant/components/bsblan/const.py      |  24 ++-
 homeassistant/components/bsblan/entity.py     |  34 +++
 homeassistant/components/bsblan/manifest.json |   4 +-
 requirements_all.txt                          |   6 +-
 requirements_test_all.txt                     |   6 +-
 tests/components/bsblan/__init__.py           |  87 --------
 tests/components/bsblan/conftest.py           |  79 +++++++
 tests/components/bsblan/fixtures/device.json  |  42 ++++
 tests/components/bsblan/fixtures/info.json    |  26 ++-
 tests/components/bsblan/fixtures/state.json   | 101 +++++++++
 tests/components/bsblan/test_config_flow.py   | 188 +++++++----------
 tests/components/bsblan/test_init.py          |  62 +++---
 16 files changed, 583 insertions(+), 429 deletions(-)
 create mode 100644 homeassistant/components/bsblan/entity.py
 create mode 100644 tests/components/bsblan/conftest.py
 create mode 100644 tests/components/bsblan/fixtures/device.json
 create mode 100644 tests/components/bsblan/fixtures/state.json

diff --git a/.coveragerc b/.coveragerc
index 636d506de14..ee420ca0f3b 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -161,6 +161,8 @@ omit =
     homeassistant/components/brunt/const.py
     homeassistant/components/brunt/cover.py
     homeassistant/components/bsblan/climate.py
+    homeassistant/components/bsblan/const.py
+    homeassistant/components/bsblan/entity.py
     homeassistant/components/bt_home_hub_5/device_tracker.py
     homeassistant/components/bt_smarthub/device_tracker.py
     homeassistant/components/buienradar/sensor.py
diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py
index c15324825ba..6ee58989150 100644
--- a/homeassistant/components/bsblan/__init__.py
+++ b/homeassistant/components/bsblan/__init__.py
@@ -1,7 +1,7 @@
 """The BSB-Lan integration."""
-from datetime import timedelta
+import dataclasses
 
-from bsblan import BSBLan, BSBLanConnectionError
+from bsblan import BSBLAN, Device, Info, State
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
@@ -12,21 +12,29 @@ from homeassistant.const import (
     Platform,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
 
-from .const import CONF_PASSKEY, DATA_BSBLAN_CLIENT, DOMAIN
-
-SCAN_INTERVAL = timedelta(seconds=30)
+from .const import CONF_PASSKEY, DOMAIN, LOGGER, SCAN_INTERVAL
 
 PLATFORMS = [Platform.CLIMATE]
 
 
+@dataclasses.dataclass
+class HomeAssistantBSBLANData:
+    """BSBLan data stored in the Home Assistant data object."""
+
+    coordinator: DataUpdateCoordinator[State]
+    client: BSBLAN
+    device: Device
+    info: Info
+
+
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Set up BSB-Lan from a config entry."""
 
     session = async_get_clientsession(hass)
-    bsblan = BSBLan(
+    bsblan = BSBLAN(
         entry.data[CONF_HOST],
         passkey=entry.data[CONF_PASSKEY],
         port=entry.data[CONF_PORT],
@@ -35,13 +43,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         session=session,
     )
 
-    try:
-        await bsblan.info()
-    except BSBLanConnectionError as exception:
-        raise ConfigEntryNotReady from exception
-
-    hass.data.setdefault(DOMAIN, {})
-    hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan}
+    coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator(
+        hass,
+        LOGGER,
+        name=f"{DOMAIN}_{entry.data[CONF_HOST]}",
+        update_interval=SCAN_INTERVAL,
+        update_method=bsblan.state,
+    )
+    await coordinator.async_config_entry_first_refresh()
+
+    device = await bsblan.device()
+    info = await bsblan.info()
+    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantBSBLANData(
+        client=bsblan,
+        coordinator=coordinator,
+        device=device,
+        info=info,
+    )
 
     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
 
@@ -49,13 +67,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
 
 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
-    """Unload BSBLan config entry."""
-
-    unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
-    if unload_ok:
+    """Unload BSBLAN config entry."""
+    if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
         # Cleanup
         del hass.data[DOMAIN][entry.entry_id]
         if not hass.data[DOMAIN]:
             del hass.data[DOMAIN]
-
     return unload_ok
diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py
index b7fa2bb8010..e9774055a85 100644
--- a/homeassistant/components/bsblan/climate.py
+++ b/homeassistant/components/bsblan/climate.py
@@ -1,11 +1,9 @@
 """BSBLAN platform to control a compatible Climate Device."""
 from __future__ import annotations
 
-from datetime import timedelta
-import logging
 from typing import Any
 
-from bsblan import BSBLan, BSBLanError, Info, State
+from bsblan import BSBLAN, BSBLANError, Device, Info, State
 
 from homeassistant.components.climate import (
     ATTR_HVAC_MODE,
@@ -19,15 +17,18 @@ from homeassistant.components.climate import (
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.device_registry import format_mac
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import (
+    CoordinatorEntity,
+    DataUpdateCoordinator,
+)
 
-from .const import ATTR_TARGET_TEMPERATURE, DATA_BSBLAN_CLIENT, DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
+from . import HomeAssistantBSBLANData
+from .const import ATTR_TARGET_TEMPERATURE, DOMAIN, LOGGER
+from .entity import BSBLANEntity
 
 PARALLEL_UPDATES = 1
-SCAN_INTERVAL = timedelta(seconds=20)
 
 HVAC_MODES = [
     HVACMode.AUTO,
@@ -40,130 +41,122 @@ PRESET_MODES = [
     PRESET_NONE,
 ]
 
-HA_STATE_TO_BSBLAN = {
-    HVACMode.AUTO: "1",
-    HVACMode.HEAT: "3",
-    HVACMode.OFF: "0",
-}
-
-BSBLAN_TO_HA_STATE = {value: key for key, value in HA_STATE_TO_BSBLAN.items()}
-
-HA_PRESET_TO_BSBLAN = {
-    PRESET_ECO: "2",
-}
-
-BSBLAN_TO_HA_PRESET = {
-    2: PRESET_ECO,
-}
-
 
 async def async_setup_entry(
     hass: HomeAssistant,
     entry: ConfigEntry,
     async_add_entities: AddEntitiesCallback,
 ) -> None:
-    """Set up BSBLan device based on a config entry."""
-    bsblan: BSBLan = hass.data[DOMAIN][entry.entry_id][DATA_BSBLAN_CLIENT]
-    info = await bsblan.info()
-    async_add_entities([BSBLanClimate(entry.entry_id, bsblan, info)], True)
+    """Set up BSBLAN device based on a config entry."""
+    data: HomeAssistantBSBLANData = hass.data[DOMAIN][entry.entry_id]
+    async_add_entities(
+        [
+            BSBLANClimate(
+                data.coordinator,
+                data.client,
+                data.device,
+                data.info,
+                entry,
+            )
+        ],
+        True,
+    )
 
 
-class BSBLanClimate(ClimateEntity):
-    """Defines a BSBLan climate device."""
+class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity):
+    """Defines a BSBLAN climate device."""
 
+    coordinator: DataUpdateCoordinator[State]
+    _attr_has_entity_name = True
+    # Determine preset modes
     _attr_supported_features = (
         ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
     )
-    _attr_hvac_modes = HVAC_MODES
     _attr_preset_modes = PRESET_MODES
 
+    # Determine hvac modes
+    _attr_hvac_modes = HVAC_MODES
+
     def __init__(
         self,
-        entry_id: str,
-        bsblan: BSBLan,
+        coordinator: DataUpdateCoordinator,
+        client: BSBLAN,
+        device: Device,
         info: Info,
+        entry: ConfigEntry,
     ) -> None:
-        """Initialize BSBLan climate device."""
-        self._attr_available = True
-        self._store_hvac_mode: HVACMode | str | None = None
-        self.bsblan = bsblan
-        self._attr_name = self._attr_unique_id = info.device_identification
-        self._attr_device_info = DeviceInfo(
-            identifiers={(DOMAIN, info.device_identification)},
-            manufacturer="BSBLan",
-            model=info.controller_variant,
-            name="BSBLan Device",
+        """Initialize BSBLAN climate device."""
+        super().__init__(client, device, info, entry)
+        CoordinatorEntity.__init__(self, coordinator)
+        self._attr_unique_id = f"{format_mac(device.MAC)}-climate"
+
+        self._attr_min_temp = float(self.coordinator.data.min_temp.value)
+        self._attr_max_temp = float(self.coordinator.data.max_temp.value)
+        self._attr_temperature_unit = (
+            TEMP_CELSIUS
+            if self.coordinator.data.current_temperature.unit == "&deg;C"
+            else TEMP_FAHRENHEIT
         )
 
+    @property
+    def current_temperature(self) -> float | None:
+        """Return the current temperature."""
+        return float(self.coordinator.data.current_temperature.value)
+
+    @property
+    def target_temperature(self) -> float | None:
+        """Return the temperature we try to reach."""
+        return float(self.coordinator.data.target_temperature.value)
+
+    @property
+    def hvac_mode(self) -> str:
+        """Return hvac operation ie. heat, cool mode."""
+        if self.coordinator.data.hvac_mode.value == PRESET_ECO:
+            return HVACMode.AUTO
+
+        return self.coordinator.data.hvac_mode.value
+
+    @property
+    def preset_mode(self) -> str | None:
+        """Return the current preset mode."""
+        if (
+            self.hvac_mode == HVACMode.AUTO
+            and self.coordinator.data.hvac_mode.value == PRESET_ECO
+        ):
+            return PRESET_ECO
+        return PRESET_NONE
+
+    async def async_set_hvac_mode(self, hvac_mode: str) -> None:
+        """Set hvac mode."""
+        await self.async_set_data(hvac_mode=hvac_mode)
+
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set preset mode."""
-        _LOGGER.debug("Setting preset mode to: %s", preset_mode)
-        if preset_mode == PRESET_NONE:
-            # restore previous hvac mode
-            self._attr_hvac_mode = self._store_hvac_mode
-        else:
-            # Store hvac mode.
-            self._store_hvac_mode = self._attr_hvac_mode
+        # only allow preset mode when hvac mode is auto
+        if self.hvac_mode == HVACMode.AUTO:
             await self.async_set_data(preset_mode=preset_mode)
-
-    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
-        """Set HVAC mode."""
-        _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode)
-        # preset should be none when hvac mode is set
-        self._attr_preset_mode = PRESET_NONE
-        await self.async_set_data(hvac_mode=hvac_mode)
+        else:
+            LOGGER.error("Can't set preset mode when hvac mode is not auto")
 
     async def async_set_temperature(self, **kwargs: Any) -> None:
         """Set new target temperatures."""
         await self.async_set_data(**kwargs)
 
     async def async_set_data(self, **kwargs: Any) -> None:
-        """Set device settings using BSBLan."""
+        """Set device settings using BSBLAN."""
         data = {}
-
         if ATTR_TEMPERATURE in kwargs:
             data[ATTR_TARGET_TEMPERATURE] = kwargs[ATTR_TEMPERATURE]
-            _LOGGER.debug("Set temperature data = %s", data)
-
         if ATTR_HVAC_MODE in kwargs:
-            data[ATTR_HVAC_MODE] = HA_STATE_TO_BSBLAN[kwargs[ATTR_HVAC_MODE]]
-            _LOGGER.debug("Set hvac mode data = %s", data)
-
+            data[ATTR_HVAC_MODE] = kwargs[ATTR_HVAC_MODE]
         if ATTR_PRESET_MODE in kwargs:
-            # for now we set the preset as hvac_mode as the api expect this
-            data[ATTR_HVAC_MODE] = HA_PRESET_TO_BSBLAN[kwargs[ATTR_PRESET_MODE]]
-
+            # If preset mode is None, set hvac to auto
+            if kwargs[ATTR_PRESET_MODE] == PRESET_NONE:
+                data[ATTR_HVAC_MODE] = HVACMode.AUTO
+            else:
+                data[ATTR_HVAC_MODE] = kwargs[ATTR_PRESET_MODE]
         try:
-            await self.bsblan.thermostat(**data)
-        except BSBLanError:
-            _LOGGER.error("An error occurred while updating the BSBLan device")
-            self._attr_available = False
-
-    async def async_update(self) -> None:
-        """Update BSBlan entity."""
-        try:
-            state: State = await self.bsblan.state()
-        except BSBLanError:
-            if self.available:
-                _LOGGER.error("An error occurred while updating the BSBLan device")
-            self._attr_available = False
-            return
-
-        self._attr_available = True
-
-        self._attr_current_temperature = float(state.current_temperature.value)
-        self._attr_target_temperature = float(state.target_temperature.value)
-
-        # check if preset is active else get hvac mode
-        _LOGGER.debug("state hvac/preset mode: %s", state.hvac_mode.value)
-        if state.hvac_mode.value == "2":
-            self._attr_preset_mode = PRESET_ECO
-        else:
-            self._attr_hvac_mode = BSBLAN_TO_HA_STATE[state.hvac_mode.value]
-            self._attr_preset_mode = PRESET_NONE
-
-        self._attr_temperature_unit = (
-            TEMP_CELSIUS
-            if state.current_temperature.unit == "&deg;C"
-            else TEMP_FAHRENHEIT
-        )
+            await self.client.thermostat(**data)
+        except BSBLANError:
+            LOGGER.error("An error occurred while updating the BSBLAN device")
+        await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py
index dff77739106..e12e6e5c6cf 100644
--- a/homeassistant/components/bsblan/config_flow.py
+++ b/homeassistant/components/bsblan/config_flow.py
@@ -1,27 +1,33 @@
 """Config flow for BSB-Lan integration."""
 from __future__ import annotations
 
-import logging
 from typing import Any
 
-from bsblan import BSBLan, BSBLanError, Info
+from bsblan import BSBLAN, BSBLANError
 import voluptuous as vol
 
 from homeassistant.config_entries import ConfigFlow
 from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
+from homeassistant.core import callback
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.device_registry import format_mac
 
-from .const import CONF_DEVICE_IDENT, CONF_PASSKEY, DOMAIN
+from .const import CONF_PASSKEY, DEFAULT_PORT, DOMAIN
 
-_LOGGER = logging.getLogger(__name__)
 
-
-class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
-    """Handle a BSBLan config flow."""
+class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
+    """Handle a BSBLAN config flow."""
 
     VERSION = 1
 
+    host: str
+    port: int
+    mac: str
+    passkey: str | None = None
+    username: str | None = None
+    password: str | None = None
+
     async def async_step_user(
         self, user_input: dict[str, Any] | None = None
     ) -> FlowResult:
@@ -29,33 +35,20 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
         if user_input is None:
             return self._show_setup_form()
 
+        self.host = user_input[CONF_HOST]
+        self.port = user_input[CONF_PORT]
+        self.passkey = user_input.get(CONF_PASSKEY)
+        self.username = user_input.get(CONF_USERNAME)
+        self.password = user_input.get(CONF_PASSWORD)
+
         try:
-            info = await self._get_bsblan_info(
-                host=user_input[CONF_HOST],
-                port=user_input[CONF_PORT],
-                passkey=user_input.get(CONF_PASSKEY),
-                username=user_input.get(CONF_USERNAME),
-                password=user_input.get(CONF_PASSWORD),
-            )
-        except BSBLanError:
+            await self._get_bsblan_info()
+        except BSBLANError:
             return self._show_setup_form({"base": "cannot_connect"})
 
-        # Check if already configured
-        await self.async_set_unique_id(info.device_identification)
-        self._abort_if_unique_id_configured()
-
-        return self.async_create_entry(
-            title=info.device_identification,
-            data={
-                CONF_HOST: user_input[CONF_HOST],
-                CONF_PORT: user_input[CONF_PORT],
-                CONF_PASSKEY: user_input.get(CONF_PASSKEY),
-                CONF_DEVICE_IDENT: info.device_identification,
-                CONF_USERNAME: user_input.get(CONF_USERNAME),
-                CONF_PASSWORD: user_input.get(CONF_PASSWORD),
-            },
-        )
+        return self._async_create_entry()
 
+    @callback
     def _show_setup_form(self, errors: dict | None = None) -> FlowResult:
         """Show the setup form to the user."""
         return self.async_show_form(
@@ -63,7 +56,7 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
             data_schema=vol.Schema(
                 {
                     vol.Required(CONF_HOST): str,
-                    vol.Optional(CONF_PORT, default=80): int,
+                    vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
                     vol.Optional(CONF_PASSKEY): str,
                     vol.Optional(CONF_USERNAME): str,
                     vol.Optional(CONF_PASSWORD): str,
@@ -72,23 +65,39 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
             errors=errors or {},
         )
 
-    async def _get_bsblan_info(
-        self,
-        host: str,
-        username: str | None,
-        password: str | None,
-        passkey: str | None,
-        port: int,
-    ) -> Info:
-        """Get device information from an BSBLan device."""
+    @callback
+    def _async_create_entry(self) -> FlowResult:
+        return self.async_create_entry(
+            title=format_mac(self.mac),
+            data={
+                CONF_HOST: self.host,
+                CONF_PORT: self.port,
+                CONF_PASSKEY: self.passkey,
+                CONF_USERNAME: self.username,
+                CONF_PASSWORD: self.password,
+            },
+        )
+
+    async def _get_bsblan_info(self, raise_on_progress: bool = True) -> None:
+        """Get device information from an BSBLAN device."""
         session = async_get_clientsession(self.hass)
-        _LOGGER.debug("request bsblan.info:")
-        bsblan = BSBLan(
-            host,
-            username=username,
-            password=password,
-            passkey=passkey,
-            port=port,
+        bsblan = BSBLAN(
+            host=self.host,
+            username=self.username,
+            password=self.password,
+            passkey=self.passkey,
+            port=self.port,
             session=session,
         )
-        return await bsblan.info()
+        device = await bsblan.device()
+        self.mac = device.MAC
+
+        await self.async_set_unique_id(
+            format_mac(self.mac), raise_on_progress=raise_on_progress
+        )
+        self._abort_if_unique_id_configured(
+            updates={
+                CONF_HOST: self.host,
+                CONF_PORT: self.port,
+            }
+        )
diff --git a/homeassistant/components/bsblan/const.py b/homeassistant/components/bsblan/const.py
index 0dc2e15a7b4..0de9a29a27b 100644
--- a/homeassistant/components/bsblan/const.py
+++ b/homeassistant/components/bsblan/const.py
@@ -1,23 +1,25 @@
 """Constants for the BSB-Lan integration."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
 from typing import Final
 
-DOMAIN = "bsblan"
+# Integration domain
+DOMAIN: Final = "bsblan"
+
+LOGGER = logging.getLogger(__package__)
+SCAN_INTERVAL = timedelta(seconds=12)
 
+# Services
 DATA_BSBLAN_CLIENT: Final = "bsblan_client"
-DATA_BSBLAN_TIMER: Final = "bsblan_timer"
-DATA_BSBLAN_UPDATED: Final = "bsblan_updated"
 
 ATTR_TARGET_TEMPERATURE: Final = "target_temperature"
 ATTR_INSIDE_TEMPERATURE: Final = "inside_temperature"
 ATTR_OUTSIDE_TEMPERATURE: Final = "outside_temperature"
 
-ATTR_STATE_ON: Final = "on"
-ATTR_STATE_OFF: Final = "off"
-
-CONF_DEVICE_IDENT: Final = "device_identification"
-CONF_CONTROLLER_FAM: Final = "controller_family"
-CONF_CONTROLLER_VARI: Final = "controller_variant"
+CONF_PASSKEY: Final = "passkey"
 
-SENSOR_TYPE_TEMPERATURE: Final = "temperature"
+CONF_DEVICE_IDENT: Final = "RVS21.831F/127"
 
-CONF_PASSKEY: Final = "passkey"
+DEFAULT_PORT: Final = 80
diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py
new file mode 100644
index 00000000000..3e8a493d53b
--- /dev/null
+++ b/homeassistant/components/bsblan/entity.py
@@ -0,0 +1,34 @@
+"""Base entity for the BSBLAN integration."""
+from __future__ import annotations
+
+from bsblan import BSBLAN, Device, Info
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST
+from homeassistant.helpers.device_registry import format_mac
+from homeassistant.helpers.entity import DeviceInfo, Entity
+
+from .const import DOMAIN
+
+
+class BSBLANEntity(Entity):
+    """Defines a BSBLAN entity."""
+
+    def __init__(
+        self,
+        client: BSBLAN,
+        device: Device,
+        info: Info,
+        entry: ConfigEntry,
+    ) -> None:
+        """Initialize an BSBLAN entity."""
+        self.client = client
+
+        self._attr_device_info = DeviceInfo(
+            identifiers={(DOMAIN, format_mac(device.MAC))},
+            manufacturer="BSBLAN Inc.",
+            model=info.device_identification.value,
+            name=device.name,
+            sw_version=f"{device.version})",
+            configuration_url=f"http://{entry.data[CONF_HOST]}",
+        )
diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json
index 88eefb7f9c0..2857cce7b39 100644
--- a/homeassistant/components/bsblan/manifest.json
+++ b/homeassistant/components/bsblan/manifest.json
@@ -1,9 +1,9 @@
 {
   "domain": "bsblan",
-  "name": "BSB-Lan",
+  "name": "BSBLAN",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/bsblan",
-  "requirements": ["bsblan==0.5.0"],
+  "requirements": ["python-bsblan==0.5.5"],
   "codeowners": ["@liudger"],
   "iot_class": "local_polling",
   "loggers": ["bsblan"]
diff --git a/requirements_all.txt b/requirements_all.txt
index 5132420a59e..3205c01fe68 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -465,9 +465,6 @@ brottsplatskartan==0.0.1
 # homeassistant.components.brunt
 brunt==1.2.0
 
-# homeassistant.components.bsblan
-bsblan==0.5.0
-
 # homeassistant.components.bluetooth_tracker
 bt_proximity==0.2.1
 
@@ -1949,6 +1946,9 @@ pythinkingcleaner==0.0.3
 # homeassistant.components.blockchain
 python-blockchain-api==0.0.2
 
+# homeassistant.components.bsblan
+python-bsblan==0.5.5
+
 # homeassistant.components.clementine
 python-clementine-remote==1.0.1
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f4d6db00394..fd8b9bc2ba0 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -372,9 +372,6 @@ brother==2.0.0
 # homeassistant.components.brunt
 brunt==1.2.0
 
-# homeassistant.components.bsblan
-bsblan==0.5.0
-
 # homeassistant.components.bthome
 bthome-ble==1.2.2
 
@@ -1369,6 +1366,9 @@ pytankerkoenig==0.0.6
 # homeassistant.components.tautulli
 pytautulli==21.11.0
 
+# homeassistant.components.bsblan
+python-bsblan==0.5.5
+
 # homeassistant.components.ecobee
 python-ecobee-api==0.2.14
 
diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py
index f2e88d97ba2..d233fa068ea 100644
--- a/tests/components/bsblan/__init__.py
+++ b/tests/components/bsblan/__init__.py
@@ -1,88 +1 @@
 """Tests for the bsblan integration."""
-
-from homeassistant.components.bsblan.const import (
-    CONF_DEVICE_IDENT,
-    CONF_PASSKEY,
-    DOMAIN,
-)
-from homeassistant.const import (
-    CONF_HOST,
-    CONF_PASSWORD,
-    CONF_PORT,
-    CONF_USERNAME,
-    CONTENT_TYPE_JSON,
-)
-from homeassistant.core import HomeAssistant
-
-from tests.common import MockConfigEntry, load_fixture
-from tests.test_util.aiohttp import AiohttpClientMocker
-
-
-async def init_integration(
-    hass: HomeAssistant,
-    aioclient_mock: AiohttpClientMocker,
-    skip_setup: bool = False,
-) -> MockConfigEntry:
-    """Set up the BSBLan integration in Home Assistant."""
-
-    aioclient_mock.post(
-        "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
-        params={"Parameter": "6224,6225,6226"},
-        text=load_fixture("bsblan/info.json"),
-        headers={"Content-Type": CONTENT_TYPE_JSON},
-    )
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        unique_id="RVS21.831F/127",
-        data={
-            CONF_HOST: "example.local",
-            CONF_USERNAME: "nobody",
-            CONF_PASSWORD: "qwerty",
-            CONF_PASSKEY: "1234",
-            CONF_PORT: 80,
-            CONF_DEVICE_IDENT: "RVS21.831F/127",
-        },
-    )
-
-    entry.add_to_hass(hass)
-
-    if not skip_setup:
-        await hass.config_entries.async_setup(entry.entry_id)
-        await hass.async_block_till_done()
-
-    return entry
-
-
-async def init_integration_without_auth(
-    hass: HomeAssistant,
-    aioclient_mock: AiohttpClientMocker,
-    skip_setup: bool = False,
-) -> MockConfigEntry:
-    """Set up the BSBLan integration in Home Assistant."""
-
-    aioclient_mock.post(
-        "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
-        params={"Parameter": "6224,6225,6226"},
-        text=load_fixture("bsblan/info.json"),
-        headers={"Content-Type": CONTENT_TYPE_JSON},
-    )
-
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-        unique_id="RVS21.831F/127",
-        data={
-            CONF_HOST: "example.local",
-            CONF_PASSKEY: "1234",
-            CONF_PORT: 80,
-            CONF_DEVICE_IDENT: "RVS21.831F/127",
-        },
-    )
-
-    entry.add_to_hass(hass)
-
-    if not skip_setup:
-        await hass.config_entries.async_setup(entry.entry_id)
-        await hass.async_block_till_done()
-
-    return entry
diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py
new file mode 100644
index 00000000000..44d87745b3f
--- /dev/null
+++ b/tests/components/bsblan/conftest.py
@@ -0,0 +1,79 @@
+"""Fixtures for BSBLAN integration tests."""
+from collections.abc import Generator
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from bsblan import Device, Info, State
+import pytest
+
+from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry, load_fixture
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+    """Return the default mocked config entry."""
+    return MockConfigEntry(
+        title="BSBLAN Setup",
+        domain=DOMAIN,
+        data={
+            CONF_HOST: "127.0.0.1",
+            CONF_PORT: 80,
+            CONF_PASSKEY: "1234",
+            CONF_USERNAME: "admin",
+            CONF_PASSWORD: "admin1234",
+        },
+        unique_id="00:80:41:19:69:90",
+    )
+
+
+@pytest.fixture
+def mock_setup_entry() -> Generator[AsyncMock, None, None]:
+    """Mock setting up a config entry."""
+    with patch(
+        "homeassistant.components.bsblan.async_setup_entry", return_value=True
+    ) as mock_setup:
+        yield mock_setup
+
+
+@pytest.fixture
+def mock_bsblan_config_flow() -> Generator[None, MagicMock, None]:
+    """Return a mocked BSBLAN client."""
+    with patch(
+        "homeassistant.components.bsblan.config_flow.BSBLAN", autospec=True
+    ) as bsblan_mock:
+        bsblan = bsblan_mock.return_value
+        bsblan.device.return_value = Device.parse_raw(
+            load_fixture("device.json", DOMAIN)
+        )
+        bsblan.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN))
+        yield bsblan
+
+
+@pytest.fixture
+def mock_bsblan(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
+    """Return a mocked BSBLAN client."""
+
+    with patch("homeassistant.components.bsblan.BSBLAN", autospec=True) as bsblan_mock:
+        bsblan = bsblan_mock.return_value
+        bsblan.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN))
+        bsblan.device.return_value = Device.parse_raw(
+            load_fixture("device.json", DOMAIN)
+        )
+        bsblan.state.return_value = State.parse_raw(load_fixture("state.json", DOMAIN))
+        yield bsblan
+
+
+@pytest.fixture
+async def init_integration(
+    hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_bsblan: MagicMock
+) -> MockConfigEntry:
+    """Set up the bsblan 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/bsblan/fixtures/device.json b/tests/components/bsblan/fixtures/device.json
new file mode 100644
index 00000000000..10543d72253
--- /dev/null
+++ b/tests/components/bsblan/fixtures/device.json
@@ -0,0 +1,42 @@
+{
+  "name": "BSB-LAN",
+  "version": "1.0.38-20200730234859",
+  "freeram": 85479,
+  "uptime": 969402857,
+  "MAC": "00:80:41:19:69:90",
+  "freespace": 0,
+  "bus": "BSB",
+  "buswritable": 1,
+  "busaddr": 66,
+  "busdest": 0,
+  "monitor": 0,
+  "verbose": 1,
+  "protectedGPIO": [
+    { "pin": 0 },
+    { "pin": 1 },
+    { "pin": 4 },
+    { "pin": 10 },
+    { "pin": 11 },
+    { "pin": 12 },
+    { "pin": 13 },
+    { "pin": 18 },
+    { "pin": 19 },
+    { "pin": 20 },
+    { "pin": 21 },
+    { "pin": 22 },
+    { "pin": 23 },
+    { "pin": 50 },
+    { "pin": 51 },
+    { "pin": 52 },
+    { "pin": 53 },
+    { "pin": 62 },
+    { "pin": 63 },
+    { "pin": 64 },
+    { "pin": 65 },
+    { "pin": 66 },
+    { "pin": 67 },
+    { "pin": 68 },
+    { "pin": 69 }
+  ],
+  "averages": []
+}
diff --git a/tests/components/bsblan/fixtures/info.json b/tests/components/bsblan/fixtures/info.json
index 08ae7e46247..556c7463e17 100644
--- a/tests/components/bsblan/fixtures/info.json
+++ b/tests/components/bsblan/fixtures/info.json
@@ -1,23 +1,29 @@
 {
-  "6224": {
-    "name": "Geräte-Identifikation",
+  "device_identification": {
+    "name": "Gerte-Identifikation",
+    "error": 0,
     "value": "RVS21.831F/127",
-    "unit": "",
     "desc": "",
-    "dataType": 7
+    "dataType": 7,
+    "readonly": 0,
+    "unit": ""
   },
-  "6225": {
+  "controller_family": {
     "name": "Device family",
+    "error": 0,
     "value": "211",
-    "unit": "",
     "desc": "",
-    "dataType": 0
+    "dataType": 0,
+    "readonly": 0,
+    "unit": ""
   },
-  "6226": {
+  "controller_variant": {
     "name": "Device variant",
+    "error": 0,
     "value": "127",
-    "unit": "",
     "desc": "",
-    "dataType": 0
+    "dataType": 0,
+    "readonly": 0,
+    "unit": ""
   }
 }
diff --git a/tests/components/bsblan/fixtures/state.json b/tests/components/bsblan/fixtures/state.json
new file mode 100644
index 00000000000..51d4cf2e136
--- /dev/null
+++ b/tests/components/bsblan/fixtures/state.json
@@ -0,0 +1,101 @@
+{
+  "hvac_mode": {
+    "name": "Operating mode",
+    "error": 0,
+    "value": "heat",
+    "desc": "Komfort",
+    "dataType": 1,
+    "readonly": 0,
+    "unit": ""
+  },
+  "target_temperature": {
+    "name": "Room temperature Comfort setpoint",
+    "error": 0,
+    "value": "18.5",
+    "desc": "",
+    "dataType": 0,
+    "readonly": 0,
+    "unit": "&deg;C"
+  },
+  "target_temperature_high": {
+    "name": "Komfortsollwert Maximum",
+    "error": 0,
+    "value": "23.0",
+    "desc": "",
+    "dataType": 0,
+    "readonly": 0,
+    "unit": "&deg;C"
+  },
+  "target_temperature_low": {
+    "name": "Room temp reduced setpoint",
+    "error": 0,
+    "value": "17.0",
+    "desc": "",
+    "dataType": 0,
+    "readonly": 0,
+    "unit": "&deg;C"
+  },
+  "min_temp": {
+    "name": "Room temp frost protection setpoint",
+    "error": 0,
+    "value": "8.0",
+    "desc": "",
+    "dataType": 0,
+    "readonly": 0,
+    "unit": "&deg;C"
+  },
+  "max_temp": {
+    "name": "Summer/winter changeover temp heat circuit 1",
+    "error": 0,
+    "value": "20.0",
+    "desc": "",
+    "dataType": 0,
+    "readonly": 0,
+    "unit": "&deg;C"
+  },
+  "hvac_mode2": {
+    "name": "Operating mode",
+    "error": 0,
+    "value": "2",
+    "desc": "Reduziert",
+    "dataType": 1,
+    "readonly": 0,
+    "unit": ""
+  },
+  "hvac_action": {
+    "name": "Status heating circuit 1",
+    "error": 0,
+    "value": "122",
+    "desc": "Raumtemp\u2019begrenzung",
+    "dataType": 1,
+    "readonly": 1,
+    "unit": ""
+  },
+  "outside_temperature": {
+    "name": "Outside temp sensor local",
+    "error": 0,
+    "value": "6.1",
+    "desc": "",
+    "dataType": 0,
+    "readonly": 0,
+    "unit": "&deg;C"
+  },
+  "current_temperature": {
+    "name": "Room temp 1 actual value",
+    "error": 0,
+    "value": "18.6",
+    "desc": "",
+    "dataType": 0,
+    "readonly": 1,
+    "unit": "&deg;C"
+  },
+  "room1_thermostat_mode": {
+    "name": "Raumthermostat 1",
+    "error": 0,
+    "value": "0",
+    "desc": "Kein Bedarf",
+    "dataType": 1,
+    "readonly": 1,
+    "unit": ""
+  }
+}
diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py
index b8efa960fca..a1286f43695 100644
--- a/tests/components/bsblan/test_config_flow.py
+++ b/tests/components/bsblan/test_config_flow.py
@@ -1,159 +1,119 @@
 """Tests for the BSBLan device config flow."""
-import aiohttp
+from unittest.mock import AsyncMock, MagicMock
+
+from bsblan import BSBLANConnectionError
 
 from homeassistant import data_entry_flow
 from homeassistant.components.bsblan import config_flow
-from homeassistant.components.bsblan.const import CONF_DEVICE_IDENT, CONF_PASSKEY
+from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN
 from homeassistant.config_entries import SOURCE_USER
-from homeassistant.const import (
-    CONF_HOST,
-    CONF_PASSWORD,
-    CONF_PORT,
-    CONF_USERNAME,
-    CONTENT_TYPE_JSON,
-)
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import (
+    RESULT_TYPE_ABORT,
+    RESULT_TYPE_CREATE_ENTRY,
+    RESULT_TYPE_FORM,
+)
+from homeassistant.helpers.device_registry import format_mac
 
-from . import init_integration
-
-from tests.common import load_fixture
-from tests.test_util.aiohttp import AiohttpClientMocker
+from tests.common import MockConfigEntry
 
 
-async def test_show_user_form(hass: HomeAssistant) -> None:
-    """Test that the user set up form is served."""
+async def test_full_user_flow_implementation(
+    hass: HomeAssistant,
+    mock_bsblan_config_flow: MagicMock,
+    mock_setup_entry: AsyncMock,
+) -> None:
+    """Test the full manual user flow from start to finish."""
     result = await hass.config_entries.flow.async_init(
-        config_flow.DOMAIN,
+        DOMAIN,
         context={"source": SOURCE_USER},
     )
 
-    assert result["step_id"] == "user"
-    assert result["type"] == data_entry_flow.FlowResultType.FORM
-
-
-async def test_connection_error(
-    hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
-    """Test we show user form on BSBLan connection error."""
-    aioclient_mock.post(
-        "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
-        exc=aiohttp.ClientError,
-    )
+    assert result.get("type") == RESULT_TYPE_FORM
+    assert result.get("step_id") == SOURCE_USER
+    assert "flow_id" in result
 
-    result = await hass.config_entries.flow.async_init(
-        config_flow.DOMAIN,
-        context={"source": SOURCE_USER},
-        data={
-            CONF_HOST: "example.local",
-            CONF_USERNAME: "nobody",
-            CONF_PASSWORD: "qwerty",
-            CONF_PASSKEY: "1234",
+    result2 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={
+            CONF_HOST: "127.0.0.1",
             CONF_PORT: 80,
+            CONF_PASSKEY: "1234",
+            CONF_USERNAME: "admin",
+            CONF_PASSWORD: "admin1234",
         },
     )
 
-    assert result["errors"] == {"base": "cannot_connect"}
-    assert result["step_id"] == "user"
-    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
+    assert result2.get("title") == format_mac("00:80:41:19:69:90")
+    assert result2.get("data") == {
+        CONF_HOST: "127.0.0.1",
+        CONF_PORT: 80,
+        CONF_PASSKEY: "1234",
+        CONF_USERNAME: "admin",
+        CONF_PASSWORD: "admin1234",
+    }
+    assert "result" in result2
+    assert result2["result"].unique_id == format_mac("00:80:41:19:69:90")
 
+    assert len(mock_setup_entry.mock_calls) == 1
+    assert len(mock_bsblan_config_flow.device.mock_calls) == 1
 
-async def test_user_device_exists_abort(
-    hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
-    """Test we abort zeroconf flow if BSBLan device already configured."""
-    await init_integration(hass, aioclient_mock)
 
+async def test_show_user_form(hass: HomeAssistant) -> None:
+    """Test that the user set up form is served."""
     result = await hass.config_entries.flow.async_init(
         config_flow.DOMAIN,
         context={"source": SOURCE_USER},
-        data={
-            CONF_HOST: "example.local",
-            CONF_USERNAME: "nobody",
-            CONF_PASSWORD: "qwerty",
-            CONF_PASSKEY: "1234",
-            CONF_PORT: 80,
-        },
     )
 
-    assert result["type"] == data_entry_flow.FlowResultType.ABORT
+    assert result["step_id"] == "user"
+    assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
 
 
-async def test_full_user_flow_implementation(
-    hass: HomeAssistant, aioclient_mock
+async def test_connection_error(
+    hass: HomeAssistant,
+    mock_bsblan_config_flow: MagicMock,
 ) -> None:
-    """Test the full manual user flow from start to finish."""
-    aioclient_mock.post(
-        "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
-        text=load_fixture("bsblan/info.json"),
-        headers={"Content-Type": CONTENT_TYPE_JSON},
-    )
+    """Test we show user form on BSBLan connection error."""
+    mock_bsblan_config_flow.device.side_effect = BSBLANConnectionError
 
     result = await hass.config_entries.flow.async_init(
-        config_flow.DOMAIN,
+        DOMAIN,
         context={"source": SOURCE_USER},
-    )
-
-    assert result["step_id"] == "user"
-    assert result["type"] == data_entry_flow.FlowResultType.FORM
-
-    result = await hass.config_entries.flow.async_configure(
-        result["flow_id"],
-        user_input={
-            CONF_HOST: "example.local",
-            CONF_USERNAME: "nobody",
-            CONF_PASSWORD: "qwerty",
-            CONF_PASSKEY: "1234",
+        data={
+            CONF_HOST: "127.0.0.1",
             CONF_PORT: 80,
+            CONF_PASSKEY: "1234",
+            CONF_USERNAME: "admin",
+            CONF_PASSWORD: "admin1234",
         },
     )
 
-    assert result["data"][CONF_HOST] == "example.local"
-    assert result["data"][CONF_USERNAME] == "nobody"
-    assert result["data"][CONF_PASSWORD] == "qwerty"
-    assert result["data"][CONF_PASSKEY] == "1234"
-    assert result["data"][CONF_PORT] == 80
-    assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127"
-    assert result["title"] == "RVS21.831F/127"
-    assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+    assert result.get("type") == RESULT_TYPE_FORM
+    assert result.get("errors") == {"base": "cannot_connect"}
+    assert result.get("step_id") == "user"
 
-    entries = hass.config_entries.async_entries(config_flow.DOMAIN)
-    assert entries[0].unique_id == "RVS21.831F/127"
 
-
-async def test_full_user_flow_implementation_without_auth(
-    hass: HomeAssistant, aioclient_mock
+async def test_user_device_exists_abort(
+    hass: HomeAssistant,
+    mock_bsblan_config_flow: MagicMock,
+    mock_config_entry: MockConfigEntry,
 ) -> None:
-    """Test the full manual user flow from start to finish."""
-    aioclient_mock.post(
-        "http://example2.local:80/JQ?Parameter=6224,6225,6226",
-        text=load_fixture("bsblan/info.json"),
-        headers={"Content-Type": CONTENT_TYPE_JSON},
-    )
-
+    """Test we abort flow if BSBLAN device already configured."""
+    mock_config_entry.add_to_hass(hass)
     result = await hass.config_entries.flow.async_init(
-        config_flow.DOMAIN,
+        DOMAIN,
         context={"source": SOURCE_USER},
-    )
-
-    assert result["step_id"] == "user"
-    assert result["type"] == data_entry_flow.FlowResultType.FORM
-
-    result = await hass.config_entries.flow.async_configure(
-        result["flow_id"],
-        user_input={
-            CONF_HOST: "example2.local",
+        data={
+            CONF_HOST: "127.0.0.1",
             CONF_PORT: 80,
+            CONF_PASSKEY: "1234",
+            CONF_USERNAME: "admin",
+            CONF_PASSWORD: "admin1234",
         },
     )
 
-    assert result["data"][CONF_HOST] == "example2.local"
-    assert result["data"][CONF_USERNAME] is None
-    assert result["data"][CONF_PASSWORD] is None
-    assert result["data"][CONF_PASSKEY] is None
-    assert result["data"][CONF_PORT] == 80
-    assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127"
-    assert result["title"] == "RVS21.831F/127"
-    assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
-
-    entries = hass.config_entries.async_entries(config_flow.DOMAIN)
-    assert entries[0].unique_id == "RVS21.831F/127"
+    assert result.get("type") == RESULT_TYPE_ABORT
+    assert result.get("reason") == "already_configured"
diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py
index 147ba46cb5b..34ee30a35e1 100644
--- a/tests/components/bsblan/test_init.py
+++ b/tests/components/bsblan/test_init.py
@@ -1,48 +1,46 @@
 """Tests for the BSBLan integration."""
-import aiohttp
+from unittest.mock import MagicMock
+
+from bsblan import BSBLANConnectionError
 
 from homeassistant.components.bsblan.const import DOMAIN
 from homeassistant.config_entries import ConfigEntryState
 from homeassistant.core import HomeAssistant
 
-from . import init_integration, init_integration_without_auth
-
-from tests.test_util.aiohttp import AiohttpClientMocker
+from tests.common import MockConfigEntry
 
 
-async def test_config_entry_not_ready(
-    hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+async def test_load_unload_config_entry(
+    hass: HomeAssistant,
+    mock_config_entry: MockConfigEntry,
+    mock_bsblan: MagicMock,
 ) -> None:
-    """Test the BSBLan configuration entry not ready."""
-    aioclient_mock.post(
-        "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
-        exc=aiohttp.ClientError,
-    )
-
-    entry = await init_integration(hass, aioclient_mock)
-    assert entry.state is ConfigEntryState.SETUP_RETRY
-
+    """Test the BSBLAN 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()
 
-async def test_unload_config_entry(
-    hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
-) -> None:
-    """Test the BSBLan configuration entry unloading."""
-    entry = await init_integration(hass, aioclient_mock)
-    assert hass.data[DOMAIN]
+    assert mock_config_entry.state is ConfigEntryState.LOADED
+    assert len(mock_bsblan.device.mock_calls) == 1
 
-    await hass.config_entries.async_unload(entry.entry_id)
+    await hass.config_entries.async_unload(mock_config_entry.entry_id)
     await hass.async_block_till_done()
+
     assert not hass.data.get(DOMAIN)
+    assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
 
 
-async def test_config_entry_no_authentication(
-    hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+async def test_config_entry_not_ready(
+    hass: HomeAssistant,
+    mock_config_entry: MockConfigEntry,
+    mock_bsblan: MagicMock,
 ) -> None:
-    """Test the BSBLan configuration entry not ready."""
-    aioclient_mock.post(
-        "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
-        exc=aiohttp.ClientError,
-    )
-
-    entry = await init_integration_without_auth(hass, aioclient_mock)
-    assert entry.state is ConfigEntryState.SETUP_RETRY
+    """Test the bsblan configuration entry not ready."""
+    mock_bsblan.state.side_effect = BSBLANConnectionError
+
+    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 len(mock_bsblan.state.mock_calls) == 1
+    assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
-- 
GitLab


From 6b256bab227346bdd6d0ae871855b70ebefbba01 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 12:49:59 +0200
Subject: [PATCH 565/985] Move attribution to standalone attribute [m-q]
 (#80518)

---
 homeassistant/components/magicseaweed/sensor.py     | 11 +++--------
 homeassistant/components/meteo_france/sensor.py     |  5 +----
 homeassistant/components/metoffice/sensor.py        |  3 +--
 homeassistant/components/mvglive/sensor.py          |  6 ++++--
 .../components/nederlandse_spoorwegen/sensor.py     |  7 +++----
 .../components/netatmo/netatmo_entity_base.py       |  5 +++--
 homeassistant/components/nmbs/sensor.py             |  6 +++---
 homeassistant/components/noaa_tides/sensor.py       | 12 ++++--------
 homeassistant/components/nsw_fuel_station/sensor.py |  6 +++---
 homeassistant/components/oasa_telematics/sensor.py  |  7 +++----
 homeassistant/components/opensky/sensor.py          | 13 ++++---------
 11 files changed, 32 insertions(+), 49 deletions(-)

diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py
index 13bc8b9be2e..79b77a362c4 100644
--- a/homeassistant/components/magicseaweed/sensor.py
+++ b/homeassistant/components/magicseaweed/sensor.py
@@ -12,12 +12,7 @@ from homeassistant.components.sensor import (
     SensorEntity,
     SensorEntityDescription,
 )
-from homeassistant.const import (
-    ATTR_ATTRIBUTION,
-    CONF_API_KEY,
-    CONF_MONITORED_CONDITIONS,
-    CONF_NAME,
-)
+from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -34,7 +29,6 @@ CONF_UNITS = "units"
 
 DEFAULT_UNIT = "us"
 DEFAULT_NAME = "MSW"
-DEFAULT_ATTRIBUTION = "Data provided by magicseaweed.com"
 
 ICON = "mdi:waves"
 
@@ -127,6 +121,7 @@ def setup_platform(
 class MagicSeaweedSensor(SensorEntity):
     """Implementation of a MagicSeaweed sensor."""
 
+    _attr_attribution = "Data provided by magicseaweed.com"
     _attr_icon = ICON
 
     def __init__(
@@ -151,7 +146,7 @@ class MagicSeaweedSensor(SensorEntity):
         else:
             self._attr_name = f"{hour} {name} {description.name}"
 
-        self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
+        self._attr_extra_state_attributes = {}
 
     @property
     def unit_system(self):
diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py
index 823018f405f..b3a0ad498d9 100644
--- a/homeassistant/components/meteo_france/sensor.py
+++ b/homeassistant/components/meteo_france/sensor.py
@@ -8,7 +8,6 @@ from meteofrance_api.helpers import (
 
 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.device_registry import DeviceEntryType
 from homeassistant.helpers.entity import DeviceInfo
@@ -81,6 +80,7 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity):
     """Representation of a Meteo-France sensor."""
 
     entity_description: MeteoFranceSensorEntityDescription
+    _attr_attribution = ATTRIBUTION
 
     def __init__(
         self,
@@ -94,7 +94,6 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity):
             city_name = coordinator.data.position["name"]
             self._attr_name = f"{city_name} {description.name}"
             self._attr_unique_id = f"{coordinator.data.position['lat']},{coordinator.data.position['lon']}_{description.key}"
-        self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
 
     @property
     def device_info(self) -> DeviceInfo:
@@ -162,7 +161,6 @@ class MeteoFranceRainSensor(MeteoFranceSensor):
                 f"{int((item['dt'] - reference_dt) / 60)} min": item["desc"]
                 for item in self.coordinator.data.forecast
             },
-            ATTR_ATTRIBUTION: ATTRIBUTION,
         }
 
 
@@ -192,7 +190,6 @@ class MeteoFranceAlertSensor(MeteoFranceSensor):
         """Return the state attributes."""
         return {
             **readeable_phenomenoms_dict(self.coordinator.data.phenomenons_max_colors),
-            ATTR_ATTRIBUTION: ATTRIBUTION,
         }
 
 
diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py
index 77532b379b6..82a6712af90 100644
--- a/homeassistant/components/metoffice/sensor.py
+++ b/homeassistant/components/metoffice/sensor.py
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     LENGTH_KILOMETERS,
     PERCENTAGE,
     SPEED_MILES_PER_HOUR,
@@ -180,6 +179,7 @@ class MetOfficeCurrentSensor(
 ):
     """Implementation of a Met Office current weather condition sensor."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_has_entity_name = True
 
     def __init__(
@@ -258,7 +258,6 @@ class MetOfficeCurrentSensor(
     def extra_state_attributes(self) -> dict[str, Any]:
         """Return the state attributes of the device."""
         return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_LAST_UPDATE: self.coordinator.data.now.date,
             ATTR_SENSOR_ID: self.entity_description.key,
             ATTR_SITE_ID: self.coordinator.data.site.id,
diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py
index 089d7ac5fbc..0f0d32908ef 100644
--- a/homeassistant/components/mvglive/sensor.py
+++ b/homeassistant/components/mvglive/sensor.py
@@ -9,7 +9,7 @@ import MVGLive
 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
@@ -90,6 +90,8 @@ def setup_platform(
 class MVGLiveSensor(SensorEntity):
     """Implementation of an MVG Live sensor."""
 
+    _attr_attribution = ATTRIBUTION
+
     def __init__(
         self,
         station,
@@ -210,7 +212,7 @@ class MVGLiveData:
                 continue
 
             # now select the relevant data
-            _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION}
+            _nextdep = {}
             for k in ("destination", "linename", "time", "direction", "product"):
                 _nextdep[k] = _departure.get(k, "")
             _nextdep["time"] = int(_nextdep["time"])
diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py
index 913ea3800f0..4d7a652601c 100644
--- a/homeassistant/components/nederlandse_spoorwegen/sensor.py
+++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py
@@ -10,7 +10,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_NAME
+from homeassistant.const import CONF_API_KEY, CONF_NAME
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import PlatformNotReady
 import homeassistant.helpers.config_validation as cv
@@ -20,8 +20,6 @@ from homeassistant.util import Throttle
 
 _LOGGER = logging.getLogger(__name__)
 
-ATTRIBUTION = "Data provided by NS"
-
 CONF_ROUTES = "routes"
 CONF_FROM = "from"
 CONF_TO = "to"
@@ -105,6 +103,8 @@ def valid_stations(stations, given_stations):
 class NSDepartureSensor(SensorEntity):
     """Implementation of a NS Departure Sensor."""
 
+    _attr_attribution = "Data provided by NS"
+
     def __init__(self, nsapi, name, departure, heading, via, time):
         """Initialize the sensor."""
         self._nsapi = nsapi
@@ -160,7 +160,6 @@ class NSDepartureSensor(SensorEntity):
             "transfers": self._trips[0].nr_transfers,
             "route": route,
             "remarks": None,
-            ATTR_ATTRIBUTION: ATTRIBUTION,
         }
 
         # Planned departure attributes
diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py
index 081d06f5d4f..d0359d739fd 100644
--- a/homeassistant/components/netatmo/netatmo_entity_base.py
+++ b/homeassistant/components/netatmo/netatmo_entity_base.py
@@ -8,7 +8,6 @@ from pyatmo.modules.device_types import (
     DeviceType as NetatmoDeviceType,
 )
 
-from homeassistant.const import ATTR_ATTRIBUTION
 from homeassistant.core import callback
 from homeassistant.helpers import device_registry as dr
 from homeassistant.helpers.entity import DeviceInfo, Entity
@@ -20,6 +19,8 @@ from .data_handler import PUBLIC, NetatmoDataHandler
 class NetatmoBase(Entity):
     """Netatmo entity base class."""
 
+    _attr_attribution = DEFAULT_ATTRIBUTION
+
     def __init__(self, data_handler: NetatmoDataHandler) -> None:
         """Set up Netatmo entity base."""
         self.data_handler = data_handler
@@ -31,7 +32,7 @@ class NetatmoBase(Entity):
         self._config_url: str = ""
         self._attr_name = None
         self._attr_unique_id = None
-        self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
+        self._attr_extra_state_attributes = {}
 
     async def async_added_to_hass(self) -> None:
         """Entity created."""
diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py
index 56fa0cd4a8d..51d2ae3c082 100644
--- a/homeassistant/components/nmbs/sensor.py
+++ b/homeassistant/components/nmbs/sensor.py
@@ -8,7 +8,6 @@ import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     ATTR_LATITUDE,
     ATTR_LONGITUDE,
     CONF_NAME,
@@ -100,6 +99,8 @@ def setup_platform(
 class NMBSLiveBoard(SensorEntity):
     """Get the next train from a station's liveboard."""
 
+    _attr_attribution = "https://api.irail.be/"
+
     def __init__(self, api_client, live_station, station_from, station_to):
         """Initialize the sensor for getting liveboard data."""
         self._station = live_station
@@ -149,7 +150,6 @@ class NMBSLiveBoard(SensorEntity):
             "extra_train": int(self._attrs["isExtra"]) > 0,
             "vehicle_id": self._attrs["vehicle"],
             "monitored_station": self._station,
-            ATTR_ATTRIBUTION: "https://api.irail.be/",
         }
 
         if delay > 0:
@@ -176,6 +176,7 @@ class NMBSLiveBoard(SensorEntity):
 class NMBSSensor(SensorEntity):
     """Get the the total travel time for a given connection."""
 
+    _attr_attribution = "https://api.irail.be/"
     _attr_native_unit_of_measurement = TIME_MINUTES
 
     def __init__(
@@ -223,7 +224,6 @@ class NMBSSensor(SensorEntity):
             "platform_arriving": self._attrs["arrival"]["platform"],
             "platform_departing": self._attrs["departure"]["platform"],
             "vehicle_id": self._attrs["departure"]["vehicle"],
-            ATTR_ATTRIBUTION: "https://api.irail.be/",
         }
 
         if canceled != 1:
diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py
index 8de16055714..7f3260c7635 100644
--- a/homeassistant/components/noaa_tides/sensor.py
+++ b/homeassistant/components/noaa_tides/sensor.py
@@ -9,12 +9,7 @@ import requests
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import (
-    ATTR_ATTRIBUTION,
-    CONF_NAME,
-    CONF_TIME_ZONE,
-    CONF_UNIT_SYSTEM,
-)
+from homeassistant.const import CONF_NAME, CONF_TIME_ZONE, CONF_UNIT_SYSTEM
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import PlatformNotReady
 import homeassistant.helpers.config_validation as cv
@@ -26,7 +21,6 @@ _LOGGER = logging.getLogger(__name__)
 
 CONF_STATION_ID = "station_id"
 
-DEFAULT_ATTRIBUTION = "Data provided by NOAA"
 DEFAULT_NAME = "NOAA Tides"
 DEFAULT_TIMEZONE = "lst_ldt"
 
@@ -85,6 +79,8 @@ def setup_platform(
 class NOAATidesAndCurrentsSensor(SensorEntity):
     """Representation of a NOAA Tides and Currents sensor."""
 
+    _attr_attribution = "Data provided by NOAA"
+
     def __init__(self, name, station_id, timezone, unit_system, station):
         """Initialize the sensor."""
         self._name = name
@@ -102,7 +98,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity):
     @property
     def extra_state_attributes(self):
         """Return the state attributes of this device."""
-        attr = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
+        attr = {}
         if self.data is None:
             return attr
         if self.data["hi_lo"][1] == "H":
diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py
index 82a5e3a59a8..0bc2cf12be2 100644
--- a/homeassistant/components/nsw_fuel_station/sensor.py
+++ b/homeassistant/components/nsw_fuel_station/sensor.py
@@ -6,7 +6,7 @@ import logging
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_CENT, VOLUME_LITERS
+from homeassistant.const import CURRENCY_CENT, VOLUME_LITERS
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -40,7 +40,6 @@ CONF_ALLOWED_FUEL_TYPES = [
 ]
 CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"]
 
-ATTRIBUTION = "Data provided by NSW Government FuelCheck"
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
     {
         vol.Required(CONF_STATION_ID): cv.positive_int,
@@ -88,6 +87,8 @@ class StationPriceSensor(
 ):
     """Implementation of a sensor that reports the fuel price for a station."""
 
+    _attr_attribution = "Data provided by NSW Government FuelCheck"
+
     def __init__(
         self,
         coordinator: DataUpdateCoordinator[StationPriceData],
@@ -121,7 +122,6 @@ class StationPriceSensor(
         return {
             ATTR_STATION_ID: self._station_id,
             ATTR_STATION_NAME: self._get_station_name(),
-            ATTR_ATTRIBUTION: ATTRIBUTION,
         }
 
     @property
diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py
index 3cb624190e7..664ad033cfe 100644
--- a/homeassistant/components/oasa_telematics/sensor.py
+++ b/homeassistant/components/oasa_telematics/sensor.py
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
     SensorDeviceClass,
     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
@@ -30,8 +30,6 @@ ATTR_NEXT_ARRIVAL = "next_arrival"
 ATTR_SECOND_NEXT_ARRIVAL = "second_next_arrival"
 ATTR_NEXT_DEPARTURE = "next_departure"
 
-ATTRIBUTION = "Data retrieved from telematics.oasa.gr"
-
 CONF_STOP_ID = "stop_id"
 CONF_ROUTE_ID = "route_id"
 
@@ -68,6 +66,8 @@ def setup_platform(
 class OASATelematicsSensor(SensorEntity):
     """Implementation of the OASA Telematics sensor."""
 
+    _attr_attribution = "Data retrieved from telematics.oasa.gr"
+
     def __init__(self, data, stop_id, route_id, name):
         """Initialize the sensor."""
         self.data = data
@@ -111,7 +111,6 @@ class OASATelematicsSensor(SensorEntity):
                 {
                     ATTR_ROUTE_ID: self._times[0][ATTR_ROUTE_ID],
                     ATTR_STOP_ID: self._stop_id,
-                    ATTR_ATTRIBUTION: ATTRIBUTION,
                 }
             )
         params.update(
diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py
index 66579eb8173..4c96d88f321 100644
--- a/homeassistant/components/opensky/sensor.py
+++ b/homeassistant/components/opensky/sensor.py
@@ -8,7 +8,6 @@ import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     ATTR_LATITUDE,
     ATTR_LONGITUDE,
     CONF_LATITUDE,
@@ -42,9 +41,6 @@ EVENT_OPENSKY_ENTRY = f"{DOMAIN}_entry"
 EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit"
 SCAN_INTERVAL = timedelta(seconds=12)  # opensky public limit is 10 seconds
 
-OPENSKY_ATTRIBUTION = (
-    "Information provided by the OpenSky Network (https://opensky-network.org)"
-)
 OPENSKY_API_URL = "https://opensky-network.org/api/states/all"
 OPENSKY_API_FIELDS = [
     ATTR_ICAO24,
@@ -101,6 +97,10 @@ def setup_platform(
 class OpenSkySensor(SensorEntity):
     """Open Sky Network Sensor."""
 
+    _attr_attribution = (
+        "Information provided by the OpenSky Network (https://opensky-network.org)"
+    )
+
     def __init__(self, hass, name, latitude, longitude, radius, altitude):
         """Initialize the sensor."""
         self._session = requests.Session()
@@ -188,11 +188,6 @@ class OpenSkySensor(SensorEntity):
         self._state = len(currently_tracked)
         self._previously_tracked = currently_tracked
 
-    @property
-    def extra_state_attributes(self):
-        """Return the state attributes."""
-        return {ATTR_ATTRIBUTION: OPENSKY_ATTRIBUTION}
-
     @property
     def native_unit_of_measurement(self):
         """Return the unit of measurement."""
-- 
GitLab


From 6b56336e52b512ef22906d824a4563a584a664cf Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Tue, 18 Oct 2022 13:16:27 +0200
Subject: [PATCH 566/985] Revert BSBLAN name change to fix hassfest (#80525)

---
 homeassistant/components/bsblan/manifest.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json
index 2857cce7b39..7c5422d3eff 100644
--- a/homeassistant/components/bsblan/manifest.json
+++ b/homeassistant/components/bsblan/manifest.json
@@ -1,6 +1,6 @@
 {
   "domain": "bsblan",
-  "name": "BSBLAN",
+  "name": "BSB-Lan",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/bsblan",
   "requirements": ["python-bsblan==0.5.5"],
-- 
GitLab


From c37e4b870f10304624e594fe1202d794f668c7b9 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 13:33:20 +0200
Subject: [PATCH 567/985] Move attribution to standalone attribute [r-s]
 (#80520)

* Move attribution to standalone attribute [r-s]

* Fix test
---
 homeassistant/components/raincloud/__init__.py     |  7 +++----
 homeassistant/components/raincloud/switch.py       |  4 +---
 homeassistant/components/rejseplanen/sensor.py     |  9 ++++-----
 homeassistant/components/ripple/sensor.py          | 11 +++--------
 homeassistant/components/sense/binary_sensor.py    |  7 +------
 homeassistant/components/seventeentrack/sensor.py  |  6 +++---
 homeassistant/components/srp_energy/sensor.py      | 14 ++------------
 .../components/swiss_hydrological_data/sensor.py   | 11 ++++++-----
 .../components/swiss_public_transport/sensor.py    |  7 +++----
 tests/components/srp_energy/test_sensor.py         |  4 ++--
 10 files changed, 28 insertions(+), 52 deletions(-)

diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py
index 33fd3cf1520..40a9514e1d7 100644
--- a/homeassistant/components/raincloud/__init__.py
+++ b/homeassistant/components/raincloud/__init__.py
@@ -8,7 +8,6 @@ import voluptuous as vol
 
 from homeassistant.components import persistent_notification
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     CONF_PASSWORD,
     CONF_SCAN_INTERVAL,
     CONF_USERNAME,
@@ -27,8 +26,6 @@ _LOGGER = logging.getLogger(__name__)
 
 ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60]
 
-ATTRIBUTION = "Data provided by Melnor Aquatimer.com"
-
 CONF_WATERING_TIME = "watering_minutes"
 
 NOTIFICATION_ID = "raincloud_notification"
@@ -140,6 +137,8 @@ class RainCloudHub:
 class RainCloudEntity(Entity):
     """Entity class for RainCloud devices."""
 
+    _attr_attribution = "Data provided by Melnor Aquatimer.com"
+
     def __init__(self, data, sensor_type):
         """Initialize the RainCloud entity."""
         self.data = data
@@ -167,7 +166,7 @@ class RainCloudEntity(Entity):
     @property
     def extra_state_attributes(self):
         """Return the state attributes."""
-        return {ATTR_ATTRIBUTION: ATTRIBUTION, "identifier": self.data.serial}
+        return {"identifier": self.data.serial}
 
     @property
     def icon(self):
diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py
index 89b2673f66e..abcb680daa3 100644
--- a/homeassistant/components/raincloud/switch.py
+++ b/homeassistant/components/raincloud/switch.py
@@ -7,7 +7,7 @@ from typing import Any
 import voluptuous as vol
 
 from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
+from homeassistant.const import CONF_MONITORED_CONDITIONS
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -15,7 +15,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 from . import (
     ALLOWED_WATERING_TIME,
-    ATTRIBUTION,
     CONF_WATERING_TIME,
     DATA_RAINCLOUD,
     DEFAULT_WATERING_TIME,
@@ -97,7 +96,6 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity):
     def extra_state_attributes(self):
         """Return the state attributes."""
         return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             "default_manual_timer": self._default_watering_timer,
             "identifier": self.data.serial,
         }
diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py
index 6943e7f8aa3..fa1f9cc0b24 100644
--- a/homeassistant/components/rejseplanen/sensor.py
+++ b/homeassistant/components/rejseplanen/sensor.py
@@ -15,7 +15,7 @@ import rjpl
 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
@@ -37,8 +37,6 @@ ATTR_REAL_TIME_AT = "real_time_at"
 ATTR_TRACK = "track"
 ATTR_NEXT_UP = "next_departures"
 
-ATTRIBUTION = "Data provided by rejseplanen.dk"
-
 CONF_STOP_ID = "stop_id"
 CONF_ROUTE = "route"
 CONF_DIRECTION = "direction"
@@ -100,6 +98,8 @@ def setup_platform(
 class RejseplanenTransportSensor(SensorEntity):
     """Implementation of Rejseplanen transport sensor."""
 
+    _attr_attribution = "Data provided by rejseplanen.dk"
+
     def __init__(self, data, stop_id, route, direction, name):
         """Initialize the sensor."""
         self.data = data
@@ -123,14 +123,13 @@ class RejseplanenTransportSensor(SensorEntity):
     def extra_state_attributes(self):
         """Return the state attributes."""
         if not self._times:
-            return {ATTR_STOP_ID: self._stop_id, ATTR_ATTRIBUTION: ATTRIBUTION}
+            return {ATTR_STOP_ID: self._stop_id}
 
         next_up = []
         if len(self._times) > 1:
             next_up = self._times[1:]
 
         attributes = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_NEXT_UP: next_up,
             ATTR_STOP_ID: self._stop_id,
         }
diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py
index 01f326b2a63..bab765c289c 100644
--- a/homeassistant/components/ripple/sensor.py
+++ b/homeassistant/components/ripple/sensor.py
@@ -7,14 +7,12 @@ from pyripple import get_balance
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME
+from homeassistant.const import CONF_ADDRESS, CONF_NAME
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
-ATTRIBUTION = "Data provided by ripple.com"
-
 DEFAULT_NAME = "Ripple Balance"
 
 SCAN_INTERVAL = timedelta(minutes=5)
@@ -43,6 +41,8 @@ def setup_platform(
 class RippleSensor(SensorEntity):
     """Representation of an Ripple.com sensor."""
 
+    _attr_attribution = "Data provided by ripple.com"
+
     def __init__(self, name, address):
         """Initialize the sensor."""
         self._name = name
@@ -65,11 +65,6 @@ class RippleSensor(SensorEntity):
         """Return the unit of measurement this sensor expresses itself in."""
         return self._unit_of_measurement
 
-    @property
-    def extra_state_attributes(self):
-        """Return the state attributes of the sensor."""
-        return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
     def update(self) -> None:
         """Get the latest state of the sensor."""
         if (balance := get_balance(self.address)) is not None:
diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py
index 0d4fbcf1de2..2aee20be5ae 100644
--- a/homeassistant/components/sense/binary_sensor.py
+++ b/homeassistant/components/sense/binary_sensor.py
@@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import (
     BinarySensorEntity,
 )
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ATTRIBUTION
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers import entity_registry as er
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -73,6 +72,7 @@ def sense_to_mdi(sense_icon):
 class SenseDevice(BinarySensorEntity):
     """Implementation of a Sense energy device binary sensor."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_should_poll = False
 
     def __init__(self, sense_devices_data, device, sense_monitor_id):
@@ -111,11 +111,6 @@ class SenseDevice(BinarySensorEntity):
         """Return the old not so unique id of the binary sensor."""
         return self._id
 
-    @property
-    def extra_state_attributes(self):
-        """Return the state attributes."""
-        return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
     @property
     def icon(self):
         """Return the icon of the binary sensor."""
diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py
index 363eb507710..9cc3c8ffd57 100644
--- a/homeassistant/components/seventeentrack/sensor.py
+++ b/homeassistant/components/seventeentrack/sensor.py
@@ -11,7 +11,6 @@ import voluptuous as vol
 from homeassistant.components import persistent_notification
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     ATTR_FRIENDLY_NAME,
     ATTR_LOCATION,
     CONF_PASSWORD,
@@ -112,12 +111,13 @@ async def async_setup_platform(
 class SeventeenTrackSummarySensor(SensorEntity):
     """Define a summary sensor."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_icon = "mdi:package"
     _attr_native_unit_of_measurement = "packages"
 
     def __init__(self, data, status, initial_state):
         """Initialize."""
-        self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
+        self._attr_extra_state_attributes = {}
         self._data = data
         self._state = initial_state
         self._status = status
@@ -164,12 +164,12 @@ class SeventeenTrackSummarySensor(SensorEntity):
 class SeventeenTrackPackageSensor(SensorEntity):
     """Define an individual package sensor."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_icon = "mdi:package"
 
     def __init__(self, data, package):
         """Initialize."""
         self._attr_extra_state_attributes = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_DESTINATION_COUNTRY: package.destination_country,
             ATTR_INFO_TEXT: package.info_text,
             ATTR_TIMESTAMP: package.timestamp,
diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py
index 0e6c7cda577..61b9938e474 100644
--- a/homeassistant/components/srp_energy/sensor.py
+++ b/homeassistant/components/srp_energy/sensor.py
@@ -11,7 +11,7 @@ from homeassistant.components.sensor import (
     SensorStateClass,
 )
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR
+from homeassistant.const import ENERGY_KILO_WATT_HOUR
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -83,6 +83,7 @@ async def async_setup_entry(
 class SrpEntity(SensorEntity):
     """Implementation of a Srp Energy Usage sensor."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_should_poll = False
 
     def __init__(self, coordinator):
@@ -127,17 +128,6 @@ class SrpEntity(SensorEntity):
             return f"{self.coordinator.data:.2f}"
         return None
 
-    @property
-    def extra_state_attributes(self):
-        """Return the state attributes."""
-        if not self.coordinator.data:
-            return None
-        attributes = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
-        }
-
-        return attributes
-
     @property
     def available(self):
         """Return if entity is available."""
diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py
index 7c14428c2fc..dfa8b8fba04 100644
--- a/homeassistant/components/swiss_hydrological_data/sensor.py
+++ b/homeassistant/components/swiss_hydrological_data/sensor.py
@@ -8,7 +8,7 @@ from swisshydrodata import SwissHydroData
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
+from homeassistant.const import CONF_MONITORED_CONDITIONS
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -17,8 +17,6 @@ from homeassistant.util import Throttle
 
 _LOGGER = logging.getLogger(__name__)
 
-ATTRIBUTION = "Data provided by the Swiss Federal Office for the Environment FOEN"
-
 ATTR_MAX_24H = "max-24h"
 ATTR_MEAN_24H = "mean-24h"
 ATTR_MIN_24H = "min-24h"
@@ -85,6 +83,10 @@ def setup_platform(
 class SwissHydrologicalDataSensor(SensorEntity):
     """Implementation of a Swiss hydrological sensor."""
 
+    _attr_attribution = (
+        "Data provided by the Swiss Federal Office for the Environment FOEN"
+    )
+
     def __init__(self, hydro_data, station, condition):
         """Initialize the Swiss hydrological sensor."""
         self.hydro_data = hydro_data
@@ -123,7 +125,7 @@ class SwissHydrologicalDataSensor(SensorEntity):
         attrs = {}
 
         if not self._data:
-            attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
+
             return attrs
 
         attrs[ATTR_WATER_BODY_TYPE] = self._data["water-body-type"]
@@ -131,7 +133,6 @@ class SwissHydrologicalDataSensor(SensorEntity):
         attrs[ATTR_STATION_UPDATE] = self._data["parameters"][self._condition][
             "datetime"
         ]
-        attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
 
         for entry in CONDITION_DETAILS:
             attrs[entry.replace("-", "_")] = self._data["parameters"][self._condition][
diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py
index e23b8cd3aeb..8735726f892 100644
--- a/homeassistant/components/swiss_public_transport/sensor.py
+++ b/homeassistant/components/swiss_public_transport/sensor.py
@@ -9,7 +9,7 @@ from opendata_transport.exceptions import OpendataTransportError
 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
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 import homeassistant.helpers.config_validation as cv
@@ -30,8 +30,6 @@ ATTR_TRAIN_NUMBER = "train_number"
 ATTR_TRANSFERS = "transfers"
 ATTR_DELAY = "delay"
 
-ATTRIBUTION = "Data provided by transport.opendata.ch"
-
 CONF_DESTINATION = "to"
 CONF_START = "from"
 
@@ -80,6 +78,8 @@ async def async_setup_platform(
 class SwissPublicTransportSensor(SensorEntity):
     """Implementation of an Swiss public transport sensor."""
 
+    _attr_attribution = "Data provided by transport.opendata.ch"
+
     def __init__(self, opendata, start, destination, name):
         """Initialize the sensor."""
         self._opendata = opendata
@@ -122,7 +122,6 @@ class SwissPublicTransportSensor(SensorEntity):
             ATTR_START: self._opendata.from_name,
             ATTR_TARGET: self._opendata.to_name,
             ATTR_REMAINING_TIME: f"{self._remaining_time}",
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_DELAY: self._opendata.connections[0]["delay"],
         }
 
diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py
index cb19ae8720a..3cf2837353e 100644
--- a/tests/components/srp_energy/test_sensor.py
+++ b/tests/components/srp_energy/test_sensor.py
@@ -11,7 +11,7 @@ from homeassistant.components.srp_energy.const import (
     SRP_ENERGY_DOMAIN,
 )
 from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry
-from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR
+from homeassistant.const import ENERGY_KILO_WATT_HOUR
 
 
 async def test_async_setup_entry(hass):
@@ -93,7 +93,7 @@ async def test_srp_entity(hass):
     assert srp_entity.icon == ICON
     assert srp_entity.usage == "2.00"
     assert srp_entity.should_poll is False
-    assert srp_entity.extra_state_attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
+    assert srp_entity.attribution == ATTRIBUTION
     assert srp_entity.available is not None
     assert srp_entity.device_class is SensorDeviceClass.ENERGY
     assert srp_entity.state_class is SensorStateClass.TOTAL_INCREASING
-- 
GitLab


From 62690759d483b03932939d28bbf87ce3b293c486 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 13:33:36 +0200
Subject: [PATCH 568/985] Move attribution to standalone attribute [t-z]
 (#80521)

---
 homeassistant/components/tankerkoenig/sensor.py     |  9 ++-------
 homeassistant/components/tmb/sensor.py              |  7 +++----
 homeassistant/components/tomorrowio/__init__.py     |  7 ++-----
 homeassistant/components/tomorrowio/sensor.py       |  2 --
 homeassistant/components/transport_nsw/sensor.py    | 13 +++----------
 homeassistant/components/travisci/sensor.py         |  6 ++----
 homeassistant/components/vasttrafik/sensor.py       |  6 +++---
 homeassistant/components/viaggiatreno/sensor.py     |  7 +++----
 homeassistant/components/waze_travel_time/sensor.py |  3 +--
 homeassistant/components/yandex_transport/sensor.py |  7 ++++---
 10 files changed, 23 insertions(+), 44 deletions(-)

diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py
index c63b0ea0e7e..e3b2ca9554b 100644
--- a/homeassistant/components/tankerkoenig/sensor.py
+++ b/homeassistant/components/tankerkoenig/sensor.py
@@ -5,12 +5,7 @@ import logging
 
 from homeassistant.components.sensor import SensorEntity, SensorStateClass
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import (
-    ATTR_ATTRIBUTION,
-    ATTR_LATITUDE,
-    ATTR_LONGITUDE,
-    CURRENCY_EURO,
-)
+from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CURRENCY_EURO
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
@@ -62,6 +57,7 @@ async def async_setup_entry(
 class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity):
     """Contains prices for fuel in a given station."""
 
+    _attr_attribution = ATTRIBUTION
     _attr_state_class = SensorStateClass.MEASUREMENT
     _attr_icon = "mdi:gas-station"
 
@@ -74,7 +70,6 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity):
         self._attr_native_unit_of_measurement = CURRENCY_EURO
         self._attr_unique_id = f"{station['id']}_{fuel_type}"
         attrs = {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_BRAND: station["brand"],
             ATTR_FUEL_TYPE: fuel_type,
             ATTR_STATION_NAME: station["name"],
diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py
index 8d6db00b2b9..cd9453ed430 100644
--- a/homeassistant/components/tmb/sensor.py
+++ b/homeassistant/components/tmb/sensor.py
@@ -9,7 +9,7 @@ from tmb import IBus
 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
@@ -18,8 +18,6 @@ from homeassistant.util import Throttle
 
 _LOGGER = logging.getLogger(__name__)
 
-ATTRIBUTION = "Data provided by Transport Metropolitans de Barcelona"
-
 ICON = "mdi:bus-clock"
 
 CONF_APP_ID = "app_id"
@@ -75,6 +73,8 @@ def setup_platform(
 class TMBSensor(SensorEntity):
     """Implementation of a TMB line/stop Sensor."""
 
+    _attr_attribution = "Data provided by Transport Metropolitans de Barcelona"
+
     def __init__(self, ibus_client, stop, line, name):
         """Initialize the sensor."""
         self._ibus_client = ibus_client
@@ -113,7 +113,6 @@ class TMBSensor(SensorEntity):
     def extra_state_attributes(self):
         """Return the state attributes of the last update."""
         return {
-            ATTR_ATTRIBUTION: ATTRIBUTION,
             ATTR_BUS_STOP: self._stop,
             ATTR_LINE: self._line,
         }
diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py
index 956b0901dd7..e2aaba6cdff 100644
--- a/homeassistant/components/tomorrowio/__init__.py
+++ b/homeassistant/components/tomorrowio/__init__.py
@@ -320,6 +320,8 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator):
 class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]):
     """Base Tomorrow.io Entity."""
 
+    _attr_attribution = ATTRIBUTION
+
     def __init__(
         self,
         config_entry: ConfigEntry,
@@ -346,8 +348,3 @@ class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]):
         """
         entry_id = self._config_entry.entry_id
         return self.coordinator.data[entry_id].get(CURRENT, {}).get(property_name)
-
-    @property
-    def attribution(self):
-        """Return the attribution."""
-        return ATTRIBUTION
diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py
index 1f3bb74b686..78c973fd80b 100644
--- a/homeassistant/components/tomorrowio/sensor.py
+++ b/homeassistant/components/tomorrowio/sensor.py
@@ -20,7 +20,6 @@ from homeassistant.components.sensor import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
     CONCENTRATION_PARTS_PER_MILLION,
     CONF_API_KEY,
@@ -326,7 +325,6 @@ class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity):
         self._attr_unique_id = (
             f"{self._config_entry.unique_id}_{slugify(description.name)}"
         )
-        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 IMPERIAL_SYSTEM:
diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py
index 27fd6fd67fd..116dd5c0923 100644
--- a/homeassistant/components/transport_nsw/sensor.py
+++ b/homeassistant/components/transport_nsw/sensor.py
@@ -7,13 +7,7 @@ from TransportNSW import TransportNSW
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import (
-    ATTR_ATTRIBUTION,
-    ATTR_MODE,
-    CONF_API_KEY,
-    CONF_NAME,
-    TIME_MINUTES,
-)
+from homeassistant.const import ATTR_MODE, CONF_API_KEY, CONF_NAME, TIME_MINUTES
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -26,8 +20,6 @@ ATTR_DELAY = "delay"
 ATTR_REAL_TIME = "real_time"
 ATTR_DESTINATION = "destination"
 
-ATTRIBUTION = "Data provided by Transport NSW"
-
 CONF_STOP_ID = "stop_id"
 CONF_ROUTE = "route"
 CONF_DESTINATION = "destination"
@@ -77,6 +69,8 @@ def setup_platform(
 class TransportNSWSensor(SensorEntity):
     """Implementation of an Transport NSW sensor."""
 
+    _attr_attribution = "Data provided by Transport NSW"
+
     def __init__(self, data, stop_id, name):
         """Initialize the sensor."""
         self.data = data
@@ -107,7 +101,6 @@ class TransportNSWSensor(SensorEntity):
                 ATTR_REAL_TIME: self._times[ATTR_REAL_TIME],
                 ATTR_DESTINATION: self._times[ATTR_DESTINATION],
                 ATTR_MODE: self._times[ATTR_MODE],
-                ATTR_ATTRIBUTION: ATTRIBUTION,
             }
 
     @property
diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py
index c35391d4573..ab1cd4a6b03 100644
--- a/homeassistant/components/travisci/sensor.py
+++ b/homeassistant/components/travisci/sensor.py
@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
     SensorEntityDescription,
 )
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     CONF_API_KEY,
     CONF_MONITORED_CONDITIONS,
     CONF_SCAN_INTERVAL,
@@ -28,8 +27,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 _LOGGER = logging.getLogger(__name__)
 
-ATTRIBUTION = "Information provided by https://travis-ci.org/"
-
 CONF_BRANCH = "branch"
 CONF_REPOSITORY = "repository"
 
@@ -142,6 +139,8 @@ def setup_platform(
 class TravisCISensor(SensorEntity):
     """Representation of a Travis CI sensor."""
 
+    _attr_attribution = "Information provided by https://travis-ci.org/"
+
     def __init__(
         self, data, repo_name, user, branch, description: SensorEntityDescription
     ):
@@ -159,7 +158,6 @@ class TravisCISensor(SensorEntity):
     def extra_state_attributes(self):
         """Return the state attributes."""
         attrs = {}
-        attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
 
         if self._build and self._attr_native_value is not None:
             if self._user and self.entity_description.key == "state":
diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py
index d4229066eff..118d04d3c1b 100644
--- a/homeassistant/components/vasttrafik/sensor.py
+++ b/homeassistant/components/vasttrafik/sensor.py
@@ -8,7 +8,7 @@ import vasttrafik
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_DELAY, CONF_NAME
+from homeassistant.const import CONF_DELAY, CONF_NAME
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -22,7 +22,6 @@ ATTR_ACCESSIBILITY = "accessibility"
 ATTR_DIRECTION = "direction"
 ATTR_LINE = "line"
 ATTR_TRACK = "track"
-ATTRIBUTION = "Data provided by Västtrafik"
 
 CONF_DEPARTURES = "departures"
 CONF_FROM = "from"
@@ -83,6 +82,8 @@ def setup_platform(
 class VasttrafikDepartureSensor(SensorEntity):
     """Implementation of a Vasttrafik Departure Sensor."""
 
+    _attr_attribution = "Data provided by Västtrafik"
+
     def __init__(self, planner, name, departure, heading, lines, delay):
         """Initialize the sensor."""
         self._planner = planner
@@ -158,7 +159,6 @@ class VasttrafikDepartureSensor(SensorEntity):
 
                     params = {
                         ATTR_ACCESSIBILITY: departure.get("accessibility"),
-                        ATTR_ATTRIBUTION: ATTRIBUTION,
                         ATTR_DIRECTION: departure.get("direction"),
                         ATTR_LINE: departure.get("sname"),
                         ATTR_TRACK: departure.get("track"),
diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py
index acb6ffa647e..bdc8909da0f 100644
--- a/homeassistant/components/viaggiatreno/sensor.py
+++ b/homeassistant/components/viaggiatreno/sensor.py
@@ -11,7 +11,7 @@ import async_timeout
 import voluptuous as vol
 
 from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, TIME_MINUTES
+from homeassistant.const import TIME_MINUTES
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 import homeassistant.helpers.config_validation as cv
@@ -20,8 +20,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 _LOGGER = logging.getLogger(__name__)
 
-ATTRIBUTION = "Powered by ViaggiaTreno Data"
-
 VIAGGIATRENO_ENDPOINT = (
     "http://www.viaggiatreno.it/infomobilita/"
     "resteasy/viaggiatreno/andamentoTreno/"
@@ -96,6 +94,8 @@ async def async_http_request(hass, uri):
 class ViaggiaTrenoSensor(SensorEntity):
     """Implementation of a ViaggiaTreno sensor."""
 
+    _attr_attribution = "Powered by ViaggiaTreno Data"
+
     def __init__(self, train_id, station_id, name):
         """Initialize the sensor."""
         self._state = None
@@ -132,7 +132,6 @@ class ViaggiaTrenoSensor(SensorEntity):
     @property
     def extra_state_attributes(self):
         """Return extra attributes."""
-        self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION
         return self._attributes
 
     @staticmethod
diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py
index c4ff489cadf..67081aeab5f 100644
--- a/homeassistant/components/waze_travel_time/sensor.py
+++ b/homeassistant/components/waze_travel_time/sensor.py
@@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
-    ATTR_ATTRIBUTION,
     CONF_NAME,
     CONF_REGION,
     EVENT_HOMEASSISTANT_STARTED,
@@ -115,6 +114,7 @@ async def async_setup_entry(
 class WazeTravelTime(SensorEntity):
     """Representation of a Waze travel time sensor."""
 
+    _attr_attribution = "Powered by Waze"
     _attr_native_unit_of_measurement = TIME_MINUTES
     _attr_device_class = SensorDeviceClass.DURATION
     _attr_state_class = SensorStateClass.MEASUREMENT
@@ -159,7 +159,6 @@ class WazeTravelTime(SensorEntity):
             return None
 
         return {
-            ATTR_ATTRIBUTION: "Powered by Waze",
             "duration": self._waze_data.duration,
             "distance": self._waze_data.distance,
             "route": self._waze_data.route,
diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py
index 3fdca47ef02..b7a846bbc67 100644
--- a/homeassistant/components/yandex_transport/sensor.py
+++ b/homeassistant/components/yandex_transport/sensor.py
@@ -12,7 +12,7 @@ from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
 )
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
+from homeassistant.const import CONF_NAME
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.aiohttp_client import async_create_clientsession
 import homeassistant.helpers.config_validation as cv
@@ -24,7 +24,6 @@ _LOGGER = logging.getLogger(__name__)
 
 STOP_NAME = "stop_name"
 USER_AGENT = "Home Assistant"
-ATTRIBUTION = "Data provided by maps.yandex.ru"
 
 CONF_STOP_ID = "stop_id"
 CONF_ROUTE = "routes"
@@ -70,6 +69,8 @@ async def async_setup_platform(
 class DiscoverYandexTransport(SensorEntity):
     """Implementation of yandex_transport sensor."""
 
+    _attr_attribution = "Data provided by maps.yandex.ru"
+
     def __init__(self, requester: YandexMapsRequester, stop_id, routes, name):
         """Initialize sensor."""
         self.requester = requester
@@ -138,7 +139,7 @@ class DiscoverYandexTransport(SensorEntity):
                         attrs[route] = []
                     attrs[route].append(departure["text"])
         attrs[STOP_NAME] = stop_name
-        attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
+
         if closer_time is None:
             self._state = None
         else:
-- 
GitLab


From 76bd8c431b7e45ec3c5fc872d3d36736534f0433 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 14:10:26 +0200
Subject: [PATCH 569/985] Use _attr_attribution in suez_water (#80527)

---
 homeassistant/components/suez_water/sensor.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py
index 77b1de6555e..4c0fe16a197 100644
--- a/homeassistant/components/suez_water/sensor.py
+++ b/homeassistant/components/suez_water/sensor.py
@@ -93,7 +93,8 @@ class SuezSensor(SensorEntity):
             # _state holds the volume of consumed water during previous day
             self._state = self.client.state
             self._available = True
-            self._attributes["attribution"] = self.client.attributes["attribution"]
+            self._attr_attribution = self.client.attributes["attribution"]
+
             self._attributes["this_month_consumption"] = {}
             for item in self.client.attributes["thisMonthConsumption"]:
                 self._attributes["this_month_consumption"][
-- 
GitLab


From 5442d6af0172ab3841e1198f6f26853e76855b72 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 16:41:17 +0200
Subject: [PATCH 570/985] Improve msg type hint in websocket commands (#80530)

---
 .../components/analytics/__init__.py          |  6 +-
 homeassistant/components/backup/websocket.py  |  8 +-
 homeassistant/components/camera/__init__.py   | 10 +--
 .../components/config/device_registry.py      |  6 +-
 .../components/energy/websocket_api.py        | 12 ++-
 homeassistant/components/frontend/__init__.py |  4 +-
 homeassistant/components/frontend/storage.py  |  4 +-
 .../components/hardware/websocket_api.py      |  5 +-
 .../components/hassio/websocket_api.py        |  7 +-
 homeassistant/components/history/__init__.py  |  8 +-
 homeassistant/components/insteon/api/aldb.py  | 18 ++--
 .../components/insteon/api/device.py          |  8 +-
 .../components/insteon/api/properties.py      | 12 +--
 .../components/logbook/websocket_api.py       |  4 +-
 .../components/media_source/__init__.py       |  4 +-
 .../components/media_source/local_source.py   |  3 +-
 homeassistant/components/network/websocket.py |  6 +-
 .../components/recorder/websocket_api.py      | 16 ++--
 .../components/shopping_list/__init__.py      |  7 +-
 homeassistant/components/usb/__init__.py      |  2 +-
 homeassistant/components/zwave_js/api.py      | 82 +++++++++----------
 21 files changed, 128 insertions(+), 104 deletions(-)

diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py
index f7bdb303eb7..bdc7806e456 100644
--- a/homeassistant/components/analytics/__init__.py
+++ b/homeassistant/components/analytics/__init__.py
@@ -1,4 +1,6 @@
 """Send instance and usage analytics."""
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
@@ -41,7 +43,7 @@ async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
 async def websocket_analytics(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Return analytics preferences."""
     analytics: Analytics = hass.data[DOMAIN]
@@ -62,7 +64,7 @@ async def websocket_analytics(
 async def websocket_analytics_preferences(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Update analytics preferences."""
     preferences = msg[ATTR_PREFERENCES]
diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py
index 5c12a764941..c203019cca9 100644
--- a/homeassistant/components/backup/websocket.py
+++ b/homeassistant/components/backup/websocket.py
@@ -1,4 +1,6 @@
 """Websocket commands for the Backup integration."""
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
@@ -22,7 +24,7 @@ def async_register_websocket_handlers(hass: HomeAssistant) -> None:
 async def handle_info(
     hass: HomeAssistant,
     connection: websocket_api.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """List all stored backups."""
     manager: BackupManager = hass.data[DOMAIN]
@@ -47,7 +49,7 @@ async def handle_info(
 async def handle_remove(
     hass: HomeAssistant,
     connection: websocket_api.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Remove a backup."""
     manager: BackupManager = hass.data[DOMAIN]
@@ -61,7 +63,7 @@ async def handle_remove(
 async def handle_create(
     hass: HomeAssistant,
     connection: websocket_api.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Generate a backup."""
     manager: BackupManager = hass.data[DOMAIN]
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index fa807dd1440..d860776a797 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -12,7 +12,7 @@ from functools import partial
 import logging
 import os
 from random import SystemRandom
-from typing import Final, Optional, cast, final
+from typing import Any, Final, Optional, cast, final
 
 from aiohttp import hdrs, web
 import async_timeout
@@ -786,7 +786,7 @@ class CameraMjpegStream(CameraView):
 )
 @websocket_api.async_response
 async def ws_camera_stream(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle get camera stream websocket command.
 
@@ -816,7 +816,7 @@ async def ws_camera_stream(
 )
 @websocket_api.async_response
 async def ws_camera_web_rtc_offer(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle the signal path for a WebRTC stream.
 
@@ -856,7 +856,7 @@ async def ws_camera_web_rtc_offer(
 )
 @websocket_api.async_response
 async def websocket_get_prefs(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle request for account info."""
     prefs = hass.data[DATA_CAMERA_PREFS].get(msg["entity_id"])
@@ -873,7 +873,7 @@ async def websocket_get_prefs(
 )
 @websocket_api.async_response
 async def websocket_update_prefs(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle request for account info."""
     prefs = hass.data[DATA_CAMERA_PREFS]
diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py
index 8edd9f1f4d3..6287d586343 100644
--- a/homeassistant/components/config/device_registry.py
+++ b/homeassistant/components/config/device_registry.py
@@ -1,6 +1,8 @@
 """HTTP views to interact with the device registry."""
 from __future__ import annotations
 
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant import loader
@@ -109,7 +111,9 @@ def websocket_update_device(hass, connection, msg):
 )
 @websocket_api.async_response
 async def websocket_remove_config_entry_from_device(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
 ) -> None:
     """Remove config entry from a device."""
     registry = async_get(hass)
diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py
index 7ba83cf15c9..f1b75c8ea9e 100644
--- a/homeassistant/components/energy/websocket_api.py
+++ b/homeassistant/components/energy/websocket_api.py
@@ -82,7 +82,9 @@ def _ws_with_manager(
     @websocket_api.async_response
     @functools.wraps(func)
     async def with_manager(
-        hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+        hass: HomeAssistant,
+        connection: websocket_api.ActiveConnection,
+        msg: dict[str, Any],
     ) -> None:
         manager = await async_get_manager(hass)
 
@@ -146,7 +148,7 @@ async def ws_save_prefs(
 async def ws_info(
     hass: HomeAssistant,
     connection: websocket_api.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Handle get info command."""
     forecast_platforms = await async_get_energy_platforms(hass)
@@ -168,7 +170,7 @@ async def ws_info(
 async def ws_validate(
     hass: HomeAssistant,
     connection: websocket_api.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Handle validate command."""
     connection.send_result(msg["id"], (await async_validate(hass)).as_dict())
@@ -239,7 +241,9 @@ async def ws_solar_forecast(
 )
 @websocket_api.async_response
 async def ws_get_fossil_energy_consumption(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
 ) -> None:
     """Calculate amount of fossil based energy."""
     start_time_str = msg["start_time"]
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 40989f41f19..c49af972e38 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -673,7 +673,7 @@ def websocket_get_themes(
 )
 @websocket_api.async_response
 async def websocket_get_translations(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle get translations command."""
     resources = await async_get_translations(
@@ -691,7 +691,7 @@ async def websocket_get_translations(
 @websocket_api.websocket_command({"type": "frontend/get_version"})
 @websocket_api.async_response
 async def websocket_get_version(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle get version command."""
     integration = await async_get_integration(hass, "frontend")
diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py
index bfda566de55..018850f5960 100644
--- a/homeassistant/components/frontend/storage.py
+++ b/homeassistant/components/frontend/storage.py
@@ -61,7 +61,7 @@ def with_store(orig_func: Callable) -> Callable:
 async def websocket_set_user_data(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     store: Store,
     data: dict[str, Any],
 ) -> None:
@@ -82,7 +82,7 @@ async def websocket_set_user_data(
 async def websocket_get_user_data(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     store: Store,
     data: dict[str, Any],
 ) -> None:
diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py
index 5c4b14570a9..d8dc5f28fdd 100644
--- a/homeassistant/components/hardware/websocket_api.py
+++ b/homeassistant/components/hardware/websocket_api.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 import contextlib
 from dataclasses import asdict, dataclass
 from datetime import datetime, timedelta
+from typing import Any
 
 import psutil_home_assistant as ha_psutil
 import voluptuous as vol
@@ -46,7 +47,7 @@ async def async_setup(hass: HomeAssistant) -> None:
 )
 @websocket_api.async_response
 async def ws_info(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Return hardware info."""
     hardware_info = []
@@ -72,7 +73,7 @@ async def ws_info(
 )
 @websocket_api.async_response
 async def ws_subscribe_system_status(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ):
     """Subscribe to system status updates."""
 
diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py
index eb0d6c54077..3670d5ca1fd 100644
--- a/homeassistant/components/hassio/websocket_api.py
+++ b/homeassistant/components/hassio/websocket_api.py
@@ -2,6 +2,7 @@
 import logging
 from numbers import Number
 import re
+from typing import Any
 
 import voluptuous as vol
 
@@ -60,7 +61,7 @@ def async_load_websocket_api(hass: HomeAssistant):
 @websocket_api.websocket_command({vol.Required(WS_TYPE): WS_TYPE_SUBSCRIBE})
 @websocket_api.async_response
 async def websocket_subscribe(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ):
     """Subscribe to supervisor events."""
 
@@ -83,7 +84,7 @@ async def websocket_subscribe(
 )
 @websocket_api.async_response
 async def websocket_supervisor_event(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ):
     """Publish events from the Supervisor."""
     connection.send_result(msg[WS_ID])
@@ -101,7 +102,7 @@ async def websocket_supervisor_event(
 )
 @websocket_api.async_response
 async def websocket_supervisor_api(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ):
     """Websocket handler to call Supervisor API."""
     if not connection.user.is_admin and not WS_NO_ADMIN_ENDPOINTS.match(
diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py
index bae6c95507e..165ef72d525 100644
--- a/homeassistant/components/history/__init__.py
+++ b/homeassistant/components/history/__init__.py
@@ -6,7 +6,7 @@ from datetime import datetime as dt, timedelta
 from http import HTTPStatus
 import logging
 import time
-from typing import cast
+from typing import Any, cast
 
 from aiohttp import web
 import voluptuous as vol
@@ -79,7 +79,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
 )
 @websocket_api.async_response
 async def ws_get_statistics_during_period(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle statistics websocket command."""
     _LOGGER.warning(
@@ -97,7 +97,7 @@ async def ws_get_statistics_during_period(
 )
 @websocket_api.async_response
 async def ws_get_list_statistic_ids(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Fetch a list of available statistic_id."""
     _LOGGER.warning(
@@ -164,7 +164,7 @@ def _ws_get_significant_states(
 )
 @websocket_api.async_response
 async def ws_get_history_during_period(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle history during period websocket command."""
     start_time_str = msg["start_time"]
diff --git a/homeassistant/components/insteon/api/aldb.py b/homeassistant/components/insteon/api/aldb.py
index a119707b03d..77ece2709d8 100644
--- a/homeassistant/components/insteon/api/aldb.py
+++ b/homeassistant/components/insteon/api/aldb.py
@@ -1,5 +1,7 @@
 """Web socket API for Insteon devices."""
 
+from typing import Any
+
 from pyinsteon import devices
 from pyinsteon.constants import ALDBStatus
 from pyinsteon.topics import (
@@ -71,7 +73,7 @@ async def async_reload_and_save_aldb(hass, device):
 async def websocket_get_aldb(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Get the All-Link Database for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -107,7 +109,7 @@ async def websocket_get_aldb(
 async def websocket_change_aldb_record(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Change an All-Link Database record for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -140,7 +142,7 @@ async def websocket_change_aldb_record(
 async def websocket_create_aldb_record(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Create an All-Link Database record for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -170,7 +172,7 @@ async def websocket_create_aldb_record(
 async def websocket_write_aldb(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Create an All-Link Database record for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -193,7 +195,7 @@ async def websocket_write_aldb(
 async def websocket_load_aldb(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Create an All-Link Database record for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -215,7 +217,7 @@ async def websocket_load_aldb(
 async def websocket_reset_aldb(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Create an All-Link Database record for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -237,7 +239,7 @@ async def websocket_reset_aldb(
 async def websocket_add_default_links(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Add the default All-Link Database records for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -261,7 +263,7 @@ async def websocket_add_default_links(
 async def websocket_notify_on_aldb_status(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Tell Insteon a new ALDB record was added."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py
index ea6bade4835..bffda965456 100644
--- a/homeassistant/components/insteon/api/device.py
+++ b/homeassistant/components/insteon/api/device.py
@@ -1,5 +1,7 @@
 """API interface to get an Insteon device."""
 
+from typing import Any
+
 from pyinsteon import devices
 from pyinsteon.constants import DeviceAction
 import voluptuous as vol
@@ -66,7 +68,7 @@ def notify_device_not_found(connection, msg, text):
 async def websocket_get_device(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Get an Insteon device."""
     dev_registry = dr.async_get(hass)
@@ -98,7 +100,7 @@ async def websocket_get_device(
 async def websocket_add_device(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Add one or more Insteon devices."""
 
@@ -134,7 +136,7 @@ async def websocket_add_device(
 async def websocket_cancel_add_device(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Cancel the Insteon all-linking process."""
     await devices.async_cancel_all_linking()
diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py
index 8e697a88459..7350ab14743 100644
--- a/homeassistant/components/insteon/api/properties.py
+++ b/homeassistant/components/insteon/api/properties.py
@@ -1,5 +1,7 @@
 """Property update methods and schemas."""
 
+from typing import Any
+
 from pyinsteon import devices
 from pyinsteon.config import RADIO_BUTTON_GROUPS, RAMP_RATE_IN_SEC, get_usable_value
 from pyinsteon.constants import (
@@ -158,7 +160,7 @@ def update_property(device, prop_name, value):
 async def websocket_get_properties(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Add the default All-Link Database records for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -183,7 +185,7 @@ async def websocket_get_properties(
 async def websocket_change_properties_record(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Add the default All-Link Database records for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -205,7 +207,7 @@ async def websocket_change_properties_record(
 async def websocket_write_properties(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Add the default All-Link Database records for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -235,7 +237,7 @@ async def websocket_write_properties(
 async def websocket_load_properties(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Add the default All-Link Database records for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
@@ -266,7 +268,7 @@ async def websocket_load_properties(
 async def websocket_reset_properties(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Add the default All-Link Database records for an Insteon device."""
     if not (device := devices[msg[DEVICE_ADDRESS]]):
diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py
index d28666963c8..8d0dd49bff8 100644
--- a/homeassistant/components/logbook/websocket_api.py
+++ b/homeassistant/components/logbook/websocket_api.py
@@ -260,7 +260,7 @@ async def _async_events_consumer(
 )
 @websocket_api.async_response
 async def ws_event_stream(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle logbook stream events websocket command."""
     start_time_str = msg["start_time"]
@@ -451,7 +451,7 @@ def _ws_formatted_get_events(
 )
 @websocket_api.async_response
 async def ws_get_events(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle logbook get events websocket command."""
     start_time_str = msg["start_time"]
diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py
index 47a5d7f6969..21c32c9137f 100644
--- a/homeassistant/components/media_source/__init__.py
+++ b/homeassistant/components/media_source/__init__.py
@@ -163,7 +163,7 @@ async def async_resolve_media(
 )
 @websocket_api.async_response
 async def websocket_browse_media(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Browse available media."""
     try:
@@ -185,7 +185,7 @@ async def websocket_browse_media(
 )
 @websocket_api.async_response
 async def websocket_resolve_media(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Resolve media."""
     try:
diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py
index 1ec3d6f9462..54f82365ffb 100644
--- a/homeassistant/components/media_source/local_source.py
+++ b/homeassistant/components/media_source/local_source.py
@@ -5,6 +5,7 @@ import logging
 import mimetypes
 from pathlib import Path
 import shutil
+from typing import Any
 
 from aiohttp import web
 from aiohttp.web_request import FileField
@@ -323,7 +324,7 @@ class UploadMediaView(http.HomeAssistantView):
 @websocket_api.require_admin
 @websocket_api.async_response
 async def websocket_remove_media(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Remove media."""
     try:
diff --git a/homeassistant/components/network/websocket.py b/homeassistant/components/network/websocket.py
index c19ed5e8fd7..4c55585e112 100644
--- a/homeassistant/components/network/websocket.py
+++ b/homeassistant/components/network/websocket.py
@@ -1,6 +1,8 @@
 """The Network Configuration integration websocket commands."""
 from __future__ import annotations
 
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
@@ -24,7 +26,7 @@ def async_register_websocket_commands(hass: HomeAssistant) -> None:
 async def websocket_network_adapters(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Return network preferences."""
     network = await async_get_network(hass)
@@ -48,7 +50,7 @@ async def websocket_network_adapters(
 async def websocket_network_adapters_configure(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Update network config."""
     network = await async_get_network(hass)
diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py
index c583577ec8f..d7fbce60767 100644
--- a/homeassistant/components/recorder/websocket_api.py
+++ b/homeassistant/components/recorder/websocket_api.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 
 from datetime import datetime as dt
 import logging
-from typing import Literal
+from typing import Any, Literal
 
 import voluptuous as vol
 
@@ -135,7 +135,7 @@ async def ws_handle_get_statistics_during_period(
 )
 @websocket_api.async_response
 async def ws_get_statistics_during_period(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle statistics websocket command."""
     await ws_handle_get_statistics_during_period(hass, connection, msg)
@@ -174,7 +174,7 @@ async def ws_handle_list_statistic_ids(
 )
 @websocket_api.async_response
 async def ws_list_statistic_ids(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Fetch a list of available statistic_id."""
     await ws_handle_list_statistic_ids(hass, connection, msg)
@@ -187,7 +187,7 @@ async def ws_list_statistic_ids(
 )
 @websocket_api.async_response
 async def ws_validate_statistics(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Fetch a list of available statistic_id."""
     instance = get_instance(hass)
@@ -226,7 +226,7 @@ def ws_clear_statistics(
 )
 @websocket_api.async_response
 async def ws_get_statistics_metadata(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Get metadata for a list of statistic_ids."""
     instance = get_instance(hass)
@@ -296,7 +296,7 @@ def ws_change_statistics_unit(
 )
 @websocket_api.async_response
 async def ws_adjust_sum_statistics(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Adjust sum statistics.
 
@@ -417,7 +417,7 @@ def ws_info(
 @websocket_api.websocket_command({vol.Required("type"): "backup/start"})
 @websocket_api.async_response
 async def ws_backup_start(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Backup start notification."""
 
@@ -435,7 +435,7 @@ async def ws_backup_start(
 @websocket_api.websocket_command({vol.Required("type"): "backup/end"})
 @websocket_api.async_response
 async def ws_backup_end(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Backup end notification."""
 
diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py
index 7344b729539..577786fca99 100644
--- a/homeassistant/components/shopping_list/__init__.py
+++ b/homeassistant/components/shopping_list/__init__.py
@@ -1,6 +1,7 @@
 """Support to manage a shopping list."""
 from http import HTTPStatus
 import logging
+from typing import Any
 import uuid
 
 import voluptuous as vol
@@ -372,7 +373,7 @@ def websocket_handle_items(
 async def websocket_handle_add(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Handle add item to shopping_list."""
     item = await hass.data[DOMAIN].async_add(msg["name"], connection.context(msg))
@@ -383,7 +384,7 @@ async def websocket_handle_add(
 async def websocket_handle_update(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Handle update shopping_list item."""
     msg_id = msg.pop("id")
@@ -406,7 +407,7 @@ async def websocket_handle_update(
 async def websocket_handle_clear(
     hass: HomeAssistant,
     connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Handle clearing shopping_list items."""
     await hass.data[DOMAIN].async_clear_completed(connection.context(msg))
diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py
index 55ffe111a73..7c0355fa24c 100644
--- a/homeassistant/components/usb/__init__.py
+++ b/homeassistant/components/usb/__init__.py
@@ -315,7 +315,7 @@ class USBDiscovery:
 async def websocket_usb_scan(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
 ) -> None:
     """Scan for new usb devices."""
     usb_discovery: USBDiscovery = hass.data[DOMAIN]
diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py
index 4a5b233a2f0..890df4add48 100644
--- a/homeassistant/components/zwave_js/api.py
+++ b/homeassistant/components/zwave_js/api.py
@@ -448,7 +448,7 @@ def async_register_api(hass: HomeAssistant) -> None:
 )
 @websocket_api.async_response
 async def websocket_network_status(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Get the status of the Z-Wave JS network."""
     if ENTRY_ID in msg:
@@ -518,7 +518,7 @@ async def websocket_network_status(
 async def websocket_subscribe_node_status(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Subscribe to node status update events of a Z-Wave JS node."""
@@ -559,7 +559,7 @@ async def websocket_subscribe_node_status(
 async def websocket_node_status(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Get the status of a Z-Wave JS node."""
@@ -577,7 +577,7 @@ async def websocket_node_status(
 async def websocket_node_metadata(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Get the metadata of a Z-Wave JS node."""
@@ -607,7 +607,7 @@ async def websocket_node_metadata(
 async def websocket_node_comments(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Get the comments of a Z-Wave JS node."""
@@ -648,7 +648,7 @@ async def websocket_node_comments(
 async def websocket_add_node(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -791,7 +791,7 @@ async def websocket_add_node(
 async def websocket_grant_security_classes(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -819,7 +819,7 @@ async def websocket_grant_security_classes(
 async def websocket_validate_dsk_and_enter_pin(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -849,7 +849,7 @@ async def websocket_validate_dsk_and_enter_pin(
 async def websocket_provision_smart_start_node(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -902,7 +902,7 @@ async def websocket_provision_smart_start_node(
 async def websocket_unprovision_smart_start_node(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -935,7 +935,7 @@ async def websocket_unprovision_smart_start_node(
 async def websocket_get_provisioning_entries(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -961,7 +961,7 @@ async def websocket_get_provisioning_entries(
 async def websocket_parse_qr_code_string(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -987,7 +987,7 @@ async def websocket_parse_qr_code_string(
 async def websocket_supports_feature(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1013,7 +1013,7 @@ async def websocket_supports_feature(
 async def websocket_stop_inclusion(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1040,7 +1040,7 @@ async def websocket_stop_inclusion(
 async def websocket_stop_exclusion(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1068,7 +1068,7 @@ async def websocket_stop_exclusion(
 async def websocket_remove_node(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1147,7 +1147,7 @@ async def websocket_remove_node(
 async def websocket_replace_failed_node(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Replace a failed node with a new node."""
@@ -1298,7 +1298,7 @@ async def websocket_replace_failed_node(
 async def websocket_remove_failed_node(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Remove a failed node from the Z-Wave network."""
@@ -1342,7 +1342,7 @@ async def websocket_remove_failed_node(
 async def websocket_begin_healing_network(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1369,7 +1369,7 @@ async def websocket_begin_healing_network(
 async def websocket_subscribe_heal_network_progress(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1413,7 +1413,7 @@ async def websocket_subscribe_heal_network_progress(
 async def websocket_stop_healing_network(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1440,7 +1440,7 @@ async def websocket_stop_healing_network(
 async def websocket_heal_node(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Heal a node on the Z-Wave network."""
@@ -1468,7 +1468,7 @@ async def websocket_heal_node(
 async def websocket_refresh_node_info(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Re-interview a node."""
@@ -1518,7 +1518,7 @@ async def websocket_refresh_node_info(
 async def websocket_refresh_node_values(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Refresh node values."""
@@ -1540,7 +1540,7 @@ async def websocket_refresh_node_values(
 async def websocket_refresh_node_cc_values(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Refresh node values for a particular CommandClass."""
@@ -1574,7 +1574,7 @@ async def websocket_refresh_node_cc_values(
 async def websocket_set_config_parameter(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Set a config parameter value for a Z-Wave node."""
@@ -1619,7 +1619,7 @@ async def websocket_set_config_parameter(
 @websocket_api.async_response
 @async_get_node
 async def websocket_get_config_parameters(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], node: Node
 ) -> None:
     """Get a list of configuration parameters for a Z-Wave node."""
     values = node.get_configuration_values()
@@ -1671,7 +1671,7 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict:
 async def websocket_subscribe_log_updates(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1758,7 +1758,7 @@ async def websocket_subscribe_log_updates(
 async def websocket_update_log_config(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1782,7 +1782,7 @@ async def websocket_update_log_config(
 async def websocket_get_log_config(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1809,7 +1809,7 @@ async def websocket_get_log_config(
 async def websocket_update_data_collection_preference(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1841,7 +1841,7 @@ async def websocket_update_data_collection_preference(
 async def websocket_data_collection_status(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -1868,7 +1868,7 @@ async def websocket_data_collection_status(
 async def websocket_abort_firmware_update(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Abort a firmware update."""
@@ -1889,7 +1889,7 @@ async def websocket_abort_firmware_update(
 async def websocket_is_node_firmware_update_in_progress(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Get whether firmware update is in progress for given node."""
@@ -1921,7 +1921,7 @@ def _get_firmware_update_progress_dict(
 async def websocket_subscribe_firmware_update_status(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Subscribe to the status of a firmware update."""
@@ -1994,7 +1994,7 @@ async def websocket_subscribe_firmware_update_status(
 async def websocket_get_firmware_update_capabilities(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Abort a firmware update."""
@@ -2015,7 +2015,7 @@ async def websocket_get_firmware_update_capabilities(
 async def websocket_is_any_ota_firmware_update_in_progress(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -2092,7 +2092,7 @@ class FirmwareUploadView(HomeAssistantView):
 async def websocket_check_for_config_updates(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -2121,7 +2121,7 @@ async def websocket_check_for_config_updates(
 async def websocket_install_config_update(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -2160,7 +2160,7 @@ def _get_controller_statistics_dict(
 async def websocket_subscribe_controller_statistics(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     entry: ConfigEntry,
     client: Client,
     driver: Driver,
@@ -2257,7 +2257,7 @@ def _get_node_statistics_dict(
 async def websocket_subscribe_node_statistics(
     hass: HomeAssistant,
     connection: ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     node: Node,
 ) -> None:
     """Subsribe to the statistics updates for a node."""
-- 
GitLab


From 599d61a4da096227ce4d5ba1dc0eaabceea56f49 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 18 Oct 2022 17:15:08 +0200
Subject: [PATCH 571/985] Fix payload in rest (#80544)

---
 homeassistant/components/rest/data.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py
index 86219634027..351c59a9f52 100644
--- a/homeassistant/components/rest/data.py
+++ b/homeassistant/components/rest/data.py
@@ -63,7 +63,7 @@ class RestData:
                 headers=rendered_headers,
                 params=rendered_params,
                 auth=self._auth,
-                data=self._request_data,
+                content=self._request_data,
                 timeout=self._timeout,
                 follow_redirects=True,
             )
-- 
GitLab


From 8a80ea2ed89ea1bc50191937bef598ff8b6d76fb Mon Sep 17 00:00:00 2001
From: niobos <niobos@dest-unreach.be>
Date: Tue, 18 Oct 2022 18:03:30 +0200
Subject: [PATCH 572/985] Bump velbus-aio to 2022.10.4 (#80510)

Co-authored-by: Niels Laukens <niels@dest-unreach.be>
---
 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 1acc65b898d..875ff9d497d 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.3"],
+  "requirements": ["velbus-aio==2022.10.4"],
   "config_flow": true,
   "codeowners": ["@Cereal2nd", "@brefra"],
   "dependencies": ["usb"],
diff --git a/requirements_all.txt b/requirements_all.txt
index 3205c01fe68..1e066c596de 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2481,7 +2481,7 @@ vallox-websocket-api==2.12.0
 vehicle==0.4.0
 
 # homeassistant.components.velbus
-velbus-aio==2022.10.3
+velbus-aio==2022.10.4
 
 # homeassistant.components.venstar
 venstarcolortouch==0.18
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index fd8b9bc2ba0..d5756c5db92 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1715,7 +1715,7 @@ vallox-websocket-api==2.12.0
 vehicle==0.4.0
 
 # homeassistant.components.velbus
-velbus-aio==2022.10.3
+velbus-aio==2022.10.4
 
 # homeassistant.components.venstar
 venstarcolortouch==0.18
-- 
GitLab


From 1cf801afbcb432178eb9c18017d5387f4c9349c0 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Tue, 18 Oct 2022 10:06:23 -0600
Subject: [PATCH 573/985] Remove unnecessary session scoping to several test
 fixtures (#80500)

Co-authored-by: Joakim Plate <elupus@ecce.se>
---
 tests/components/airvisual/conftest.py       |  2 +-
 tests/components/ambient_station/conftest.py |  4 ++--
 tests/components/guardian/conftest.py        | 16 ++++++++--------
 tests/components/iqvia/conftest.py           | 14 +++++++-------
 tests/components/notion/conftest.py          |  6 +++---
 tests/components/openuv/conftest.py          |  4 ++--
 tests/components/rainmachine/conftest.py     | 16 ++++++++--------
 tests/components/simplisafe/conftest.py      |  8 ++++----
 tests/components/tile/conftest.py            |  2 +-
 tests/components/watttime/conftest.py        |  4 ++--
 10 files changed, 38 insertions(+), 38 deletions(-)

diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py
index 81b22f19cc5..3e83b41a5af 100644
--- a/tests/components/airvisual/conftest.py
+++ b/tests/components/airvisual/conftest.py
@@ -50,7 +50,7 @@ def config_fixture(hass):
     }
 
 
-@pytest.fixture(name="data", scope="session")
+@pytest.fixture(name="data", scope="package")
 def data_fixture():
     """Define an update coordinator data example."""
     return json.loads(load_fixture("data.json", "airvisual"))
diff --git a/tests/components/ambient_station/conftest.py b/tests/components/ambient_station/conftest.py
index 680fa82303d..89dc4e88fb3 100644
--- a/tests/components/ambient_station/conftest.py
+++ b/tests/components/ambient_station/conftest.py
@@ -28,7 +28,7 @@ def config_entry_fixture(hass, config):
     return entry
 
 
-@pytest.fixture(name="devices", scope="session")
+@pytest.fixture(name="devices", scope="package")
 def devices_fixture():
     """Define devices data."""
     return json.loads(load_fixture("devices.json", "ambient_station"))
@@ -48,7 +48,7 @@ async def setup_ambient_station_fixture(hass, config, devices):
         yield
 
 
-@pytest.fixture(name="station_data", scope="session")
+@pytest.fixture(name="station_data", scope="package")
 def station_data_fixture():
     """Define devices data."""
     return json.loads(load_fixture("station_data.json", "ambient_station"))
diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py
index 492a486f76d..c7bffee4fff 100644
--- a/tests/components/guardian/conftest.py
+++ b/tests/components/guardian/conftest.py
@@ -32,31 +32,31 @@ def config_fixture(hass):
     }
 
 
-@pytest.fixture(name="data_sensor_pair_dump", scope="session")
+@pytest.fixture(name="data_sensor_pair_dump", scope="package")
 def data_sensor_pair_dump_fixture():
     """Define data from a successful sensor_pair_dump response."""
     return json.loads(load_fixture("sensor_pair_dump_data.json", "guardian"))
 
 
-@pytest.fixture(name="data_sensor_pair_sensor", scope="session")
+@pytest.fixture(name="data_sensor_pair_sensor", scope="package")
 def data_sensor_pair_sensor_fixture():
     """Define data from a successful sensor_pair_sensor response."""
     return json.loads(load_fixture("sensor_pair_sensor_data.json", "guardian"))
 
 
-@pytest.fixture(name="data_sensor_paired_sensor_status", scope="session")
+@pytest.fixture(name="data_sensor_paired_sensor_status", scope="package")
 def data_sensor_paired_sensor_status_fixture():
     """Define data from a successful sensor_paired_sensor_status response."""
     return json.loads(load_fixture("sensor_paired_sensor_status_data.json", "guardian"))
 
 
-@pytest.fixture(name="data_system_diagnostics", scope="session")
+@pytest.fixture(name="data_system_diagnostics", scope="package")
 def data_system_diagnostics_fixture():
     """Define data from a successful system_diagnostics response."""
     return json.loads(load_fixture("system_diagnostics_data.json", "guardian"))
 
 
-@pytest.fixture(name="data_system_onboard_sensor_status", scope="session")
+@pytest.fixture(name="data_system_onboard_sensor_status", scope="package")
 def data_system_onboard_sensor_status_fixture():
     """Define data from a successful system_onboard_sensor_status response."""
     return json.loads(
@@ -64,19 +64,19 @@ def data_system_onboard_sensor_status_fixture():
     )
 
 
-@pytest.fixture(name="data_system_ping", scope="session")
+@pytest.fixture(name="data_system_ping", scope="package")
 def data_system_ping_fixture():
     """Define data from a successful system_ping response."""
     return json.loads(load_fixture("system_ping_data.json", "guardian"))
 
 
-@pytest.fixture(name="data_valve_status", scope="session")
+@pytest.fixture(name="data_valve_status", scope="package")
 def data_valve_status_fixture():
     """Define data from a successful valve_status response."""
     return json.loads(load_fixture("valve_status_data.json", "guardian"))
 
 
-@pytest.fixture(name="data_wifi_status", scope="session")
+@pytest.fixture(name="data_wifi_status", scope="package")
 def data_wifi_status_fixture():
     """Define data from a successful wifi_status response."""
     return json.loads(load_fixture("wifi_status_data.json", "guardian"))
diff --git a/tests/components/iqvia/conftest.py b/tests/components/iqvia/conftest.py
index 65ad21cc813..b6ac1724885 100644
--- a/tests/components/iqvia/conftest.py
+++ b/tests/components/iqvia/conftest.py
@@ -26,43 +26,43 @@ def config_fixture(hass):
     }
 
 
-@pytest.fixture(name="data_allergy_forecast", scope="session")
+@pytest.fixture(name="data_allergy_forecast", scope="package")
 def data_allergy_forecast_fixture():
     """Define allergy forecast data."""
     return json.loads(load_fixture("allergy_forecast_data.json", "iqvia"))
 
 
-@pytest.fixture(name="data_allergy_index", scope="session")
+@pytest.fixture(name="data_allergy_index", scope="package")
 def data_allergy_index_fixture():
     """Define allergy index data."""
     return json.loads(load_fixture("allergy_index_data.json", "iqvia"))
 
 
-@pytest.fixture(name="data_allergy_outlook", scope="session")
+@pytest.fixture(name="data_allergy_outlook", scope="package")
 def data_allergy_outlook_fixture():
     """Define allergy outlook data."""
     return json.loads(load_fixture("allergy_outlook_data.json", "iqvia"))
 
 
-@pytest.fixture(name="data_asthma_forecast", scope="session")
+@pytest.fixture(name="data_asthma_forecast", scope="package")
 def data_asthma_forecast_fixture():
     """Define asthma forecast data."""
     return json.loads(load_fixture("asthma_forecast_data.json", "iqvia"))
 
 
-@pytest.fixture(name="data_asthma_index", scope="session")
+@pytest.fixture(name="data_asthma_index", scope="package")
 def data_asthma_index_fixture():
     """Define asthma index data."""
     return json.loads(load_fixture("asthma_index_data.json", "iqvia"))
 
 
-@pytest.fixture(name="data_disease_forecast", scope="session")
+@pytest.fixture(name="data_disease_forecast", scope="package")
 def data_disease_forecast_fixture():
     """Define disease forecast data."""
     return json.loads(load_fixture("disease_forecast_data.json", "iqvia"))
 
 
-@pytest.fixture(name="data_disease_index", scope="session")
+@pytest.fixture(name="data_disease_index", scope="package")
 def data_disease_index_fixture():
     """Define disease index data."""
     return json.loads(load_fixture("disease_index_data.json", "iqvia"))
diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py
index 603e9bc7823..7d87b9adc64 100644
--- a/tests/components/notion/conftest.py
+++ b/tests/components/notion/conftest.py
@@ -38,19 +38,19 @@ def config_fixture(hass):
     }
 
 
-@pytest.fixture(name="data_bridge", scope="session")
+@pytest.fixture(name="data_bridge", scope="package")
 def data_bridge_fixture():
     """Define bridge data."""
     return json.loads(load_fixture("bridge_data.json", "notion"))
 
 
-@pytest.fixture(name="data_sensor", scope="session")
+@pytest.fixture(name="data_sensor", scope="package")
 def data_sensor_fixture():
     """Define sensor data."""
     return json.loads(load_fixture("sensor_data.json", "notion"))
 
 
-@pytest.fixture(name="data_task", scope="session")
+@pytest.fixture(name="data_task", scope="package")
 def data_task_fixture():
     """Define task data."""
     return json.loads(load_fixture("task_data.json", "notion"))
diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py
index 8d9c1efb1b1..3caa41749ee 100644
--- a/tests/components/openuv/conftest.py
+++ b/tests/components/openuv/conftest.py
@@ -40,13 +40,13 @@ def config_fixture(hass):
     }
 
 
-@pytest.fixture(name="data_protection_window", scope="session")
+@pytest.fixture(name="data_protection_window", scope="package")
 def data_protection_window_fixture():
     """Define a fixture to return UV protection window data."""
     return json.loads(load_fixture("protection_window_data.json", "openuv"))
 
 
-@pytest.fixture(name="data_uv_index", scope="session")
+@pytest.fixture(name="data_uv_index", scope="package")
 def data_uv_index_fixture():
     """Define a fixture to return UV index data."""
     return json.loads(load_fixture("uv_index_data.json", "openuv"))
diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py
index af00a1013e0..685f307d197 100644
--- a/tests/components/rainmachine/conftest.py
+++ b/tests/components/rainmachine/conftest.py
@@ -76,19 +76,19 @@ def controller_mac_fixture():
     return "aa:bb:cc:dd:ee:ff"
 
 
-@pytest.fixture(name="data_api_versions", scope="session")
+@pytest.fixture(name="data_api_versions", scope="package")
 def data_api_versions_fixture():
     """Define API version data."""
     return json.loads(load_fixture("api_versions_data.json", "rainmachine"))
 
 
-@pytest.fixture(name="data_diagnostics_current", scope="session")
+@pytest.fixture(name="data_diagnostics_current", scope="package")
 def data_diagnostics_current_fixture():
     """Define current diagnostics data."""
     return json.loads(load_fixture("diagnostics_current_data.json", "rainmachine"))
 
 
-@pytest.fixture(name="data_machine_firmare_update_status", scope="session")
+@pytest.fixture(name="data_machine_firmare_update_status", scope="package")
 def data_machine_firmare_update_status_fixture():
     """Define machine firmware update status data."""
     return json.loads(
@@ -96,31 +96,31 @@ def data_machine_firmare_update_status_fixture():
     )
 
 
-@pytest.fixture(name="data_programs", scope="session")
+@pytest.fixture(name="data_programs", scope="package")
 def data_programs_fixture():
     """Define program data."""
     return json.loads(load_fixture("programs_data.json", "rainmachine"))
 
 
-@pytest.fixture(name="data_provision_settings", scope="session")
+@pytest.fixture(name="data_provision_settings", scope="package")
 def data_provision_settings_fixture():
     """Define provisioning settings data."""
     return json.loads(load_fixture("provision_settings_data.json", "rainmachine"))
 
 
-@pytest.fixture(name="data_restrictions_current", scope="session")
+@pytest.fixture(name="data_restrictions_current", scope="package")
 def data_restrictions_current_fixture():
     """Define current restrictions settings data."""
     return json.loads(load_fixture("restrictions_current_data.json", "rainmachine"))
 
 
-@pytest.fixture(name="data_restrictions_universal", scope="session")
+@pytest.fixture(name="data_restrictions_universal", scope="package")
 def data_restrictions_universal_fixture():
     """Define universal restrictions settings data."""
     return json.loads(load_fixture("restrictions_universal_data.json", "rainmachine"))
 
 
-@pytest.fixture(name="data_zones", scope="session")
+@pytest.fixture(name="data_zones", scope="package")
 def data_zones_fixture():
     """Define zone data."""
     return json.loads(load_fixture("zones_data.json", "rainmachine"))
diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py
index 54ab7fbe9d7..165f71cde04 100644
--- a/tests/components/simplisafe/conftest.py
+++ b/tests/components/simplisafe/conftest.py
@@ -58,25 +58,25 @@ def credentials_config_fixture():
     }
 
 
-@pytest.fixture(name="data_latest_event", scope="session")
+@pytest.fixture(name="data_latest_event", scope="package")
 def data_latest_event_fixture():
     """Define latest event data."""
     return json.loads(load_fixture("latest_event_data.json", "simplisafe"))
 
 
-@pytest.fixture(name="data_sensor", scope="session")
+@pytest.fixture(name="data_sensor", scope="package")
 def data_sensor_fixture():
     """Define sensor data."""
     return json.loads(load_fixture("sensor_data.json", "simplisafe"))
 
 
-@pytest.fixture(name="data_settings", scope="session")
+@pytest.fixture(name="data_settings", scope="package")
 def data_settings_fixture():
     """Define settings data."""
     return json.loads(load_fixture("settings_data.json", "simplisafe"))
 
 
-@pytest.fixture(name="data_subscription", scope="session")
+@pytest.fixture(name="data_subscription", scope="package")
 def data_subscription_fixture():
     """Define subscription data."""
     return json.loads(load_fixture("subscription_data.json", "simplisafe"))
diff --git a/tests/components/tile/conftest.py b/tests/components/tile/conftest.py
index aa90f1e44de..474c784ec3c 100644
--- a/tests/components/tile/conftest.py
+++ b/tests/components/tile/conftest.py
@@ -42,7 +42,7 @@ def config_fixture(hass):
     }
 
 
-@pytest.fixture(name="data_tile_details", scope="session")
+@pytest.fixture(name="data_tile_details", scope="package")
 def data_tile_details_fixture():
     """Define a Tile details data payload."""
     return json.loads(load_fixture("tile_details_data.json", "tile"))
diff --git a/tests/components/watttime/conftest.py b/tests/components/watttime/conftest.py
index c7a4f6cf32d..f3c1986fcb0 100644
--- a/tests/components/watttime/conftest.py
+++ b/tests/components/watttime/conftest.py
@@ -80,13 +80,13 @@ def config_entry_fixture(hass, config_auth, config_coordinates):
     return entry
 
 
-@pytest.fixture(name="data_grid_region", scope="session")
+@pytest.fixture(name="data_grid_region", scope="package")
 def data_grid_region_fixture():
     """Define grid region data."""
     return json.loads(load_fixture("grid_region_data.json", "watttime"))
 
 
-@pytest.fixture(name="data_realtime_emissions", scope="session")
+@pytest.fixture(name="data_realtime_emissions", scope="package")
 def data_realtime_emissions_fixture():
     """Define realtime emissions data."""
     return json.loads(load_fixture("realtime_emissions_data.json", "watttime"))
-- 
GitLab


From 1700da0ed3d73f97d73666b3bc9ac53a3fecb88a Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Tue, 18 Oct 2022 11:23:15 -0500
Subject: [PATCH 574/985] Reduce chance of bluetooth devices going unavailable
 when adapter stops responding (#80545)

---
 homeassistant/components/bluetooth/const.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py
index 9ec505bd6bf..8b0a8ae149c 100644
--- a/homeassistant/components/bluetooth/const.py
+++ b/homeassistant/components/bluetooth/const.py
@@ -51,9 +51,9 @@ FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 60 * 15
 # to be
 # 180s Time when device is removed from stack
 # - 30s check interval
-# - 20s scanner restart time * 2
+# - 30s scanner restart time * 2
 #
-SCANNER_WATCHDOG_TIMEOUT: Final = 110
+SCANNER_WATCHDOG_TIMEOUT: Final = 90
 # How often to check if the scanner has reached
 # the SCANNER_WATCHDOG_TIMEOUT without seeing anything
 SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30)
-- 
GitLab


From 414d478d3e462da211b33a0c81f846c509b222a3 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Tue, 18 Oct 2022 10:24:49 -0600
Subject: [PATCH 575/985] Ensure Enphase Envoy test fixtures aren't
 session-scoped (#80499)

* Ensure Enphase Envoy test fixtures aren't session-scoped

* Code review
---
 tests/components/enphase_envoy/conftest.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py
index cb01e8a81e4..93a76bdd510 100644
--- a/tests/components/enphase_envoy/conftest.py
+++ b/tests/components/enphase_envoy/conftest.py
@@ -36,13 +36,13 @@ def config_fixture():
     }
 
 
-@pytest.fixture(name="gateway_data", scope="session")
+@pytest.fixture(name="gateway_data", scope="package")
 def gateway_data_fixture():
     """Define a fixture to return gateway data."""
     return json.loads(load_fixture("data.json", "enphase_envoy"))
 
 
-@pytest.fixture(name="inverters_production_data", scope="session")
+@pytest.fixture(name="inverters_production_data", scope="package")
 def inverters_production_data_fixture():
     """Define a fixture to return inverter production data."""
     return json.loads(load_fixture("inverters_production.json", "enphase_envoy"))
-- 
GitLab


From 78e66b6cbe2ce34f8cc77c5ada50947957e615ee Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Tue, 18 Oct 2022 10:25:07 -0600
Subject: [PATCH 576/985] Add diagnostics to Enphase Envoy (#79950)

* Streamline Enphase Envoy config flow tests

* Don't test data results using constants

* Add diagnostics to Enphase Envoy

* Use whole config entry

* Redact serial number

* One call
---
 .../components/enphase_envoy/diagnostics.py   | 38 +++++++++++++
 .../enphase_envoy/test_diagnostics.py         | 56 +++++++++++++++++++
 2 files changed, 94 insertions(+)
 create mode 100644 homeassistant/components/enphase_envoy/diagnostics.py
 create mode 100644 tests/components/enphase_envoy/test_diagnostics.py

diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py
new file mode 100644
index 00000000000..daba57e9488
--- /dev/null
+++ b/homeassistant/components/enphase_envoy/diagnostics.py
@@ -0,0 +1,38 @@
+"""Diagnostics support for Enphase Envoy."""
+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_NAME, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import COORDINATOR, DOMAIN
+
+CONF_TITLE = "title"
+
+TO_REDACT = {
+    CONF_NAME,
+    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
+) -> dict[str, Any]:
+    """Return diagnostics for a config entry."""
+    coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
+
+    return async_redact_data(
+        {
+            "entry": entry.as_dict(),
+            "data": coordinator.data,
+        },
+        TO_REDACT,
+    )
diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py
new file mode 100644
index 00000000000..caa7d66fc95
--- /dev/null
+++ b/tests/components/enphase_envoy/test_diagnostics.py
@@ -0,0 +1,56 @@
+"""Test Enphase Envoy 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_enphase_envoy):
+    """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": "enphase_envoy",
+            "title": REDACTED,
+            "data": {
+                "host": "1.1.1.1",
+                "name": REDACTED,
+                "username": REDACTED,
+                "password": REDACTED,
+            },
+            "options": {},
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "source": "user",
+            "unique_id": REDACTED,
+            "disabled_by": None,
+        },
+        "data": {
+            "production": 1840,
+            "daily_production": 28223,
+            "seven_days_production": 174482,
+            "lifetime_production": 5924391,
+            "consumption": 1840,
+            "daily_consumption": 5923857,
+            "seven_days_consumption": 5923857,
+            "lifetime_consumption": 5923857,
+            "inverters_production": {
+                "202140024014": [136, "2022-10-08 16:43:36"],
+                "202140023294": [163, "2022-10-08 16:43:41"],
+                "202140013819": [130, "2022-10-08 16:43:31"],
+                "202140023794": [139, "2022-10-08 16:43:38"],
+                "202140023381": [130, "2022-10-08 16:43:47"],
+                "202140024176": [54, "2022-10-08 16:43:59"],
+                "202140003284": [132, "2022-10-08 16:43:55"],
+                "202140019854": [129, "2022-10-08 16:43:58"],
+                "202140020743": [131, "2022-10-08 16:43:49"],
+                "202140023531": [28, "2022-10-08 16:43:53"],
+                "202140024241": [164, "2022-10-08 16:43:33"],
+                "202140022963": [164, "2022-10-08 16:43:41"],
+                "202140023149": [118, "2022-10-08 16:43:47"],
+                "202140024828": [129, "2022-10-08 16:43:36"],
+                "202140023269": [133, "2022-10-08 16:43:43"],
+                "202140024157": [112, "2022-10-08 16:43:52"],
+            },
+        },
+    }
-- 
GitLab


From a708fc998437b2117e7b2f31b9050797ee50d1de Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Tue, 18 Oct 2022 11:48:52 -0500
Subject: [PATCH 577/985] Bump bleak-retry-connector to 2.3.1 (#80549)

---
 homeassistant/components/bluetooth/const.py      | 3 ---
 homeassistant/components/bluetooth/manager.py    | 4 +---
 homeassistant/components/bluetooth/manifest.json | 2 +-
 homeassistant/components/bluetooth/models.py     | 4 +---
 homeassistant/package_constraints.txt            | 2 +-
 requirements_all.txt                             | 2 +-
 requirements_test_all.txt                        | 2 +-
 7 files changed, 6 insertions(+), 13 deletions(-)

diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py
index 8b0a8ae149c..6d6751f6ac4 100644
--- a/homeassistant/components/bluetooth/const.py
+++ b/homeassistant/components/bluetooth/const.py
@@ -72,6 +72,3 @@ ADAPTER_ADDRESS: Final = "address"
 ADAPTER_SW_VERSION: Final = "sw_version"
 ADAPTER_HW_VERSION: Final = "hw_version"
 ADAPTER_PASSIVE_SCAN: Final = "passive_scan"
-
-
-NO_RSSI_VALUE: Final = -127
diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index abdf73f0659..b38f6005b50 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -11,6 +11,7 @@ import time
 from typing import TYPE_CHECKING, Any, Final
 
 from bleak.backends.scanner import AdvertisementDataCallback
+from bleak_retry_connector import NO_RSSI_VALUE, RSSI_SWITCH_THRESHOLD
 
 from homeassistant import config_entries
 from homeassistant.core import (
@@ -27,7 +28,6 @@ from .const import (
     ADAPTER_ADDRESS,
     ADAPTER_PASSIVE_SCAN,
     FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
-    NO_RSSI_VALUE,
     UNAVAILABLE_TRACK_SECONDS,
     AdapterDetails,
 )
@@ -67,8 +67,6 @@ APPLE_START_BYTES_WANTED: Final = {
     APPLE_DEVICE_ID_START_BYTE,
 }
 
-RSSI_SWITCH_THRESHOLD = 6
-
 MONOTONIC_TIME: Final = time.monotonic
 
 _LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json
index ef6157706bc..c11256a3657 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -7,7 +7,7 @@
   "quality_scale": "internal",
   "requirements": [
     "bleak==0.19.0",
-    "bleak-retry-connector==2.3.0",
+    "bleak-retry-connector==2.3.1",
     "bluetooth-adapters==0.6.0",
     "bluetooth-auto-recovery==0.3.4",
     "dbus-fast==1.45.0"
diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py
index 1256cd7697a..99024dd3450 100644
--- a/homeassistant/components/bluetooth/models.py
+++ b/homeassistant/components/bluetooth/models.py
@@ -18,14 +18,12 @@ from bleak.backends.scanner import (
     AdvertisementDataCallback,
     BaseBleakScanner,
 )
-from bleak_retry_connector import freshen_ble_device
+from bleak_retry_connector import NO_RSSI_VALUE, freshen_ble_device
 
 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
 from homeassistant.helpers.frame import report
 from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
 
-from .const import NO_RSSI_VALUE
-
 if TYPE_CHECKING:
 
     from .manager import BluetoothManager
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index ee70296bd4a..a88d6918303 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.3.0
+bleak-retry-connector==2.3.1
 bleak==0.19.0
 bluetooth-adapters==0.6.0
 bluetooth-auto-recovery==0.3.4
diff --git a/requirements_all.txt b/requirements_all.txt
index 1e066c596de..d263dbd73a9 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.3.0
+bleak-retry-connector==2.3.1
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index d5756c5db92..f0dd9e54cf9 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.3.0
+bleak-retry-connector==2.3.1
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
-- 
GitLab


From 98ca28ab1c095e9a789d5e5a71cb69e73b675c74 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Tue, 18 Oct 2022 20:19:04 +0300
Subject: [PATCH 578/985] Bump aioswitcher to 3.1.0 (#80534)

---
 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 d498c5f165e..14f324d8cac 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.3"],
+  "requirements": ["aioswitcher==3.1.0"],
   "quality_scale": "platinum",
   "iot_class": "local_push",
   "config_flow": true,
diff --git a/requirements_all.txt b/requirements_all.txt
index d263dbd73a9..7e967058b4a 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.3
+aioswitcher==3.1.0
 
 # homeassistant.components.syncthing
 aiosyncthing==0.5.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f0dd9e54cf9..4fd3499e36b 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.3
+aioswitcher==3.1.0
 
 # homeassistant.components.syncthing
 aiosyncthing==0.5.1
-- 
GitLab


From 8e9457d80871b4ce6253a9694bad9893d7010719 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Tue, 18 Oct 2022 22:42:22 +0300
Subject: [PATCH 579/985] Add Shelly support for sleeping Gen2 devices (#79889)

---
 homeassistant/components/shelly/__init__.py   | 122 +++++++++++++---
 .../components/shelly/binary_sensor.py        |  31 +++-
 .../components/shelly/config_flow.py          |   6 +-
 .../components/shelly/coordinator.py          |  13 +-
 homeassistant/components/shelly/entity.py     | 137 ++++++++++++++++--
 homeassistant/components/shelly/manifest.json |   3 +-
 homeassistant/components/shelly/sensor.py     |  31 +++-
 homeassistant/components/shelly/utils.py      |  36 ++++-
 requirements_all.txt                          |   2 +-
 requirements_test_all.txt                     |   2 +-
 tests/components/shelly/conftest.py           |   8 +
 11 files changed, 337 insertions(+), 54 deletions(-)

diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index bc7a3f771f7..9759fd148d0 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -39,7 +39,13 @@ from .coordinator import (
     ShellyRpcPollingCoordinator,
     get_entry_data,
 )
-from .utils import get_block_device_sleep_period, get_coap_context, get_device_entry_gen
+from .utils import (
+    get_block_device_sleep_period,
+    get_coap_context,
+    get_device_entry_gen,
+    get_rpc_device_sleep_period,
+    get_ws_context,
+)
 
 BLOCK_PLATFORMS: Final = [
     Platform.BINARY_SENSOR,
@@ -65,7 +71,10 @@ RPC_PLATFORMS: Final = [
     Platform.SWITCH,
     Platform.UPDATE,
 ]
-
+RPC_SLEEPING_PLATFORMS: Final = [
+    Platform.BINARY_SENSOR,
+    Platform.SENSOR,
+]
 
 COAP_SCHEMA: Final = vol.Schema(
     {
@@ -215,26 +224,87 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo
         entry.data.get(CONF_PASSWORD),
     )
 
-    LOGGER.debug("Setting up online RPC device %s", entry.title)
-    try:
-        async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
-            device = await RpcDevice.create(
-                aiohttp_client.async_get_clientsession(hass), options
-            )
-    except asyncio.TimeoutError as err:
-        raise ConfigEntryNotReady(str(err) or "Timeout during device setup") from err
-    except OSError as err:
-        raise ConfigEntryNotReady(str(err) or "Error during device setup") from err
-    except (AuthRequired, InvalidAuthError) as err:
-        raise ConfigEntryAuthFailed from err
+    ws_context = await get_ws_context(hass)
+
+    device = await RpcDevice.create(
+        aiohttp_client.async_get_clientsession(hass),
+        ws_context,
+        options,
+        False,
+    )
+
+    dev_reg = device_registry.async_get(hass)
+    device_entry = None
+    if entry.unique_id is not None:
+        device_entry = dev_reg.async_get_device(
+            identifiers=set(),
+            connections={
+                (
+                    device_registry.CONNECTION_NETWORK_MAC,
+                    device_registry.format_mac(entry.unique_id),
+                )
+            },
+        )
+    if device_entry and entry.entry_id not in device_entry.config_entries:
+        device_entry = None
 
+    sleep_period = entry.data.get(CONF_SLEEP_PERIOD)
     shelly_entry_data = get_entry_data(hass)[entry.entry_id]
-    shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device)
-    shelly_entry_data.rpc.async_setup()
 
-    shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device)
+    @callback
+    def _async_rpc_device_setup() -> None:
+        """Set up a RPC based device that is online."""
+        shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device)
+        shelly_entry_data.rpc.async_setup()
+
+        platforms = RPC_SLEEPING_PLATFORMS
+
+        if not entry.data.get(CONF_SLEEP_PERIOD):
+            shelly_entry_data.rpc_poll = ShellyRpcPollingCoordinator(
+                hass, entry, device
+            )
+            platforms = RPC_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)
+        shelly_entry_data.device = None
+
+        if sleep_period is None:
+            data = {**entry.data}
+            data[CONF_SLEEP_PERIOD] = get_rpc_device_sleep_period(device.config)
+            hass.config_entries.async_update_entry(entry, data=data)
+
+        _async_rpc_device_setup()
 
-    hass.config_entries.async_setup_platforms(entry, RPC_PLATFORMS)
+    if sleep_period == 0:
+        # Not a sleeping device, finish setup
+        LOGGER.debug("Setting up online RPC device %s", entry.title)
+        try:
+            async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
+                await device.initialize()
+        except asyncio.TimeoutError as err:
+            raise ConfigEntryNotReady(
+                str(err) or "Timeout during device setup"
+            ) from err
+        except OSError as err:
+            raise ConfigEntryNotReady(str(err) or "Error during device setup") from err
+        except (AuthRequired, InvalidAuthError) as err:
+            raise ConfigEntryAuthFailed from err
+        _async_rpc_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
+        shelly_entry_data.device = device
+        LOGGER.debug(
+            "Setup for device %s will resume when device is online", entry.title
+        )
+        device.subscribe_updates(_async_device_online)
+    else:
+        # Restore sensors for sleeping device
+        LOGGER.debug("Setting up offline block device %s", entry.title)
+        _async_rpc_device_setup()
 
     return True
 
@@ -243,9 +313,18 @@ 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 shelly_entry_data.device is not None:
+        # If device is present, block/rpc coordinator is not setup yet
+        shelly_entry_data.device.shutdown()
+        return True
+
+    platforms = RPC_SLEEPING_PLATFORMS
+    if not entry.data.get(CONF_SLEEP_PERIOD):
+        platforms = RPC_PLATFORMS
+
     if get_device_entry_gen(entry) == 2:
         if unload_ok := await hass.config_entries.async_unload_platforms(
-            entry, RPC_PLATFORMS
+            entry, platforms
         ):
             if shelly_entry_data.rpc:
                 await shelly_entry_data.rpc.shutdown()
@@ -253,11 +332,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
         return unload_ok
 
-    if shelly_entry_data.device is not None:
-        # If device is present, block coordinator is not setup yet
-        shelly_entry_data.device.shutdown()
-        return True
-
     platforms = BLOCK_SLEEPING_PLATFORMS
 
     if not entry.data.get(CONF_SLEEP_PERIOD):
diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py
index cc6c4494ebb..cfacdf85cfd 100644
--- a/homeassistant/components/shelly/binary_sensor.py
+++ b/homeassistant/components/shelly/binary_sensor.py
@@ -25,6 +25,7 @@ from .entity import (
     ShellyRestAttributeEntity,
     ShellyRpcAttributeEntity,
     ShellySleepingBlockAttributeEntity,
+    ShellySleepingRpcAttributeEntity,
     async_setup_entry_attribute_entities,
     async_setup_entry_rest,
     async_setup_entry_rpc,
@@ -209,9 +210,19 @@ async def async_setup_entry(
 ) -> None:
     """Set up sensors for device."""
     if get_device_entry_gen(config_entry) == 2:
-        return async_setup_entry_rpc(
-            hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor
-        )
+        if config_entry.data[CONF_SLEEP_PERIOD]:
+            async_setup_entry_rpc(
+                hass,
+                config_entry,
+                async_add_entities,
+                RPC_SENSORS,
+                RpcSleepingBinarySensor,
+            )
+        else:
+            async_setup_entry_rpc(
+                hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor
+            )
+        return
 
     if config_entry.data[CONF_SLEEP_PERIOD]:
         async_setup_entry_attribute_entities(
@@ -289,3 +300,17 @@ class BlockSleepingBinarySensor(ShellySleepingBlockAttributeEntity, BinarySensor
             return bool(self.attribute_value)
 
         return self.last_state == STATE_ON
+
+
+class RpcSleepingBinarySensor(ShellySleepingRpcAttributeEntity, BinarySensorEntity):
+    """Represent a RPC sleeping binary sensor entity."""
+
+    entity_description: RpcBinarySensorDescription
+
+    @property
+    def is_on(self) -> bool | None:
+        """Return true if RPC sensor state is on."""
+        if self.coordinator.device.initialized:
+            return bool(self.attribute_value)
+
+        return self.last_state == STATE_ON
diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py
index c2c80b48fc0..baa9c218d5f 100644
--- a/homeassistant/components/shelly/config_flow.py
+++ b/homeassistant/components/shelly/config_flow.py
@@ -29,6 +29,8 @@ from .utils import (
     get_info_gen,
     get_model_name,
     get_rpc_device_name,
+    get_rpc_device_sleep_period,
+    get_ws_context,
 )
 
 HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str})
@@ -54,8 +56,10 @@ async def validate_input(
 
     async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
         if get_info_gen(info) == 2:
+            ws_context = await get_ws_context(hass)
             rpc_device = await RpcDevice.create(
                 aiohttp_client.async_get_clientsession(hass),
+                ws_context,
                 options,
             )
             await rpc_device.shutdown()
@@ -63,7 +67,7 @@ async def validate_input(
 
             return {
                 "title": get_rpc_device_name(rpc_device),
-                CONF_SLEEP_PERIOD: 0,
+                CONF_SLEEP_PERIOD: get_rpc_device_sleep_period(rpc_device.config),
                 "model": rpc_device.shelly.get("model"),
                 "gen": 2,
             }
diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py
index 6259571d078..ec115cfc69f 100644
--- a/homeassistant/components/shelly/coordinator.py
+++ b/homeassistant/components/shelly/coordinator.py
@@ -53,7 +53,7 @@ class ShellyEntryData:
     """Class for sharing data within a given config entry."""
 
     block: ShellyBlockCoordinator | None = None
-    device: BlockDevice | None = None
+    device: BlockDevice | RpcDevice | None = None
     rest: ShellyRestCoordinator | None = None
     rpc: ShellyRpcCoordinator | None = None
     rpc_poll: ShellyRpcPollingCoordinator | None = None
@@ -353,12 +353,16 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
         """Initialize the Shelly RPC device coordinator."""
         self.device_id: str | None = None
 
+        if sleep_period := entry.data[CONF_SLEEP_PERIOD]:
+            update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period
+        else:
+            update_interval = RPC_RECONNECT_INTERVAL
         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),
+            update_interval=timedelta(seconds=update_interval),
         )
         self.entry = entry
         self.device = device
@@ -424,6 +428,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
 
     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 UpdateFailed(
+                f"Sleeping device did not update within {sleep_period} seconds interval"
+            )
         if self.device.connected:
             return
 
diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py
index c27f0210e6a..27f7b5dc689 100644
--- a/homeassistant/components/shelly/entity.py
+++ b/homeassistant/components/shelly/entity.py
@@ -14,11 +14,12 @@ from homeassistant.core import HomeAssistant, callback
 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.entity_registry import RegistryEntry
 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, LOGGER
+from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, LOGGER
 from .coordinator import (
     ShellyBlockCoordinator,
     ShellyRpcCoordinator,
@@ -40,9 +41,7 @@ def async_setup_entry_attribute_entities(
     async_add_entities: AddEntitiesCallback,
     sensors: Mapping[tuple[str, str], BlockEntityDescription],
     sensor_class: Callable,
-    description_class: Callable[
-        [entity_registry.RegistryEntry], BlockEntityDescription
-    ],
+    description_class: Callable[[RegistryEntry], BlockEntityDescription],
 ) -> None:
     """Set up entities for attributes."""
     coordinator = get_entry_data(hass)[config_entry.entry_id].block
@@ -115,9 +114,7 @@ def async_restore_block_attribute_entities(
     coordinator: ShellyBlockCoordinator,
     sensors: Mapping[tuple[str, str], BlockEntityDescription],
     sensor_class: Callable,
-    description_class: Callable[
-        [entity_registry.RegistryEntry], BlockEntityDescription
-    ],
+    description_class: Callable[[RegistryEntry], BlockEntityDescription],
 ) -> None:
     """Restore block attributes entities."""
     entities = []
@@ -154,11 +151,35 @@ def async_setup_entry_rpc(
     sensors: Mapping[str, RpcEntityDescription],
     sensor_class: Callable,
 ) -> None:
-    """Set up entities for REST sensors."""
+    """Set up entities for RPC sensors."""
     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
+
+    if coordinator.device.initialized:
+        async_setup_rpc_attribute_entities(
+            hass, config_entry, async_add_entities, sensors, sensor_class
+        )
+    else:
+        async_restore_rpc_attribute_entities(
+            hass, config_entry, async_add_entities, coordinator, sensors, sensor_class
+        )
+
+
+@callback
+def async_setup_rpc_attribute_entities(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+    sensors: Mapping[str, RpcEntityDescription],
+    sensor_class: Callable,
+) -> None:
+    """Set up entities for RPC attributes."""
+    coordinator = get_entry_data(hass)[config_entry.entry_id].rpc
+    assert coordinator
+
+    if not (sleep_period := config_entry.data[CONF_SLEEP_PERIOD]):
+        polling_coordinator = get_entry_data(hass)[config_entry.entry_id].rpc_poll
+        assert polling_coordinator
 
     entities = []
     for sensor_id in sensors:
@@ -183,13 +204,52 @@ def async_setup_entry_rpc(
                 async_remove_shelly_entity(hass, domain, unique_id)
             else:
                 if description.use_polling_coordinator:
-                    entities.append(
-                        sensor_class(polling_coordinator, key, sensor_id, description)
-                    )
+                    if not sleep_period:
+                        entities.append(
+                            sensor_class(
+                                polling_coordinator, key, sensor_id, description
+                            )
+                        )
                 else:
                     entities.append(
                         sensor_class(coordinator, key, sensor_id, description)
                     )
+    if not entities:
+        return
+
+    async_add_entities(entities)
+
+
+@callback
+def async_restore_rpc_attribute_entities(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+    coordinator: ShellyRpcCoordinator,
+    sensors: Mapping[str, RpcEntityDescription],
+    sensor_class: Callable,
+) -> None:
+    """Restore block attributes entities."""
+    entities = []
+
+    ent_reg = entity_registry.async_get(hass)
+    entries = entity_registry.async_entries_for_config_entry(
+        ent_reg, config_entry.entry_id
+    )
+
+    domain = sensor_class.__module__.split(".")[-1]
+
+    for entry in entries:
+        if entry.domain != domain:
+            continue
+
+        key = entry.unique_id.split("-")[-2]
+        attribute = entry.unique_id.split("-")[-1]
+
+        if description := sensors.get(attribute):
+            entities.append(
+                sensor_class(coordinator, key, attribute, description, entry)
+            )
 
     if not entities:
         return
@@ -336,7 +396,7 @@ class ShellyRpcEntity(entity.Entity):
     @property
     def available(self) -> bool:
         """Available."""
-        return self.coordinator.device.connected
+        return self.coordinator.last_update_success
 
     @property
     def status(self) -> dict:
@@ -552,7 +612,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
         block: Block | None,
         attribute: str,
         description: BlockEntityDescription,
-        entry: entity_registry.RegistryEntry | None = None,
+        entry: RegistryEntry | None = None,
         sensors: Mapping[tuple[str, str], BlockEntityDescription] | None = None,
     ) -> None:
         """Initialize the sleeping sensor."""
@@ -621,3 +681,50 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
                 LOGGER.debug("Entity %s attached to block", self.name)
                 super()._update_callback()
                 return
+
+
+class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
+    """Helper class to represent a sleeping rpc attribute."""
+
+    entity_description: RpcEntityDescription
+
+    # pylint: disable=super-init-not-called
+    def __init__(
+        self,
+        coordinator: ShellyRpcCoordinator,
+        key: str,
+        attribute: str,
+        description: RpcEntityDescription,
+        entry: RegistryEntry | None = None,
+    ) -> None:
+        """Initialize the sleeping sensor."""
+        self.last_state: StateType = None
+        self.coordinator = coordinator
+        self.key = key
+        self.attribute = attribute
+        self.entity_description = description
+
+        self._attr_should_poll = False
+        self._attr_device_info = DeviceInfo(
+            connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}
+        )
+        self._attr_unique_id = (
+            self._attr_unique_id
+        ) = f"{coordinator.mac}-{key}-{attribute}"
+        self._last_value = None
+
+        if coordinator.device.initialized:
+            self._attr_name = get_rpc_entity_name(
+                coordinator.device, key, description.name
+            )
+        elif entry is not None:
+            self._attr_name = cast(str, entry.original_name)
+
+    async def async_added_to_hass(self) -> None:
+        """Handle entity which will be added."""
+        await super().async_added_to_hass()
+
+        last_state = await self.async_get_last_state()
+
+        if last_state is not None:
+            self.last_state = last_state.state
diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json
index d9a0e21764a..6996a42e022 100644
--- a/homeassistant/components/shelly/manifest.json
+++ b/homeassistant/components/shelly/manifest.json
@@ -3,7 +3,8 @@
   "name": "Shelly",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/shelly",
-  "requirements": ["aioshelly==2.0.2"],
+  "requirements": ["aioshelly==3.0.0"],
+  "dependencies": ["http"],
   "zeroconf": [
     {
       "type": "_http._tcp.local.",
diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py
index b4516b098a2..3ddabf7ca2b 100644
--- a/homeassistant/components/shelly/sensor.py
+++ b/homeassistant/components/shelly/sensor.py
@@ -42,6 +42,7 @@ from .entity import (
     ShellyRestAttributeEntity,
     ShellyRpcAttributeEntity,
     ShellySleepingBlockAttributeEntity,
+    ShellySleepingRpcAttributeEntity,
     async_setup_entry_attribute_entities,
     async_setup_entry_rest,
     async_setup_entry_rpc,
@@ -451,9 +452,19 @@ async def async_setup_entry(
 ) -> None:
     """Set up sensors for device."""
     if get_device_entry_gen(config_entry) == 2:
-        return async_setup_entry_rpc(
-            hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor
-        )
+        if config_entry.data[CONF_SLEEP_PERIOD]:
+            async_setup_entry_rpc(
+                hass,
+                config_entry,
+                async_add_entities,
+                RPC_SENSORS,
+                RpcSleepingSensor,
+            )
+        else:
+            async_setup_entry_rpc(
+                hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor
+            )
+        return
 
     if config_entry.data[CONF_SLEEP_PERIOD]:
         async_setup_entry_attribute_entities(
@@ -553,3 +564,17 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
             return self.attribute_value
 
         return self.last_state
+
+
+class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
+    """Represent a RPC sleeping sensor."""
+
+    entity_description: RpcSensorDescription
+
+    @property
+    def native_value(self) -> StateType:
+        """Return value of sensor."""
+        if self.coordinator.device.initialized:
+            return self.attribute_value
+
+        return self.last_state
diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py
index 985935b3939..79f5a5848f0 100644
--- a/homeassistant/components/shelly/utils.py
+++ b/homeassistant/components/shelly/utils.py
@@ -4,10 +4,12 @@ from __future__ import annotations
 from datetime import datetime, timedelta
 from typing import Any, cast
 
+from aiohttp.web import Request, WebSocketResponse
 from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice
 from aioshelly.const import MODEL_NAMES
-from aioshelly.rpc_device import RpcDevice
+from aioshelly.rpc_device import RpcDevice, WsServer
 
+from homeassistant.components.http import HomeAssistantView
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
 from homeassistant.core import HomeAssistant, callback
@@ -212,7 +214,7 @@ def get_shbtn_input_triggers() -> list[tuple[str, str]]:
 
 @singleton.singleton("shelly_coap")
 async def get_coap_context(hass: HomeAssistant) -> COAP:
-    """Get CoAP context to be used in all Shelly devices."""
+    """Get CoAP context to be used in all Shelly Gen1 devices."""
     context = COAP()
     if DOMAIN in hass.data:
         port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT)
@@ -226,10 +228,33 @@ async def get_coap_context(hass: HomeAssistant) -> COAP:
         context.close()
 
     hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
-
     return context
 
 
+class ShellyReceiver(HomeAssistantView):
+    """Handle pushes from Shelly Gen2 devices."""
+
+    requires_auth = False
+    url = "/api/shelly/ws"
+    name = "api:shelly:ws"
+
+    def __init__(self, ws_server: WsServer) -> None:
+        """Initialize the Shelly receiver view."""
+        self._ws_server = ws_server
+
+    async def get(self, request: Request) -> WebSocketResponse:
+        """Start a get request."""
+        return await self._ws_server.websocket_handler(request)
+
+
+@singleton.singleton("shelly_ws_server")
+async def get_ws_context(hass: HomeAssistant) -> WsServer:
+    """Get websocket server context to be used in all Shelly Gen2 devices."""
+    ws_server = WsServer()
+    hass.http.register_view(ShellyReceiver(ws_server))
+    return ws_server
+
+
 def get_block_device_sleep_period(settings: dict[str, Any]) -> int:
     """Return the device sleep period in seconds or 0 for non sleeping devices."""
     sleep_period = 0
@@ -242,6 +267,11 @@ def get_block_device_sleep_period(settings: dict[str, Any]) -> int:
     return sleep_period * 60  # minutes to seconds
 
 
+def get_rpc_device_sleep_period(config: dict[str, Any]) -> int:
+    """Return the device sleep period in seconds or 0 for non sleeping devices."""
+    return cast(int, config["sys"].get("sleep", {}).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"))
diff --git a/requirements_all.txt b/requirements_all.txt
index 7e967058b4a..4aa218c610b 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==2.0.2
+aioshelly==3.0.0
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 4fd3499e36b..853e257c0df 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==2.0.2
+aioshelly==3.0.0
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py
index cca4aebb9ea..33bad4b1fc0 100644
--- a/tests/components/shelly/conftest.py
+++ b/tests/components/shelly/conftest.py
@@ -146,6 +146,13 @@ def mock_coap():
         yield
 
 
+@pytest.fixture(autouse=True)
+def mock_ws_server():
+    """Mock out ws_server."""
+    with patch("homeassistant.components.shelly.utils.get_ws_context"):
+        yield
+
+
 @pytest.fixture
 def device_reg(hass):
     """Return an empty, loaded, registry."""
@@ -211,6 +218,7 @@ async def mock_rpc_device():
             update=AsyncMock(),
             trigger_ota_update=AsyncMock(),
             trigger_reboot=AsyncMock(),
+            initialize=AsyncMock(),
             initialized=True,
             shutdown=AsyncMock(),
         )
-- 
GitLab


From 1b130eb271e4f5fe70396d1758f3b73f4149969f Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
Date: Tue, 18 Oct 2022 21:55:50 +0200
Subject: [PATCH 580/985] Save last target temperature in Shelly climate
 platform (#80561)

Save last target temp
---
 homeassistant/components/shelly/climate.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py
index 7bf43ce33cb..a71cdd0b46d 100644
--- a/homeassistant/components/shelly/climate.py
+++ b/homeassistant/components/shelly/climate.py
@@ -132,6 +132,7 @@ class BlockSleepingClimate(
         self.last_state: State | None = None
         self.last_state_attributes: Mapping[str, Any]
         self._preset_modes: list[str] = []
+        self._last_target_temp = 20.0
 
         if self.block is not None and self.device_block is not None:
             self._unique_id = f"{self.coordinator.mac}-{self.block.description}"
@@ -260,9 +261,15 @@ class BlockSleepingClimate(
     async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
         """Set hvac mode."""
         if hvac_mode == HVACMode.OFF:
+            if isinstance(self.target_temperature, float):
+                self._last_target_temp = self.target_temperature
             await self.set_state_full_path(
                 target_t_enabled=1, target_t=f"{self._attr_min_temp}"
             )
+        if hvac_mode == HVACMode.HEAT:
+            await self.set_state_full_path(
+                target_t_enabled=1, target_t=self._last_target_temp
+            )
 
     async def async_set_preset_mode(self, preset_mode: str) -> None:
         """Set preset mode."""
-- 
GitLab


From 551b3374f13c2600c5f2fc9ada0a8ff4e844a2b1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Tue, 18 Oct 2022 14:57:32 -0500
Subject: [PATCH 581/985] Bump aiohomekit to 2.1.1 (#80560)

fixes #80455

changelog: https://github.com/Jc2k/aiohomekit/compare/2.1.0...2.1.1
---
 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 07db0b4a256..8e82c59568b 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.1.0"],
+  "requirements": ["aiohomekit==2.1.1"],
   "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 4aa218c610b..68cbd7e076d 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.1.0
+aiohomekit==2.1.1
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 853e257c0df..520c7fde01f 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.1.0
+aiohomekit==2.1.1
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From a717ea8afc2c7f3ff39e05c85919c68376a777cb Mon Sep 17 00:00:00 2001
From: Patrick ZAJDA <patrick@zajda.fr>
Date: Tue, 18 Oct 2022 22:09:23 +0200
Subject: [PATCH 582/985] Migrate Broadlink to new entity naming style (#80187)

* Migrate Broadlink to new entity naming style

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

* Add some tests

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>

Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>
---
 homeassistant/components/broadlink/light.py  |   3 +-
 homeassistant/components/broadlink/remote.py |   3 +-
 homeassistant/components/broadlink/sensor.py |   5 +-
 homeassistant/components/broadlink/switch.py |  10 +-
 tests/components/broadlink/__init__.py       |  10 ++
 tests/components/broadlink/test_device.py    |  13 +-
 tests/components/broadlink/test_remote.py    |   7 +-
 tests/components/broadlink/test_sensors.py   |  51 ++++++--
 tests/components/broadlink/test_switch.py    | 126 +++++++++++++++++++
 9 files changed, 205 insertions(+), 23 deletions(-)
 create mode 100644 tests/components/broadlink/test_switch.py

diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py
index c4dd48b7dc0..d42e2b76b99 100644
--- a/homeassistant/components/broadlink/light.py
+++ b/homeassistant/components/broadlink/light.py
@@ -44,10 +44,11 @@ async def async_setup_entry(
 class BroadlinkLight(BroadlinkEntity, LightEntity):
     """Representation of a Broadlink light."""
 
+    _attr_has_entity_name = True
+
     def __init__(self, device):
         """Initialize the light."""
         super().__init__(device)
-        self._attr_name = f"{device.name} Light"
         self._attr_unique_id = device.unique_id
         self._attr_supported_color_modes = set()
 
diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py
index da72f4fcb06..4bbb3fe1513 100644
--- a/homeassistant/components/broadlink/remote.py
+++ b/homeassistant/components/broadlink/remote.py
@@ -106,6 +106,8 @@ async def async_setup_entry(
 class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
     """Representation of a Broadlink remote."""
 
+    _attr_has_entity_name = True
+
     def __init__(self, device, codes, flags):
         """Initialize the remote."""
         super().__init__(device)
@@ -116,7 +118,6 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
         self._flags = defaultdict(int)
         self._lock = asyncio.Lock()
 
-        self._attr_name = f"{device.name} Remote"
         self._attr_is_on = True
         self._attr_supported_features = (
             RemoteEntityFeature.LEARN_COMMAND | RemoteEntityFeature.DELETE_COMMAND
diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py
index f908391893f..4d362482e64 100644
--- a/homeassistant/components/broadlink/sensor.py
+++ b/homeassistant/components/broadlink/sensor.py
@@ -32,7 +32,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     ),
     SensorEntityDescription(
         key="air_quality",
-        name="Air Quality",
+        name="Air quality",
     ),
     SensorEntityDescription(
         key="humidity",
@@ -113,12 +113,13 @@ async def async_setup_entry(
 class BroadlinkSensor(BroadlinkEntity, SensorEntity):
     """Representation of a Broadlink sensor."""
 
+    _attr_has_entity_name = True
+
     def __init__(self, device, description: SensorEntityDescription):
         """Initialize the sensor."""
         super().__init__(device)
         self.entity_description = description
 
-        self._attr_name = f"{device.name} {description.name}"
         self._attr_native_value = self._coordinator.data[description.key]
         self._attr_unique_id = f"{device.unique_id}-{description.key}"
 
diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py
index 229949b7ee2..009536a9adb 100644
--- a/homeassistant/components/broadlink/switch.py
+++ b/homeassistant/components/broadlink/switch.py
@@ -145,7 +145,6 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
         super().__init__(device)
         self._command_on = command_on
         self._command_off = command_off
-        self._attr_name = f"{device.name} Switch"
 
     async def async_added_to_hass(self) -> None:
         """Call when the switch is added to hass."""
@@ -198,6 +197,8 @@ class BroadlinkRMSwitch(BroadlinkSwitch):
 class BroadlinkSP1Switch(BroadlinkSwitch):
     """Representation of a Broadlink SP1 switch."""
 
+    _attr_has_entity_name = True
+
     def __init__(self, device):
         """Initialize the switch."""
         super().__init__(device, 1, 0)
@@ -219,6 +220,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch):
     """Representation of a Broadlink SP2 switch."""
 
     _attr_assumed_state = False
+    _attr_has_entity_name = True
 
     def __init__(self, device, *args, **kwargs):
         """Initialize the switch."""
@@ -234,13 +236,14 @@ class BroadlinkMP1Slot(BroadlinkSwitch):
     """Representation of a Broadlink MP1 slot."""
 
     _attr_assumed_state = False
+    _attr_has_entity_name = True
 
     def __init__(self, device, slot):
         """Initialize the switch."""
         super().__init__(device, 1, 0)
         self._slot = slot
         self._attr_is_on = self._coordinator.data[f"s{slot}"]
-        self._attr_name = f"{device.name} S{slot}"
+        self._attr_name = f"S{slot}"
         self._attr_unique_id = f"{device.unique_id}-s{slot}"
 
     def _update_state(self, data):
@@ -263,6 +266,7 @@ class BroadlinkBG1Slot(BroadlinkSwitch):
     """Representation of a Broadlink BG1 slot."""
 
     _attr_assumed_state = False
+    _attr_has_entity_name = True
 
     def __init__(self, device, slot):
         """Initialize the switch."""
@@ -270,7 +274,7 @@ class BroadlinkBG1Slot(BroadlinkSwitch):
         self._slot = slot
         self._attr_is_on = self._coordinator.data[f"pwr{slot}"]
 
-        self._attr_name = f"{device.name} S{slot}"
+        self._attr_name = f"S{slot}"
         self._attr_device_class = SwitchDeviceClass.OUTLET
         self._attr_unique_id = f"{device.unique_id}-s{slot}"
 
diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py
index ce7e79bdff6..8cdb4f478a3 100644
--- a/tests/components/broadlink/__init__.py
+++ b/tests/components/broadlink/__init__.py
@@ -78,6 +78,16 @@ BROADLINK_DEVICES = {
         57,
         5,
     ),
+    "Gaming room": (
+        "192.168.0.65",
+        "34ea34b61d2d",
+        "MP1-1K4S",
+        "Broadlink",
+        "MP1",
+        0x4EB5,
+        57,
+        5,
+    ),
 }
 
 
diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py
index 5430af9e311..c50494a9b84 100644
--- a/tests/components/broadlink/test_device.py
+++ b/tests/components/broadlink/test_device.py
@@ -6,6 +6,7 @@ import broadlink.exceptions as blke
 from homeassistant.components.broadlink.const import DOMAIN
 from homeassistant.components.broadlink.device import get_domains
 from homeassistant.config_entries import ConfigEntryState
+from homeassistant.const import ATTR_FRIENDLY_NAME
 from homeassistant.helpers.entity_registry import async_entries_for_device
 
 from . import get_device
@@ -266,7 +267,11 @@ async def test_device_setup_registry(hass):
     assert device_entry.sw_version == device.fwversion
 
     for entry in async_entries_for_device(entity_registry, device_entry.id):
-        assert entry.original_name.startswith(device.name)
+        assert (
+            hass.states.get(entry.entity_id)
+            .attributes[ATTR_FRIENDLY_NAME]
+            .startswith(device.name)
+        )
 
 
 async def test_device_unload_works(hass):
@@ -345,4 +350,8 @@ async def test_device_update_listener(hass):
     )
     assert device_entry.name == "New Name"
     for entry in async_entries_for_device(entity_registry, device_entry.id):
-        assert entry.original_name.startswith("New Name")
+        assert (
+            hass.states.get(entry.entity_id)
+            .attributes[ATTR_FRIENDLY_NAME]
+            .startswith("New Name")
+        )
diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py
index a3b291efd00..d4fd9cd75b4 100644
--- a/tests/components/broadlink/test_remote.py
+++ b/tests/components/broadlink/test_remote.py
@@ -9,7 +9,7 @@ from homeassistant.components.remote import (
     SERVICE_TURN_OFF,
     SERVICE_TURN_ON,
 )
-from homeassistant.const import STATE_OFF, STATE_ON, Platform
+from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON, Platform
 from homeassistant.helpers.entity_registry import async_entries_for_device
 
 from . import get_device
@@ -39,7 +39,10 @@ async def test_remote_setup_works(hass):
         assert len(remotes) == 1
 
         remote = remotes[0]
-        assert remote.original_name == f"{device.name} Remote"
+        assert (
+            hass.states.get(remote.entity_id).attributes[ATTR_FRIENDLY_NAME]
+            == device.name
+        )
         assert hass.states.get(remote.entity_id).state == STATE_ON
         assert mock_setup.api.auth.call_count == 1
 
diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py
index b5a49fdae15..13190883ef0 100644
--- a/tests/components/broadlink/test_sensors.py
+++ b/tests/components/broadlink/test_sensors.py
@@ -3,7 +3,7 @@ from datetime import timedelta
 
 from homeassistant.components.broadlink.const import DOMAIN
 from homeassistant.components.broadlink.updater import BroadlinkSP4UpdateManager
-from homeassistant.const import Platform
+from homeassistant.const import ATTR_FRIENDLY_NAME, Platform
 from homeassistant.helpers.entity_component import async_update_entity
 from homeassistant.helpers.entity_registry import async_entries_for_device
 from homeassistant.util import dt
@@ -39,13 +39,16 @@ async def test_a1_sensor_setup(hass):
     assert len(sensors) == 5
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {
         (f"{device.name} Temperature", "27.4"),
         (f"{device.name} Humidity", "59.3"),
-        (f"{device.name} Air Quality", "3"),
+        (f"{device.name} Air quality", "3"),
         (f"{device.name} Light", "2"),
         (f"{device.name} Noise", "1"),
     }
@@ -86,13 +89,16 @@ async def test_a1_sensor_update(hass):
     assert mock_setup.api.check_sensors_raw.call_count == 2
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {
         (f"{device.name} Temperature", "22.5"),
         (f"{device.name} Humidity", "47.4"),
-        (f"{device.name} Air Quality", "2"),
+        (f"{device.name} Air quality", "2"),
         (f"{device.name} Light", "3"),
         (f"{device.name} Noise", "2"),
     }
@@ -118,7 +124,10 @@ async def test_rm_pro_sensor_setup(hass):
     assert len(sensors) == 1
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {(f"{device.name} Temperature", "18.2")}
@@ -147,7 +156,10 @@ async def test_rm_pro_sensor_update(hass):
     assert mock_setup.api.check_sensors.call_count == 2
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {(f"{device.name} Temperature", "25.8")}
@@ -179,7 +191,10 @@ async def test_rm_pro_filter_crazy_temperature(hass):
     assert mock_setup.api.check_sensors.call_count == 2
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {(f"{device.name} Temperature", "22.9")}
@@ -225,7 +240,10 @@ async def test_rm4_pro_hts2_sensor_setup(hass):
     assert len(sensors) == 2
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {
@@ -257,7 +275,10 @@ async def test_rm4_pro_hts2_sensor_update(hass):
     assert mock_setup.api.check_sensors.call_count == 2
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {
@@ -316,7 +337,10 @@ async def test_scb1e_sensor_setup(hass):
     assert len(sensors) == 5
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {
@@ -378,7 +402,10 @@ async def test_scb1e_sensor_update(hass):
     assert mock_setup.api.get_state.call_count == 2
 
     sensors_and_states = {
-        (sensor.original_name, hass.states.get(sensor.entity_id).state)
+        (
+            hass.states.get(sensor.entity_id).attributes[ATTR_FRIENDLY_NAME],
+            hass.states.get(sensor.entity_id).state,
+        )
         for sensor in sensors
     }
     assert sensors_and_states == {
diff --git a/tests/components/broadlink/test_switch.py b/tests/components/broadlink/test_switch.py
new file mode 100644
index 00000000000..9a7fc7e1ec9
--- /dev/null
+++ b/tests/components/broadlink/test_switch.py
@@ -0,0 +1,126 @@
+"""Tests for Broadlink switches."""
+from homeassistant.components.broadlink.const import DOMAIN
+from homeassistant.components.switch import (
+    DOMAIN as SWITCH_DOMAIN,
+    SERVICE_TURN_OFF,
+    SERVICE_TURN_ON,
+)
+from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON, Platform
+from homeassistant.helpers.entity_registry import async_entries_for_device
+
+from . import get_device
+
+from tests.common import mock_device_registry, mock_registry
+
+
+async def test_switch_setup_works(hass):
+    """Test a successful setup with a switch."""
+    device = get_device("Dining room")
+    device_registry = mock_device_registry(hass)
+    entity_registry = mock_registry(hass)
+    mock_setup = await device.setup_entry(hass)
+
+    device_entry = device_registry.async_get_device(
+        {(DOMAIN, mock_setup.entry.unique_id)}
+    )
+    entries = async_entries_for_device(entity_registry, device_entry.id)
+    switches = [entry for entry in entries if entry.domain == Platform.SWITCH]
+    assert len(switches) == 1
+
+    switch = switches[0]
+    assert (
+        hass.states.get(switch.entity_id).attributes[ATTR_FRIENDLY_NAME] == device.name
+    )
+    assert hass.states.get(switch.entity_id).state == STATE_OFF
+    assert mock_setup.api.auth.call_count == 1
+
+
+async def test_switch_turn_off_turn_on(hass):
+    """Test send turn on and off for a switch."""
+    device = get_device("Dining room")
+    device_registry = mock_device_registry(hass)
+    entity_registry = mock_registry(hass)
+    mock_setup = await device.setup_entry(hass)
+
+    device_entry = device_registry.async_get_device(
+        {(DOMAIN, mock_setup.entry.unique_id)}
+    )
+    entries = async_entries_for_device(entity_registry, device_entry.id)
+    switches = [entry for entry in entries if entry.domain == Platform.SWITCH]
+    assert len(switches) == 1
+
+    switch = switches[0]
+    await hass.services.async_call(
+        SWITCH_DOMAIN,
+        SERVICE_TURN_OFF,
+        {"entity_id": switch.entity_id},
+        blocking=True,
+    )
+    assert hass.states.get(switch.entity_id).state == STATE_OFF
+
+    await hass.services.async_call(
+        SWITCH_DOMAIN,
+        SERVICE_TURN_ON,
+        {"entity_id": switch.entity_id},
+        blocking=True,
+    )
+    assert hass.states.get(switch.entity_id).state == STATE_ON
+
+    assert mock_setup.api.auth.call_count == 1
+
+
+async def test_slots_switch_setup_works(hass):
+    """Test a successful setup with a switch with slots."""
+    device = get_device("Gaming room")
+    device_registry = mock_device_registry(hass)
+    entity_registry = mock_registry(hass)
+    mock_setup = await device.setup_entry(hass)
+
+    device_entry = device_registry.async_get_device(
+        {(DOMAIN, mock_setup.entry.unique_id)}
+    )
+    entries = async_entries_for_device(entity_registry, device_entry.id)
+    switches = [entry for entry in entries if entry.domain == Platform.SWITCH]
+    assert len(switches) == 4
+
+    for slot, switch in enumerate(switches):
+        assert (
+            hass.states.get(switch.entity_id).attributes[ATTR_FRIENDLY_NAME]
+            == f"{device.name} S{slot+1}"
+        )
+        assert hass.states.get(switch.entity_id).state == STATE_OFF
+        assert mock_setup.api.auth.call_count == 1
+
+
+async def test_slots_switch_turn_off_turn_on(hass):
+    """Test send turn on and off for a switch with slots."""
+    device = get_device("Gaming room")
+    device_registry = mock_device_registry(hass)
+    entity_registry = mock_registry(hass)
+    mock_setup = await device.setup_entry(hass)
+
+    device_entry = device_registry.async_get_device(
+        {(DOMAIN, mock_setup.entry.unique_id)}
+    )
+    entries = async_entries_for_device(entity_registry, device_entry.id)
+    switches = [entry for entry in entries if entry.domain == Platform.SWITCH]
+    assert len(switches) == 4
+
+    for switch in switches:
+        await hass.services.async_call(
+            SWITCH_DOMAIN,
+            SERVICE_TURN_OFF,
+            {"entity_id": switch.entity_id},
+            blocking=True,
+        )
+        assert hass.states.get(switch.entity_id).state == STATE_OFF
+
+        await hass.services.async_call(
+            SWITCH_DOMAIN,
+            SERVICE_TURN_ON,
+            {"entity_id": switch.entity_id},
+            blocking=True,
+        )
+        assert hass.states.get(switch.entity_id).state == STATE_ON
+
+        assert mock_setup.api.auth.call_count == 1
-- 
GitLab


From 8db416f1d6f506fca8ce31e4fa76a9eba769eec2 Mon Sep 17 00:00:00 2001
From: Ivan Puddu <dn0sar@users.noreply.github.com>
Date: Tue, 18 Oct 2022 22:53:34 +0200
Subject: [PATCH 583/985] Skip webostv trigger validation before the domain is
 setup (#80372)

* Skip trigger validation before the domain is setup

* Included None as return type

* Keep function signature intact. Check at the source
---
 homeassistant/components/webostv/device_trigger.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py
index 859accc86e6..ef3e74a7daa 100644
--- a/homeassistant/components/webostv/device_trigger.py
+++ b/homeassistant/components/webostv/device_trigger.py
@@ -46,7 +46,8 @@ async def async_validate_trigger_config(
         device_id = config[CONF_DEVICE_ID]
         try:
             device = async_get_device_entry_by_device_id(hass, device_id)
-            async_get_client_wrapper_by_device_entry(hass, device)
+            if DOMAIN in hass.data:
+                async_get_client_wrapper_by_device_entry(hass, device)
         except ValueError as err:
             raise InvalidDeviceAutomationConfig(err) from err
 
-- 
GitLab


From 86f9b8231fb02f0c4793e7d39d14d4c0786bb6aa Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 18 Oct 2022 23:51:36 +0200
Subject: [PATCH 584/985] Update demetriek to 0.4.0 (#80567)

---
 homeassistant/components/lametric/manifest.json | 2 +-
 requirements_all.txt                            | 2 +-
 requirements_test_all.txt                       | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json
index f3d48c53058..fcfcc082c35 100644
--- a/homeassistant/components/lametric/manifest.json
+++ b/homeassistant/components/lametric/manifest.json
@@ -2,7 +2,7 @@
   "domain": "lametric",
   "name": "LaMetric",
   "documentation": "https://www.home-assistant.io/integrations/lametric",
-  "requirements": ["demetriek==0.3.0"],
+  "requirements": ["demetriek==0.4.0"],
   "codeowners": ["@robbiet480", "@frenck", "@bachya"],
   "iot_class": "local_polling",
   "dependencies": ["application_credentials"],
diff --git a/requirements_all.txt b/requirements_all.txt
index 68cbd7e076d..8a1320eaa17 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -560,7 +560,7 @@ defusedxml==0.7.1
 deluge-client==1.7.1
 
 # homeassistant.components.lametric
-demetriek==0.3.0
+demetriek==0.4.0
 
 # homeassistant.components.denonavr
 denonavr==0.10.11
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 520c7fde01f..3b3b488ca15 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -434,7 +434,7 @@ defusedxml==0.7.1
 deluge-client==1.7.1
 
 # homeassistant.components.lametric
-demetriek==0.3.0
+demetriek==0.4.0
 
 # homeassistant.components.denonavr
 denonavr==0.10.11
-- 
GitLab


From a04c9892e736d041ded9f6be201640440d906058 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Wed, 19 Oct 2022 00:35:27 +0000
Subject: [PATCH 585/985] [ci skip] Translation update

---
 .../components/binary_sensor/translations/pl.json      | 10 +++++-----
 .../components/google_travel_time/translations/pl.json |  3 ++-
 .../components/google_travel_time/translations/tr.json |  3 ++-
 homeassistant/components/lametric/translations/tr.json |  2 ++
 4 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json
index d27d6443b55..e81344436c8 100644
--- a/homeassistant/components/binary_sensor/translations/pl.json
+++ b/homeassistant/components/binary_sensor/translations/pl.json
@@ -106,18 +106,18 @@
         }
     },
     "device_class": {
-        "co": "tlenek_w\u0119gla",
-        "cold": "zimno",
+        "co": "tlenek w\u0119gla",
+        "cold": "ch\u0142\u00f3d",
         "gas": "gaz",
-        "heat": "gor\u0105co",
-        "moisture": "wilgotno\u015b\u0107",
+        "heat": "ciep\u0142o",
+        "moisture": "wilgo\u0107",
         "motion": "ruch",
         "occupancy": "obecno\u015b\u0107",
         "power": "zasilanie",
         "problem": "problem",
         "smoke": "dym",
         "sound": "d\u017awi\u0119k",
-        "vibration": "wibracja"
+        "vibration": "wibracje"
     },
     "state": {
         "_": {
diff --git a/homeassistant/components/google_travel_time/translations/pl.json b/homeassistant/components/google_travel_time/translations/pl.json
index d6c88cf8822..0890a4fd350 100644
--- a/homeassistant/components/google_travel_time/translations/pl.json
+++ b/homeassistant/components/google_travel_time/translations/pl.json
@@ -4,7 +4,8 @@
             "already_configured": "Lokalizacja jest ju\u017c skonfigurowana"
         },
         "error": {
-            "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+            "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+            "invalid_auth": "Niepoprawne uwierzytelnienie"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/tr.json b/homeassistant/components/google_travel_time/translations/tr.json
index 421179ff1a0..117ca7797fb 100644
--- a/homeassistant/components/google_travel_time/translations/tr.json
+++ b/homeassistant/components/google_travel_time/translations/tr.json
@@ -4,7 +4,8 @@
             "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f"
         },
         "error": {
-            "cannot_connect": "Ba\u011flanma hatas\u0131"
+            "cannot_connect": "Ba\u011flanma hatas\u0131",
+            "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/lametric/translations/tr.json b/homeassistant/components/lametric/translations/tr.json
index 83f1f0e2ca4..5362e625f83 100644
--- a/homeassistant/components/lametric/translations/tr.json
+++ b/homeassistant/components/lametric/translations/tr.json
@@ -8,6 +8,8 @@
             "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})",
+            "reauth_device_not_found": "Yeniden do\u011frulamaya \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131z cihaz bu LaMetric hesab\u0131nda bulunamad\u0131",
+            "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu",
             "unknown": "Beklenmeyen hata"
         },
         "error": {
-- 
GitLab


From ddba653158b1f53624edeef3ea1d5da04b8eda46 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 03:29:11 +0200
Subject: [PATCH 586/985] Add new codeowners to scrape (#80569)

---
 CODEOWNERS                                    | 4 ++--
 homeassistant/components/scrape/manifest.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index 5783cf31cab..60bc4d99a8d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -966,8 +966,8 @@ build.json @home-assistant/supervisor
 /homeassistant/components/schedule/ @home-assistant/core
 /tests/components/schedule/ @home-assistant/core
 /homeassistant/components/schluter/ @prairieapps
-/homeassistant/components/scrape/ @fabaff
-/tests/components/scrape/ @fabaff
+/homeassistant/components/scrape/ @fabaff @gjohansson-ST @epenet
+/tests/components/scrape/ @fabaff @gjohansson-ST @epenet
 /homeassistant/components/screenlogic/ @dieselrabbit @bdraco
 /tests/components/screenlogic/ @dieselrabbit @bdraco
 /homeassistant/components/script/ @home-assistant/core
diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json
index 4f8ea3d1481..b7e5660e381 100644
--- a/homeassistant/components/scrape/manifest.json
+++ b/homeassistant/components/scrape/manifest.json
@@ -4,6 +4,6 @@
   "documentation": "https://www.home-assistant.io/integrations/scrape",
   "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.1"],
   "after_dependencies": ["rest"],
-  "codeowners": ["@fabaff"],
+  "codeowners": ["@fabaff", "@gjohansson-ST", "@epenet"],
   "iot_class": "cloud_polling"
 }
-- 
GitLab


From e3919babb226d572c768527ec745557b758e2321 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Wed, 19 Oct 2022 03:36:19 +0200
Subject: [PATCH 587/985] Add chart service to LaMetric (#80554)

---
 homeassistant/components/lametric/const.py    |  4 +-
 homeassistant/components/lametric/services.py | 68 +++++++++++---
 .../components/lametric/services.yaml         | 59 +++++++-----
 tests/components/lametric/test_services.py    | 93 ++++++++++++++++++-
 4 files changed, 187 insertions(+), 37 deletions(-)

diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py
index 4c6ea64c835..4f9472b24f4 100644
--- a/homeassistant/components/lametric/const.py
+++ b/homeassistant/components/lametric/const.py
@@ -19,10 +19,12 @@ LOGGER = logging.getLogger(__package__)
 SCAN_INTERVAL = timedelta(seconds=30)
 
 CONF_CYCLES: Final = "cycles"
+CONF_DATA: Final = "data"
 CONF_ICON_TYPE: Final = "icon_type"
 CONF_LIFETIME: Final = "lifetime"
+CONF_MESSAGE: Final = "message"
 CONF_PRIORITY: Final = "priority"
 CONF_SOUND: Final = "sound"
-CONF_MESSAGE: Final = "message"
 
 SERVICE_MESSAGE: Final = "message"
+SERVICE_CHART: Final = "chart"
diff --git a/homeassistant/components/lametric/services.py b/homeassistant/components/lametric/services.py
index be8dea5d7bf..2cbdfff6fd8 100644
--- a/homeassistant/components/lametric/services.py
+++ b/homeassistant/components/lametric/services.py
@@ -1,6 +1,11 @@
 """Support for LaMetric time services."""
+from __future__ import annotations
+
+from collections.abc import Sequence
+
 from demetriek import (
     AlarmSound,
+    Chart,
     LaMetricError,
     Model,
     Notification,
@@ -19,20 +24,21 @@ from homeassistant.helpers import config_validation as cv
 
 from .const import (
     CONF_CYCLES,
+    CONF_DATA,
     CONF_ICON_TYPE,
     CONF_MESSAGE,
     CONF_PRIORITY,
     CONF_SOUND,
     DOMAIN,
+    SERVICE_CHART,
     SERVICE_MESSAGE,
 )
 from .coordinator import LaMetricDataUpdateCoordinator
 from .helpers import async_get_coordinator_by_device_id
 
-SERVICE_MESSAGE_SCHEMA = vol.Schema(
+SERVICE_BASE_SCHEMA = vol.Schema(
     {
         vol.Required(CONF_DEVICE_ID): cv.string,
-        vol.Required(CONF_MESSAGE): cv.string,
         vol.Optional(CONF_CYCLES, default=1): cv.positive_int,
         vol.Optional(CONF_ICON_TYPE, default=NotificationIconType.NONE): vol.Coerce(
             NotificationIconType
@@ -43,34 +49,73 @@ SERVICE_MESSAGE_SCHEMA = vol.Schema(
         vol.Optional(CONF_SOUND): vol.Any(
             vol.Coerce(AlarmSound), vol.Coerce(NotificationSound)
         ),
+    }
+)
+
+SERVICE_MESSAGE_SCHEMA = SERVICE_BASE_SCHEMA.extend(
+    {
+        vol.Required(CONF_MESSAGE): cv.string,
         vol.Optional(CONF_ICON): cv.string,
     }
 )
 
+SERVICE_CHART_SCHEMA = SERVICE_BASE_SCHEMA.extend(
+    {
+        vol.Required(CONF_DATA): vol.All(cv.ensure_list, [vol.Coerce(int)]),
+    }
+)
+
 
 @callback
 def async_setup_services(hass: HomeAssistant) -> None:
     """Set up services for the LaMetric integration."""
 
-    async def _async_service_text(call: ServiceCall) -> None:
+    async def _async_service_chart(call: ServiceCall) -> None:
+        """Send a chart to a LaMetric device."""
+        coordinator = async_get_coordinator_by_device_id(
+            hass, call.data[CONF_DEVICE_ID]
+        )
+        await async_send_notification(
+            coordinator, call, [Chart(data=call.data[CONF_DATA])]
+        )
+
+    async def _async_service_message(call: ServiceCall) -> None:
         """Send a message to a LaMetric device."""
         coordinator = async_get_coordinator_by_device_id(
             hass, call.data[CONF_DEVICE_ID]
         )
-        await async_service_text(coordinator, call)
+        await async_send_notification(
+            coordinator,
+            call,
+            [
+                Simple(
+                    icon=call.data.get(CONF_ICON),
+                    text=call.data[CONF_MESSAGE],
+                )
+            ],
+        )
+
+    hass.services.async_register(
+        DOMAIN,
+        SERVICE_CHART,
+        _async_service_chart,
+        schema=SERVICE_CHART_SCHEMA,
+    )
 
     hass.services.async_register(
         DOMAIN,
         SERVICE_MESSAGE,
-        _async_service_text,
+        _async_service_message,
         schema=SERVICE_MESSAGE_SCHEMA,
     )
 
 
-async def async_service_text(
-    coordinator: LaMetricDataUpdateCoordinator, call: ServiceCall
+async def async_send_notification(
+    coordinator: LaMetricDataUpdateCoordinator,
+    call: ServiceCall,
+    frames: Sequence[Chart | Simple],
 ) -> None:
-    """Send a message to an LaMetric device."""
+    """Send a notification to an LaMetric device."""
     sound = None
     if CONF_SOUND in call.data:
         sound = Sound(id=call.data[CONF_SOUND], category=None)
@@ -79,12 +124,7 @@ async def async_service_text(
         icon_type=NotificationIconType(call.data[CONF_ICON_TYPE]),
         priority=NotificationPriority(call.data.get(CONF_PRIORITY)),
         model=Model(
-            frames=[
-                Simple(
-                    icon=call.data.get(CONF_ICON),
-                    text=call.data[CONF_MESSAGE],
-                )
-            ],
+            frames=frames,
             cycles=call.data[CONF_CYCLES],
             sound=sound,
         ),
diff --git a/homeassistant/components/lametric/services.yaml b/homeassistant/components/lametric/services.yaml
index 28cf982b4cd..5e8db5f7da4 100644
--- a/homeassistant/components/lametric/services.yaml
+++ b/homeassistant/components/lametric/services.yaml
@@ -1,29 +1,22 @@
-message:
-  name: Display a message
-  description: Display a message with an optional icon on a LaMetric device.
+chart:
+  name: Display a chart
+  description: Display a chart on a LaMetric device.
   fields:
-    device_id:
+    device_id: &device_id
       name: Device
-      description: The LaMetric device to display the message on.
+      description: The LaMetric device to display the chart on.
       required: true
       selector:
         device:
           integration: lametric
-    message:
-      name: Message
-      description: The message to display.
+    data:
+      name: Data
+      description: The list of data points in the chart
       required: true
+      example: "[1,2,3,4,5,4,3,2,1]"
       selector:
-        text:
-    icon:
-      name: Icon
-      description: >-
-        The ID number of the icon or animation to display. List of all icons
-        and their IDs can be found at: https://developer.lametric.com/icons
-      required: false
-      selector:
-        text:
-    sound:
+        object:
+    sound: &sound
       name: Sound
       description: The notification sound to play.
       required: false
@@ -126,7 +119,7 @@ message:
               value: "wind"
             - label: "Wind short"
               value: "wind_short"
-    cycles:
+    cycles: &cycles
       name: Cycles
       description: >-
         The number of times to display the message. When set to 0, the message
@@ -138,7 +131,7 @@ message:
           min: 0
           max: 10
           mode: slider
-    icon_type:
+    icon_type: &icon_type
       name: Icon type
       description: >-
         The type of icon to display, indicating the nature of the notification.
@@ -154,7 +147,7 @@ message:
               value: "info"
             - label: "Alert"
               value: "alert"
-    priority:
+    priority: &priority
       name: Priority
       description: >-
         The priority of the notification. When the device is running in
@@ -172,3 +165,27 @@ message:
               value: "warning"
             - label: "Critical"
               value: "critical"
+
+message:
+  name: Display a message
+  description: Display a message with an optional icon on a LaMetric device.
+  fields:
+    device_id: *device_id
+    message:
+      name: Message
+      description: The message to display.
+      required: true
+      selector:
+        text:
+    icon:
+      name: Icon
+      description: >-
+        The ID number of the icon or animation to display. List of all icons
+        and their IDs can be found at: https://developer.lametric.com/icons
+      required: false
+      selector:
+        text:
+    sound: *sound
+    cycles: *cycles
+    icon_type: *icon_type
+    priority: *priority
diff --git a/tests/components/lametric/test_services.py b/tests/components/lametric/test_services.py
index 50427755769..4792db266f8 100644
--- a/tests/components/lametric/test_services.py
+++ b/tests/components/lametric/test_services.py
@@ -2,6 +2,7 @@
 from unittest.mock import MagicMock
 
 from demetriek import (
+    Chart,
     LaMetricError,
     Notification,
     NotificationIconType,
@@ -14,11 +15,13 @@ import pytest
 
 from homeassistant.components.lametric.const import (
     CONF_CYCLES,
+    CONF_DATA,
     CONF_ICON_TYPE,
     CONF_MESSAGE,
     CONF_PRIORITY,
     CONF_SOUND,
     DOMAIN,
+    SERVICE_CHART,
     SERVICE_MESSAGE,
 )
 from homeassistant.const import CONF_DEVICE_ID, CONF_ICON
@@ -29,12 +32,100 @@ from homeassistant.helpers import entity_registry as er
 from tests.common import MockConfigEntry
 
 
+async def test_service_chart(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_lametric: MagicMock,
+) -> None:
+    """Test the LaMetric chart service."""
+    entity_registry = er.async_get(hass)
+
+    entry = entity_registry.async_get("button.frenck_s_lametric_next_app")
+    assert entry
+    assert entry.device_id
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_CHART,
+        {
+            CONF_DEVICE_ID: entry.device_id,
+            CONF_DATA: [1, 2, 3, 4, 5, 4, 3, 2, 1],
+        },
+        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 Chart
+    assert frame.data == [1, 2, 3, 4, 5, 4, 3, 2, 1]
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_CHART,
+        {
+            CONF_DATA: [1, 2, 3, 4, 5, 4, 3, 2, 1],
+            CONF_DEVICE_ID: entry.device_id,
+            CONF_CYCLES: 3,
+            CONF_ICON_TYPE: "info",
+            CONF_PRIORITY: "critical",
+            CONF_SOUND: "cat",
+        },
+        blocking=True,
+    )
+
+    assert len(mock_lametric.notify.mock_calls) == 2
+
+    notification: Notification = mock_lametric.notify.mock_calls[1][2]["notification"]
+    assert notification.icon_type is NotificationIconType.INFO
+    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.CAT
+    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 Chart
+    assert frame.data == [1, 2, 3, 4, 5, 4, 3, 2, 1]
+
+    mock_lametric.notify.side_effect = LaMetricError
+    with pytest.raises(
+        HomeAssistantError, match="Could not send LaMetric notification"
+    ):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_CHART,
+            {
+                CONF_DEVICE_ID: entry.device_id,
+                CONF_DATA: [1, 2, 3, 4, 5],
+            },
+            blocking=True,
+        )
+
+    assert len(mock_lametric.notify.mock_calls) == 3
+
+
 async def test_service_message(
     hass: HomeAssistant,
     init_integration: MockConfigEntry,
     mock_lametric: MagicMock,
 ) -> None:
-    """Test the LaMetric text service."""
+    """Test the LaMetric message service."""
     entity_registry = er.async_get(hass)
 
     entry = entity_registry.async_get("button.frenck_s_lametric_next_app")
-- 
GitLab


From eca45f9dd0ae3a85ef37f0f83fab33331d7c2bb9 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 04:08:06 +0200
Subject: [PATCH 588/985] Add websocket type hints in conversation (#80535)

---
 .../components/conversation/__init__.py       | 26 +++++++++++++++----
 1 file changed, 21 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py
index 9fd6d1ad3e2..deab740909e 100644
--- a/homeassistant/components/conversation/__init__.py
+++ b/homeassistant/components/conversation/__init__.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 from http import HTTPStatus
 import logging
 import re
+from typing import Any
 
 import voluptuous as vol
 
@@ -84,7 +85,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str}
 )
 @websocket_api.async_response
-async def websocket_process(hass, connection, msg):
+async def websocket_process(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Process text."""
     connection.send_result(
         msg["id"],
@@ -96,7 +101,11 @@ async def websocket_process(hass, connection, msg):
 
 @websocket_api.websocket_command({"type": "conversation/agent/info"})
 @websocket_api.async_response
-async def websocket_get_agent_info(hass, connection, msg):
+async def websocket_get_agent_info(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Do we need onboarding."""
     agent = await _get_agent(hass)
 
@@ -111,7 +120,11 @@ async def websocket_get_agent_info(hass, connection, msg):
 
 @websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool})
 @websocket_api.async_response
-async def websocket_set_onboarding(hass, connection, msg):
+async def websocket_set_onboarding(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Set onboarding status."""
     agent = await _get_agent(hass)
 
@@ -120,7 +133,7 @@ async def websocket_set_onboarding(hass, connection, msg):
     if success:
         connection.send_result(msg["id"])
     else:
-        connection.send_error(msg["id"])
+        connection.send_error(msg["id"], "error", "Failed to set onboarding")
 
 
 class ConversationProcessView(http.HomeAssistantView):
@@ -165,7 +178,10 @@ async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent:
 
 
 async def _async_converse(
-    hass: core.HomeAssistant, text: str, conversation_id: str, context: core.Context
+    hass: core.HomeAssistant,
+    text: str,
+    conversation_id: str | None,
+    context: core.Context,
 ) -> intent.IntentResponse:
     """Process text and get intent."""
     agent = await _get_agent(hass)
-- 
GitLab


From 60640b443670fa1226f1823a8ddd68ed089552fd Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 04:15:23 +0200
Subject: [PATCH 589/985] Add websocket type hints in components (#80533)

---
 homeassistant/components/cloud/http_api.py    | 79 ++++++++++++++++---
 .../components/mobile_app/websocket_api.py    |  9 ++-
 homeassistant/components/mqtt/__init__.py     |  6 +-
 .../components/trace/websocket_api.py         | 19 ++++-
 homeassistant/components/webhook/__init__.py  |  6 +-
 5 files changed, 99 insertions(+), 20 deletions(-)

diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index ebeb79dcd2a..90b8bad108f 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -4,6 +4,7 @@ import dataclasses
 from functools import wraps
 from http import HTTPStatus
 import logging
+from typing import Any
 
 import aiohttp
 import async_timeout
@@ -282,7 +283,11 @@ class CloudForgotPasswordView(HomeAssistantView):
 
 @websocket_api.websocket_command({vol.Required("type"): "cloud/status"})
 @websocket_api.async_response
-async def websocket_cloud_status(hass, connection, msg):
+async def websocket_cloud_status(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle request for account info.
 
     Async friendly.
@@ -316,7 +321,11 @@ def _require_cloud_login(handler):
 @_require_cloud_login
 @websocket_api.websocket_command({vol.Required("type"): "cloud/subscription"})
 @websocket_api.async_response
-async def websocket_subscription(hass, connection, msg):
+async def websocket_subscription(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle request for account info."""
     cloud = hass.data[DOMAIN]
     try:
@@ -347,7 +356,11 @@ async def websocket_subscription(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_update_prefs(hass, connection, msg):
+async def websocket_update_prefs(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle request for account info."""
     cloud = hass.data[DOMAIN]
 
@@ -392,7 +405,11 @@ async def websocket_update_prefs(hass, connection, msg):
 )
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def websocket_hook_create(hass, connection, msg):
+async def websocket_hook_create(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle request for account info."""
     cloud = hass.data[DOMAIN]
     hook = await cloud.cloudhooks.async_create(msg["webhook_id"], False)
@@ -408,7 +425,11 @@ async def websocket_hook_create(hass, connection, msg):
 )
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def websocket_hook_delete(hass, connection, msg):
+async def websocket_hook_delete(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle request for account info."""
     cloud = hass.data[DOMAIN]
     await cloud.cloudhooks.async_delete(msg["webhook_id"])
@@ -470,7 +491,11 @@ async def _account_data(hass: HomeAssistant, cloud: Cloud):
 @websocket_api.websocket_command({"type": "cloud/remote/connect"})
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def websocket_remote_connect(hass, connection, msg):
+async def websocket_remote_connect(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle request for connect remote."""
     cloud = hass.data[DOMAIN]
     await cloud.client.prefs.async_update(remote_enabled=True)
@@ -482,7 +507,11 @@ async def websocket_remote_connect(hass, connection, msg):
 @websocket_api.websocket_command({"type": "cloud/remote/disconnect"})
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def websocket_remote_disconnect(hass, connection, msg):
+async def websocket_remote_disconnect(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle request for disconnect remote."""
     cloud = hass.data[DOMAIN]
     await cloud.client.prefs.async_update(remote_enabled=False)
@@ -494,7 +523,11 @@ async def websocket_remote_disconnect(hass, connection, msg):
 @websocket_api.websocket_command({"type": "cloud/google_assistant/entities"})
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def google_assistant_list(hass, connection, msg):
+async def google_assistant_list(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """List all google assistant entities."""
     cloud = hass.data[DOMAIN]
     gconf = await cloud.client.get_google_config()
@@ -528,7 +561,11 @@ async def google_assistant_list(hass, connection, msg):
 )
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def google_assistant_update(hass, connection, msg):
+async def google_assistant_update(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Update google assistant config."""
     cloud = hass.data[DOMAIN]
     changes = dict(msg)
@@ -547,7 +584,11 @@ async def google_assistant_update(hass, connection, msg):
 @websocket_api.websocket_command({"type": "cloud/alexa/entities"})
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def alexa_list(hass, connection, msg):
+async def alexa_list(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """List all alexa entities."""
     cloud = hass.data[DOMAIN]
     alexa_config = await cloud.client.get_alexa_config()
@@ -578,7 +619,11 @@ async def alexa_list(hass, connection, msg):
 )
 @websocket_api.async_response
 @_ws_handle_cloud_errors
-async def alexa_update(hass, connection, msg):
+async def alexa_update(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Update alexa entity config."""
     cloud = hass.data[DOMAIN]
     changes = dict(msg)
@@ -596,7 +641,11 @@ async def alexa_update(hass, connection, msg):
 @_require_cloud_login
 @websocket_api.websocket_command({"type": "cloud/alexa/sync"})
 @websocket_api.async_response
-async def alexa_sync(hass, connection, msg):
+async def alexa_sync(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Sync with Alexa."""
     cloud = hass.data[DOMAIN]
     alexa_config = await cloud.client.get_alexa_config()
@@ -622,7 +671,11 @@ async def alexa_sync(hass, connection, msg):
 
 @websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str})
 @websocket_api.async_response
-async def thingtalk_convert(hass, connection, msg):
+async def thingtalk_convert(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Convert a query."""
     cloud = hass.data[DOMAIN]
 
diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py
index 4b0863d77af..5225100662c 100644
--- a/homeassistant/components/mobile_app/websocket_api.py
+++ b/homeassistant/components/mobile_app/websocket_api.py
@@ -2,11 +2,12 @@
 from __future__ import annotations
 
 from functools import wraps
+from typing import Any
 
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
 
 from .const import CONF_USER_ID, DATA_CONFIG_ENTRIES, DATA_PUSH_CHANNEL, DOMAIN
 from .push_notification import PushChannel
@@ -88,7 +89,11 @@ def handle_push_notification_confirm(hass, connection, msg):
 )
 @_ensure_webhook_access
 @websocket_api.async_response
-async def handle_push_notification_channel(hass, connection, msg):
+async def handle_push_notification_channel(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Set up a direct push notification channel."""
     webhook_id = msg["webhook_id"]
     registered_channels: dict[str, PushChannel] = hass.data[DOMAIN][DATA_PUSH_CHANNEL]
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 36299c72608..a63b6137fea 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -511,7 +511,11 @@ def websocket_mqtt_info(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_subscribe(hass, connection, msg):
+async def websocket_subscribe(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Subscribe to a MQTT topic."""
     if not connection.user.is_admin:
         raise Unauthorized
diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py
index 2ea58db895a..ba79ae68968 100644
--- a/homeassistant/components/trace/websocket_api.py
+++ b/homeassistant/components/trace/websocket_api.py
@@ -1,5 +1,6 @@
 """Websocket API for automation."""
 import json
+from typing import Any
 
 import voluptuous as vol
 
@@ -54,7 +55,11 @@ def async_setup(hass: HomeAssistant) -> None:
     }
 )
 @websocket_api.async_response
-async def websocket_trace_get(hass, connection, msg):
+async def websocket_trace_get(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Get a script or automation trace."""
     key = f"{msg['domain']}.{msg['item_id']}"
     run_id = msg["run_id"]
@@ -83,7 +88,11 @@ async def websocket_trace_get(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_trace_list(hass, connection, msg):
+async def websocket_trace_list(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Summarize script and automation traces."""
     wanted_domain = msg["domain"]
     key = f"{msg['domain']}.{msg['item_id']}" if "item_id" in msg else None
@@ -102,7 +111,11 @@ async def websocket_trace_list(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_trace_contexts(hass, connection, msg):
+async def websocket_trace_contexts(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Retrieve contexts we have traces for."""
     key = f"{msg['domain']}.{msg['item_id']}" if "item_id" in msg else None
 
diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py
index 449de006bf9..f78d9059ee9 100644
--- a/homeassistant/components/webhook/__init__.py
+++ b/homeassistant/components/webhook/__init__.py
@@ -195,7 +195,11 @@ def websocket_list(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_handle(hass, connection, msg):
+async def websocket_handle(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle an incoming webhook via the WS API."""
     request = MockRequest(
         content=msg["body"].encode("utf-8"),
-- 
GitLab


From 971ac015e7b1d1205e22fb3183fcfe699ae9e19b Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 04:15:55 +0200
Subject: [PATCH 590/985] Add websocket type hints in config (#80532)

---
 homeassistant/components/config/auth.py       | 27 +++++++++++--
 .../config/auth_provider_homeassistant.py     | 40 ++++++++++++++-----
 .../components/config/config_entries.py       | 36 ++++++++++++-----
 homeassistant/components/config/core.py       | 17 ++++++--
 4 files changed, 92 insertions(+), 28 deletions(-)

diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py
index 15fc6634f5b..1699a4c8509 100644
--- a/homeassistant/components/config/auth.py
+++ b/homeassistant/components/config/auth.py
@@ -1,7 +1,10 @@
 """Offer API to configure Home Assistant auth."""
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
+from homeassistant.core import HomeAssistant
 
 WS_TYPE_LIST = "config/auth/list"
 SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
@@ -29,7 +32,11 @@ async def async_setup(hass):
 
 @websocket_api.require_admin
 @websocket_api.async_response
-async def websocket_list(hass, connection, msg):
+async def websocket_list(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Return a list of users."""
     result = [_user_info(u) for u in await hass.auth.async_get_users()]
 
@@ -38,7 +45,11 @@ async def websocket_list(hass, connection, msg):
 
 @websocket_api.require_admin
 @websocket_api.async_response
-async def websocket_delete(hass, connection, msg):
+async def websocket_delete(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Delete a user."""
     if msg["user_id"] == connection.user.id:
         connection.send_message(
@@ -69,7 +80,11 @@ async def websocket_delete(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_create(hass, connection, msg):
+async def websocket_create(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Create a user."""
     user = await hass.auth.async_create_user(
         msg["name"], group_ids=msg.get("group_ids"), local_only=msg.get("local_only")
@@ -92,7 +107,11 @@ async def websocket_create(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_update(hass, connection, msg):
+async def websocket_update(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Update a user."""
     if not (user := await hass.auth.async_get_user(msg.pop("user_id"))):
         connection.send_message(
diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py
index b6874720c47..d0606a748a9 100644
--- a/homeassistant/components/config/auth_provider_homeassistant.py
+++ b/homeassistant/components/config/auth_provider_homeassistant.py
@@ -1,9 +1,11 @@
 """Offer API to configure the Home Assistant auth provider."""
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant.auth.providers import homeassistant as auth_ha
 from homeassistant.components import websocket_api
-from homeassistant.components.websocket_api import decorators
+from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import Unauthorized
 
 
@@ -16,7 +18,7 @@ async def async_setup(hass):
     return True
 
 
-@decorators.websocket_command(
+@websocket_api.websocket_command(
     {
         vol.Required("type"): "config/auth_provider/homeassistant/create",
         vol.Required("user_id"): str,
@@ -26,7 +28,11 @@ async def async_setup(hass):
 )
 @websocket_api.require_admin
 @websocket_api.async_response
-async def websocket_create(hass, connection, msg):
+async def websocket_create(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Create credentials and attach to a user."""
     provider = auth_ha.async_get_provider(hass)
 
@@ -56,7 +62,7 @@ async def websocket_create(hass, connection, msg):
     connection.send_result(msg["id"])
 
 
-@decorators.websocket_command(
+@websocket_api.websocket_command(
     {
         vol.Required("type"): "config/auth_provider/homeassistant/delete",
         vol.Required("username"): str,
@@ -64,7 +70,11 @@ async def websocket_create(hass, connection, msg):
 )
 @websocket_api.require_admin
 @websocket_api.async_response
-async def websocket_delete(hass, connection, msg):
+async def websocket_delete(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Delete username and related credential."""
     provider = auth_ha.async_get_provider(hass)
     credentials = await provider.async_get_or_create_credentials(
@@ -90,7 +100,7 @@ async def websocket_delete(hass, connection, msg):
     connection.send_result(msg["id"])
 
 
-@decorators.websocket_command(
+@websocket_api.websocket_command(
     {
         vol.Required("type"): "config/auth_provider/homeassistant/change_password",
         vol.Required("current_password"): str,
@@ -98,7 +108,11 @@ async def websocket_delete(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def websocket_change_password(hass, connection, msg):
+async def websocket_change_password(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Change current user password."""
     if (user := connection.user) is None:
         connection.send_error(msg["id"], "user_not_found", "User not found")
@@ -130,7 +144,7 @@ async def websocket_change_password(hass, connection, msg):
     connection.send_result(msg["id"])
 
 
-@decorators.websocket_command(
+@websocket_api.websocket_command(
     {
         vol.Required(
             "type"
@@ -139,9 +153,13 @@ async def websocket_change_password(hass, connection, msg):
         vol.Required("password"): str,
     }
 )
-@decorators.require_admin
-@decorators.async_response
-async def websocket_admin_change_password(hass, connection, msg):
+@websocket_api.require_admin
+@websocket_api.async_response
+async def websocket_admin_change_password(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Change password of any user."""
     if not connection.user.is_owner:
         raise Unauthorized(context=connection.context(msg))
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index fb96a99827d..6f2320a2e93 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -13,7 +13,6 @@ from homeassistant import config_entries, data_entry_flow
 from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
 from homeassistant.components import websocket_api
 from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.websocket_api.connection import ActiveConnection
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import DependencyError, Unauthorized
 from homeassistant.helpers.data_entry_flow import (
@@ -291,7 +290,11 @@ def get_entry(
     }
 )
 @websocket_api.async_response
-async def config_entry_update(hass, connection, msg):
+async def config_entry_update(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Update config entry."""
     changes = dict(msg)
     changes.pop("id")
@@ -311,9 +314,10 @@ async def config_entry_update(hass, connection, msg):
         "require_restart": False,
     }
 
+    initial_state = entry.state
     if (
         old_disable_polling != entry.pref_disable_polling
-        and entry.state is config_entries.ConfigEntryState.LOADED
+        and initial_state is config_entries.ConfigEntryState.LOADED
     ):
         if not await hass.config_entries.async_reload(entry.entry_id):
             result["require_restart"] = (
@@ -334,14 +338,18 @@ async def config_entry_update(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def config_entry_disable(hass, connection, msg):
+async def config_entry_disable(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Disable config entry."""
     if (disabled_by := msg["disabled_by"]) is not None:
         disabled_by = config_entries.ConfigEntryDisabler(disabled_by)
 
-    result = False
+    success = False
     try:
-        result = await hass.config_entries.async_set_disabled_by(
+        success = await hass.config_entries.async_set_disabled_by(
             msg["entry_id"], disabled_by
         )
     except config_entries.OperationNotAllowed:
@@ -351,7 +359,7 @@ async def config_entry_disable(hass, connection, msg):
         send_entry_not_found(connection, msg["id"])
         return
 
-    result = {"require_restart": not result}
+    result = {"require_restart": not success}
 
     connection.send_result(msg["id"], result)
 
@@ -361,7 +369,11 @@ async def config_entry_disable(hass, connection, msg):
     {"type": "config_entries/ignore_flow", "flow_id": str, "title": str}
 )
 @websocket_api.async_response
-async def ignore_config_flow(hass, connection, msg):
+async def ignore_config_flow(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Ignore a config flow."""
     flow = next(
         (
@@ -399,7 +411,9 @@ async def ignore_config_flow(hass, connection, msg):
 )
 @websocket_api.async_response
 async def config_entries_get(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
 ) -> None:
     """Return matching config entries by type and/or domain."""
     connection.send_result(
@@ -418,7 +432,9 @@ async def config_entries_get(
 )
 @websocket_api.async_response
 async def config_entries_subscribe(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
 ) -> None:
     """Subscribe to config entry updates."""
     type_filter = msg.get("type_filter")
diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py
index d6d97b5caca..5ffb2d4db99 100644
--- a/homeassistant/components/config/core.py
+++ b/homeassistant/components/config/core.py
@@ -1,10 +1,13 @@
 """Component to interact with Hassbian tools."""
 
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
 from homeassistant.components.http import HomeAssistantView
 from homeassistant.config import async_check_ha_config_file
+from homeassistant.core import HomeAssistant
 from homeassistant.helpers import config_validation as cv
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.util import location, unit_system
@@ -49,7 +52,11 @@ class CheckConfigView(HomeAssistantView):
     }
 )
 @websocket_api.async_response
-async def websocket_update_config(hass, connection, msg):
+async def websocket_update_config(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle update core config command."""
     data = dict(msg)
     data.pop("id")
@@ -65,12 +72,16 @@ async def websocket_update_config(hass, connection, msg):
 @websocket_api.require_admin
 @websocket_api.websocket_command({"type": "config/core/detect"})
 @websocket_api.async_response
-async def websocket_detect_config(hass, connection, msg):
+async def websocket_detect_config(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Detect core config."""
     session = async_get_clientsession(hass)
     location_info = await location.async_detect_location_info(session)
 
-    info = {}
+    info: dict[str, Any] = {}
 
     if location_info is None:
         connection.send_result(msg["id"], info)
-- 
GitLab


From c0be1d9fad68c4525b1f199ec014a0a93978e156 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 04:23:17 +0200
Subject: [PATCH 591/985] Add websocket type hints in blueprint (#80531)

---
 homeassistant/components/blueprint/models.py  |  2 +-
 .../components/blueprint/websocket_api.py     | 45 ++++++++++++-------
 2 files changed, 29 insertions(+), 18 deletions(-)

diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py
index 7547a701220..ee81a583391 100644
--- a/homeassistant/components/blueprint/models.py
+++ b/homeassistant/components/blueprint/models.py
@@ -51,7 +51,7 @@ class Blueprint:
 
     def __init__(
         self,
-        data: dict,
+        data: dict[str, Any],
         *,
         path: str | None = None,
         expected_domain: str | None = None,
diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py
index 0b84d1d08c2..a9bcf5ded1c 100644
--- a/homeassistant/components/blueprint/websocket_api.py
+++ b/homeassistant/components/blueprint/websocket_api.py
@@ -1,6 +1,8 @@
 """Websocket API for blueprint."""
 from __future__ import annotations
 
+from typing import Any, cast
+
 import async_timeout
 import voluptuous as vol
 
@@ -31,12 +33,14 @@ def async_setup(hass: HomeAssistant):
     }
 )
 @websocket_api.async_response
-async def ws_list_blueprints(hass, connection, msg):
+async def ws_list_blueprints(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """List available blueprints."""
-    domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get(
-        DOMAIN, {}
-    )
-    results = {}
+    domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {})
+    results: dict[str, Any] = {}
 
     if msg["domain"] not in domain_blueprints:
         connection.send_result(msg["id"], results)
@@ -62,7 +66,11 @@ async def ws_list_blueprints(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def ws_import_blueprint(hass, connection, msg):
+async def ws_import_blueprint(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Import a blueprint."""
     async with async_timeout.timeout(10):
         imported_blueprint = await importer.fetch_blueprint_from_url(hass, msg["url"])
@@ -96,15 +104,17 @@ async def ws_import_blueprint(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def ws_save_blueprint(hass, connection, msg):
+async def ws_save_blueprint(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Save a blueprint."""
 
     path = msg["path"]
     domain = msg["domain"]
 
-    domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get(
-        DOMAIN, {}
-    )
+    domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {})
 
     if domain not in domain_blueprints:
         connection.send_error(
@@ -112,9 +122,8 @@ async def ws_save_blueprint(hass, connection, msg):
         )
 
     try:
-        blueprint = models.Blueprint(
-            yaml.parse_yaml(msg["yaml"]), expected_domain=domain
-        )
+        yaml_data = cast(dict[str, Any], yaml.parse_yaml(msg["yaml"]))
+        blueprint = models.Blueprint(yaml_data, expected_domain=domain)
         if "source_url" in msg:
             blueprint.update_metadata(source_url=msg["source_url"])
     except HomeAssistantError as err:
@@ -143,15 +152,17 @@ async def ws_save_blueprint(hass, connection, msg):
     }
 )
 @websocket_api.async_response
-async def ws_delete_blueprint(hass, connection, msg):
+async def ws_delete_blueprint(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Delete a blueprint."""
 
     path = msg["path"]
     domain = msg["domain"]
 
-    domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get(
-        DOMAIN, {}
-    )
+    domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {})
 
     if domain not in domain_blueprints:
         connection.send_error(
-- 
GitLab


From 31a787558fd312331b55e5c2c4b33341fc3601fc Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 19 Oct 2022 07:58:47 +0200
Subject: [PATCH 592/985] Ensure recorder test fixture is setup before hass
 fixture (#80528)

* Ensure recorder test fixture is setup before hass fixture

* Adjust more tests
---
 tests/components/analytics/test_analytics.py  |  4 +-
 tests/components/automation/test_recorder.py  |  2 +-
 tests/components/calendar/test_recorder.py    |  2 +-
 tests/components/camera/test_recorder.py      |  2 +-
 tests/components/climate/test_recorder.py     |  2 +-
 tests/components/demo/test_init.py            | 12 +--
 tests/components/energy/test_sensor.py        | 24 ++---
 tests/components/energy/test_validate.py      |  2 +-
 tests/components/energy/test_websocket_api.py | 10 +-
 tests/components/fan/test_recorder.py         |  2 +-
 tests/components/filter/test_sensor.py        | 16 ++--
 tests/components/group/test_recorder.py       |  2 +-
 tests/components/history/test_init.py         | 36 ++++----
 tests/components/history_stats/test_sensor.py | 38 ++++----
 tests/components/humidifier/test_recorder.py  |  2 +-
 .../components/input_boolean/test_recorder.py |  2 +-
 .../components/input_button/test_recorder.py  |  2 +-
 .../input_datetime/test_recorder.py           |  2 +-
 .../components/input_number/test_recorder.py  |  2 +-
 .../components/input_select/test_recorder.py  |  2 +-
 tests/components/input_text/test_recorder.py  |  2 +-
 tests/components/light/test_recorder.py       |  2 +-
 tests/components/logbook/test_init.py         | 92 +++++++++----------
 .../components/logbook/test_websocket_api.py  | 56 +++++------
 .../components/media_player/test_recorder.py  |  2 +-
 tests/components/number/test_recorder.py      |  2 +-
 tests/components/plant/test_init.py           |  2 +-
 tests/components/recorder/test_backup.py      | 10 +-
 .../test_filters_with_entityfilter.py         | 22 ++---
 tests/components/recorder/test_history.py     |  8 +-
 tests/components/recorder/test_init.py        | 30 +++---
 tests/components/recorder/test_purge.py       | 36 ++++----
 tests/components/recorder/test_run_history.py |  2 +-
 tests/components/recorder/test_statistics.py  |  2 +-
 .../components/recorder/test_system_health.py |  8 +-
 tests/components/recorder/test_util.py        |  2 +-
 .../components/recorder/test_websocket_api.py | 44 ++++-----
 tests/components/schedule/test_recorder.py    |  2 +-
 tests/components/script/test_recorder.py      |  2 +-
 tests/components/select/test_recorder.py      |  2 +-
 tests/components/sensor/test_recorder.py      | 20 ++--
 tests/components/siren/test_recorder.py       |  2 +-
 tests/components/sql/test_config_flow.py      | 20 ++--
 tests/components/statistics/test_sensor.py    |  6 +-
 tests/components/sun/test_recorder.py         |  2 +-
 tests/components/tibber/test_config_flow.py   |  6 +-
 tests/components/tibber/test_diagnostics.py   |  2 +-
 tests/components/tibber/test_statistics.py    |  2 +-
 tests/components/update/test_recorder.py      |  2 +-
 tests/components/vacuum/test_recorder.py      |  2 +-
 .../components/water_heater/test_recorder.py  |  2 +-
 tests/components/weather/test_recorder.py     |  2 +-
 tests/conftest.py                             | 13 ++-
 tests/helpers/test_recorder.py                |  2 +-
 54 files changed, 293 insertions(+), 284 deletions(-)

diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py
index 82a61126432..b73338add35 100644
--- a/tests/components/analytics/test_analytics.py
+++ b/tests/components/analytics/test_analytics.py
@@ -451,7 +451,7 @@ async def test_send_with_no_energy(hass, aioclient_mock):
     assert "energy" not in postdata
 
 
-async def test_send_with_no_energy_config(hass, aioclient_mock, recorder_mock):
+async def test_send_with_no_energy_config(recorder_mock, hass, aioclient_mock):
     """Test send base preferences are defined."""
     aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
     analytics = Analytics(hass)
@@ -473,7 +473,7 @@ async def test_send_with_no_energy_config(hass, aioclient_mock, recorder_mock):
     assert not postdata["energy"]["configured"]
 
 
-async def test_send_with_energy_config(hass, aioclient_mock, recorder_mock):
+async def test_send_with_energy_config(recorder_mock, hass, aioclient_mock):
     """Test send base preferences are defined."""
     aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200)
     analytics = Analytics(hass)
diff --git a/tests/components/automation/test_recorder.py b/tests/components/automation/test_recorder.py
index 4067393b76c..8ce543a3f47 100644
--- a/tests/components/automation/test_recorder.py
+++ b/tests/components/automation/test_recorder.py
@@ -27,7 +27,7 @@ def calls(hass):
     return async_mock_service(hass, "test", "automation")
 
 
-async def test_exclude_attributes(hass, recorder_mock, calls):
+async def test_exclude_attributes(recorder_mock, hass, calls):
     """Test automation registered attributes to be excluded."""
     assert await async_setup_component(
         hass,
diff --git a/tests/components/calendar/test_recorder.py b/tests/components/calendar/test_recorder.py
index 0fbcaf38432..85e32155723 100644
--- a/tests/components/calendar/test_recorder.py
+++ b/tests/components/calendar/test_recorder.py
@@ -13,7 +13,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_events_http_api(hass, recorder_mock):
+async def test_events_http_api(recorder_mock, hass):
     """Test the calendar demo view."""
     await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
     await hass.async_block_till_done()
diff --git a/tests/components/camera/test_recorder.py b/tests/components/camera/test_recorder.py
index 1217997a996..3417399d729 100644
--- a/tests/components/camera/test_recorder.py
+++ b/tests/components/camera/test_recorder.py
@@ -20,7 +20,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test camera registered attributes to be excluded."""
     await async_setup_component(
         hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/climate/test_recorder.py b/tests/components/climate/test_recorder.py
index bf254d2c02f..be3a0f22856 100644
--- a/tests/components/climate/test_recorder.py
+++ b/tests/components/climate/test_recorder.py
@@ -26,7 +26,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test climate registered attributes to be excluded."""
     await async_setup_component(
         hass, climate.DOMAIN, {climate.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py
index f7a89fe63c1..1bec552a55f 100644
--- a/tests/components/demo/test_init.py
+++ b/tests/components/demo/test_init.py
@@ -22,20 +22,20 @@ from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 from tests.components.recorder.common import async_wait_recording_done
 
 
-@pytest.fixture(autouse=True)
+@pytest.fixture
 def mock_history(hass):
     """Mock history component loaded."""
     hass.config.components.add("history")
 
 
 @pytest.fixture(autouse=True)
-def mock_device_tracker_update_config(hass):
+def mock_device_tracker_update_config():
     """Prevent device tracker from creating known devices file."""
     with patch("homeassistant.components.device_tracker.legacy.update_config"):
         yield
 
 
-async def test_setting_up_demo(hass):
+async def test_setting_up_demo(mock_history, hass):
     """Test if we can set up the demo and dump it to JSON."""
     assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
     await hass.async_block_till_done()
@@ -52,7 +52,7 @@ async def test_setting_up_demo(hass):
         )
 
 
-async def test_demo_statistics(hass, recorder_mock):
+async def test_demo_statistics(recorder_mock, mock_history, hass):
     """Test that the demo components makes some statistics available."""
     assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
     await hass.async_block_till_done()
@@ -82,7 +82,7 @@ async def test_demo_statistics(hass, recorder_mock):
     } in statistic_ids
 
 
-async def test_demo_statistics_growth(hass, recorder_mock):
+async def test_demo_statistics_growth(recorder_mock, mock_history, hass):
     """Test that the demo sum statistics adds to the previous state."""
     hass.config.units = IMPERIAL_SYSTEM
 
@@ -120,7 +120,7 @@ async def test_demo_statistics_growth(hass, recorder_mock):
     assert statistics[statistic_id][0]["sum"] <= (2**20 + 24)
 
 
-async def test_issues_created(hass, hass_client, hass_ws_client):
+async def test_issues_created(mock_history, hass, hass_client, hass_ws_client):
     """Test issues are created and can be fixed."""
     assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
     assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py
index 364fdeb365b..138c2494d66 100644
--- a/tests/components/energy/test_sensor.py
+++ b/tests/components/energy/test_sensor.py
@@ -49,7 +49,7 @@ def get_statistics_for_entity(statistics_results, entity_id):
     return None
 
 
-async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> None:
+async def test_cost_sensor_no_states(setup_integration, hass, hass_storage) -> None:
     """Test sensors are created."""
     energy_data = data.EnergyManager.default_preferences()
     energy_data["energy_sources"].append(
@@ -75,7 +75,7 @@ async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> N
     # TODO: No states, should the cost entity refuse to setup?
 
 
-async def test_cost_sensor_attributes(hass, hass_storage, setup_integration) -> None:
+async def test_cost_sensor_attributes(setup_integration, hass, hass_storage) -> None:
     """Test sensor attributes."""
     energy_data = data.EnergyManager.default_preferences()
     energy_data["energy_sources"].append(
@@ -124,10 +124,10 @@ async def test_cost_sensor_attributes(hass, hass_storage, setup_integration) ->
     ],
 )
 async def test_cost_sensor_price_entity_total_increasing(
+    setup_integration,
     hass,
     hass_storage,
     hass_ws_client,
-    setup_integration,
     initial_energy,
     initial_cost,
     price_entity,
@@ -327,10 +327,10 @@ async def test_cost_sensor_price_entity_total_increasing(
 )
 @pytest.mark.parametrize("energy_state_class", ["total", "measurement"])
 async def test_cost_sensor_price_entity_total(
+    setup_integration,
     hass,
     hass_storage,
     hass_ws_client,
-    setup_integration,
     initial_energy,
     initial_cost,
     price_entity,
@@ -533,10 +533,10 @@ async def test_cost_sensor_price_entity_total(
 )
 @pytest.mark.parametrize("energy_state_class", ["total"])
 async def test_cost_sensor_price_entity_total_no_reset(
+    setup_integration,
     hass,
     hass_storage,
     hass_ws_client,
-    setup_integration,
     initial_energy,
     initial_cost,
     price_entity,
@@ -706,7 +706,7 @@ async def test_cost_sensor_price_entity_total_no_reset(
     ],
 )
 async def test_cost_sensor_handle_energy_units(
-    hass, hass_storage, setup_integration, energy_unit, factor
+    setup_integration, hass, hass_storage, energy_unit, factor
 ) -> None:
     """Test energy cost price from sensor entity."""
     energy_attributes = {
@@ -771,7 +771,7 @@ async def test_cost_sensor_handle_energy_units(
     ],
 )
 async def test_cost_sensor_handle_price_units(
-    hass, hass_storage, setup_integration, price_unit, factor
+    setup_integration, hass, hass_storage, price_unit, factor
 ) -> None:
     """Test energy cost price from sensor entity."""
     energy_attributes = {
@@ -834,7 +834,7 @@ async def test_cost_sensor_handle_price_units(
 
 @pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS))
 async def test_cost_sensor_handle_gas(
-    hass, hass_storage, setup_integration, unit
+    setup_integration, hass, hass_storage, unit
 ) -> None:
     """Test gas cost price from sensor entity."""
     energy_attributes = {
@@ -884,7 +884,7 @@ async def test_cost_sensor_handle_gas(
 
 
 async def test_cost_sensor_handle_gas_kwh(
-    hass, hass_storage, setup_integration
+    setup_integration, hass, hass_storage
 ) -> None:
     """Test gas cost price from sensor entity."""
     energy_attributes = {
@@ -935,7 +935,7 @@ async def test_cost_sensor_handle_gas_kwh(
 
 @pytest.mark.parametrize("state_class", [None])
 async def test_cost_sensor_wrong_state_class(
-    hass, hass_storage, setup_integration, caplog, state_class
+    setup_integration, hass, hass_storage, caplog, state_class
 ) -> None:
     """Test energy sensor rejects sensor with wrong state_class."""
     energy_attributes = {
@@ -996,7 +996,7 @@ async def test_cost_sensor_wrong_state_class(
 
 @pytest.mark.parametrize("state_class", [SensorStateClass.MEASUREMENT])
 async def test_cost_sensor_state_class_measurement_no_reset(
-    hass, hass_storage, setup_integration, caplog, state_class
+    setup_integration, hass, hass_storage, caplog, state_class
 ) -> None:
     """Test energy sensor rejects state_class measurement with no last_reset."""
     energy_attributes = {
@@ -1051,7 +1051,7 @@ async def test_cost_sensor_state_class_measurement_no_reset(
     assert state.state == STATE_UNKNOWN
 
 
-async def test_inherit_source_unique_id(hass, hass_storage, setup_integration):
+async def test_inherit_source_unique_id(setup_integration, hass, hass_storage):
     """Test sensor inherits unique ID from source."""
     energy_data = data.EnergyManager.default_preferences()
     energy_data["energy_sources"].append(
diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py
index 9e78e91f7f7..a6d1578c789 100644
--- a/tests/components/energy/test_validate.py
+++ b/tests/components/energy/test_validate.py
@@ -48,7 +48,7 @@ def mock_get_metadata():
 
 
 @pytest.fixture(autouse=True)
-async def mock_energy_manager(hass, recorder_mock):
+async def mock_energy_manager(recorder_mock, hass):
     """Set up energy."""
     assert await async_setup_component(hass, "energy", {"energy": {}})
     manager = await async_get_manager(hass)
diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py
index 343e814f3a8..536077d6b15 100644
--- a/tests/components/energy/test_websocket_api.py
+++ b/tests/components/energy/test_websocket_api.py
@@ -16,7 +16,7 @@ from tests.components.recorder.common import (
 
 
 @pytest.fixture(autouse=True)
-async def setup_integration(hass, recorder_mock):
+async def setup_integration(recorder_mock, hass):
     """Set up the integration."""
     assert await async_setup_component(hass, "energy", {})
 
@@ -289,7 +289,7 @@ async def test_get_solar_forecast(hass, hass_ws_client, mock_energy_platform) ->
 
 
 @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00")
-async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_mock):
+async def test_fossil_energy_consumption_no_co2(recorder_mock, hass, hass_ws_client):
     """Test fossil_energy_consumption when co2 data is missing."""
     now = dt_util.utcnow()
     later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00"))
@@ -450,7 +450,7 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m
 
 
 @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00")
-async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_mock):
+async def test_fossil_energy_consumption_hole(recorder_mock, hass, hass_ws_client):
     """Test fossil_energy_consumption when some data points lack sum."""
     now = dt_util.utcnow()
     later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00"))
@@ -611,7 +611,7 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc
 
 
 @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00")
-async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_mock):
+async def test_fossil_energy_consumption_no_data(recorder_mock, hass, hass_ws_client):
     """Test fossil_energy_consumption when there is no data."""
     now = dt_util.utcnow()
     later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00"))
@@ -759,7 +759,7 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_
 
 
 @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00")
-async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock):
+async def test_fossil_energy_consumption(recorder_mock, hass, hass_ws_client):
     """Test fossil_energy_consumption with co2 sensor data."""
     now = dt_util.utcnow()
     later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00"))
diff --git a/tests/components/fan/test_recorder.py b/tests/components/fan/test_recorder.py
index 604f5e3a2e9..9a4dc14685a 100644
--- a/tests/components/fan/test_recorder.py
+++ b/tests/components/fan/test_recorder.py
@@ -16,7 +16,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test fan registered attributes to be excluded."""
     await async_setup_component(hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "demo"}})
     await hass.async_block_till_done()
diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py
index e6c42d370e6..b440ac7889b 100644
--- a/tests/components/filter/test_sensor.py
+++ b/tests/components/filter/test_sensor.py
@@ -59,7 +59,7 @@ async def test_setup_fail(hass):
         await hass.async_block_till_done()
 
 
-async def test_chain(hass, recorder_mock, values):
+async def test_chain(recorder_mock, hass, values):
     """Test if filter chaining works."""
     config = {
         "sensor": {
@@ -87,7 +87,7 @@ async def test_chain(hass, recorder_mock, values):
 
 
 @pytest.mark.parametrize("missing", (True, False))
-async def test_chain_history(hass, recorder_mock, values, missing):
+async def test_chain_history(recorder_mock, hass, values, missing):
     """Test if filter chaining works, when a source is and isn't recorded."""
     config = {
         "sensor": {
@@ -141,7 +141,7 @@ async def test_chain_history(hass, recorder_mock, values, missing):
             assert state.state == "17.05"
 
 
-async def test_source_state_none(hass, recorder_mock, values):
+async def test_source_state_none(recorder_mock, hass, values):
     """Test is source sensor state is null and sets state to STATE_UNKNOWN."""
 
     config = {
@@ -201,7 +201,7 @@ async def test_source_state_none(hass, recorder_mock, values):
     assert state.state == STATE_UNKNOWN
 
 
-async def test_history_time(hass, recorder_mock):
+async def test_history_time(recorder_mock, hass):
     """Test loading from history based on a time window."""
     config = {
         "sensor": {
@@ -239,7 +239,7 @@ async def test_history_time(hass, recorder_mock):
         assert state.state == "18.0"
 
 
-async def test_setup(hass, recorder_mock):
+async def test_setup(recorder_mock, hass):
     """Test if filter attributes are inherited."""
     config = {
         "sensor": {
@@ -280,7 +280,7 @@ async def test_setup(hass, recorder_mock):
         assert entity_id == "sensor.test"
 
 
-async def test_invalid_state(hass, recorder_mock):
+async def test_invalid_state(recorder_mock, hass):
     """Test if filter attributes are inherited."""
     config = {
         "sensor": {
@@ -310,7 +310,7 @@ async def test_invalid_state(hass, recorder_mock):
         assert state.state == STATE_UNAVAILABLE
 
 
-async def test_timestamp_state(hass, recorder_mock):
+async def test_timestamp_state(recorder_mock, hass):
     """Test if filter state is a datetime."""
     config = {
         "sensor": {
@@ -469,7 +469,7 @@ def test_time_sma(values):
     assert filtered.state == 21.5
 
 
-async def test_reload(hass, recorder_mock):
+async def test_reload(recorder_mock, hass):
     """Verify we can reload filter sensors."""
     hass.states.async_set("sensor.test_monitored", 12345)
     await async_setup_component(
diff --git a/tests/components/group/test_recorder.py b/tests/components/group/test_recorder.py
index 7a4a41839ef..0d89bd9a1e0 100644
--- a/tests/components/group/test_recorder.py
+++ b/tests/components/group/test_recorder.py
@@ -16,7 +16,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test number registered attributes to be excluded."""
     hass.states.async_set("light.bowl", STATE_ON)
 
diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py
index 5441722c9d7..981ff3bc08d 100644
--- a/tests/components/history/test_init.py
+++ b/tests/components/history/test_init.py
@@ -587,7 +587,7 @@ def record_states(hass):
     return zero, four, states
 
 
-async def test_fetch_period_api(hass, hass_client, recorder_mock):
+async def test_fetch_period_api(recorder_mock, hass, hass_client):
     """Test the fetch period view for history."""
     await async_setup_component(hass, "history", {})
     client = await hass_client()
@@ -596,7 +596,7 @@ async def test_fetch_period_api(hass, hass_client, recorder_mock):
 
 
 async def test_fetch_period_api_with_use_include_order(
-    hass, hass_client, recorder_mock
+    recorder_mock, hass, hass_client
 ):
     """Test the fetch period view for history with include order."""
     await async_setup_component(
@@ -607,7 +607,7 @@ async def test_fetch_period_api_with_use_include_order(
     assert response.status == HTTPStatus.OK
 
 
-async def test_fetch_period_api_with_minimal_response(hass, recorder_mock, hass_client):
+async def test_fetch_period_api_with_minimal_response(recorder_mock, hass, hass_client):
     """Test the fetch period view for history with minimal_response."""
     now = dt_util.utcnow()
     await async_setup_component(hass, "history", {})
@@ -647,7 +647,7 @@ async def test_fetch_period_api_with_minimal_response(hass, recorder_mock, hass_
     ).replace('"', "")
 
 
-async def test_fetch_period_api_with_no_timestamp(hass, hass_client, recorder_mock):
+async def test_fetch_period_api_with_no_timestamp(recorder_mock, hass, hass_client):
     """Test the fetch period view for history with no timestamp."""
     await async_setup_component(hass, "history", {})
     client = await hass_client()
@@ -655,7 +655,7 @@ async def test_fetch_period_api_with_no_timestamp(hass, hass_client, recorder_mo
     assert response.status == HTTPStatus.OK
 
 
-async def test_fetch_period_api_with_include_order(hass, hass_client, recorder_mock):
+async def test_fetch_period_api_with_include_order(recorder_mock, hass, hass_client):
     """Test the fetch period view for history."""
     await async_setup_component(
         hass,
@@ -676,7 +676,7 @@ async def test_fetch_period_api_with_include_order(hass, hass_client, recorder_m
 
 
 async def test_fetch_period_api_with_entity_glob_include(
-    hass, hass_client, recorder_mock
+    recorder_mock, hass, hass_client
 ):
     """Test the fetch period view for history."""
     await async_setup_component(
@@ -704,7 +704,7 @@ async def test_fetch_period_api_with_entity_glob_include(
 
 
 async def test_fetch_period_api_with_entity_glob_exclude(
-    hass, hass_client, recorder_mock
+    recorder_mock, hass, hass_client
 ):
     """Test the fetch period view for history."""
     await async_setup_component(
@@ -744,7 +744,7 @@ async def test_fetch_period_api_with_entity_glob_exclude(
 
 
 async def test_fetch_period_api_with_entity_glob_include_and_exclude(
-    hass, hass_client, recorder_mock
+    recorder_mock, hass, hass_client
 ):
     """Test the fetch period view for history."""
     await async_setup_component(
@@ -786,7 +786,7 @@ async def test_fetch_period_api_with_entity_glob_include_and_exclude(
     assert response_json[3][0]["entity_id"] == "switch.match"
 
 
-async def test_entity_ids_limit_via_api(hass, hass_client, recorder_mock):
+async def test_entity_ids_limit_via_api(recorder_mock, hass, hass_client):
     """Test limiting history to entity_ids."""
     await async_setup_component(
         hass,
@@ -811,7 +811,7 @@ async def test_entity_ids_limit_via_api(hass, hass_client, recorder_mock):
 
 
 async def test_entity_ids_limit_via_api_with_skip_initial_state(
-    hass, hass_client, recorder_mock
+    recorder_mock, hass, hass_client
 ):
     """Test limiting history to entity_ids with skip_initial_state."""
     await async_setup_component(
@@ -844,7 +844,7 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(
     assert response_json[1][0]["entity_id"] == "light.cow"
 
 
-async def test_statistics_during_period(hass, hass_ws_client, recorder_mock, caplog):
+async def test_statistics_during_period(recorder_mock, hass, hass_ws_client, caplog):
     """Test history/statistics_during_period forwards to recorder."""
     now = dt_util.utcnow()
     await async_setup_component(hass, "history", {})
@@ -889,7 +889,7 @@ async def test_statistics_during_period(hass, hass_ws_client, recorder_mock, cap
         ws_mock.assert_awaited_once()
 
 
-async def test_list_statistic_ids(hass, hass_ws_client, recorder_mock, caplog):
+async def test_list_statistic_ids(recorder_mock, hass, hass_ws_client, caplog):
     """Test history/list_statistic_ids forwards to recorder."""
     await async_setup_component(hass, "history", {})
     client = await hass_ws_client()
@@ -914,7 +914,7 @@ async def test_list_statistic_ids(hass, hass_ws_client, recorder_mock, caplog):
         ws_mock.assert_called_once()
 
 
-async def test_history_during_period(hass, hass_ws_client, recorder_mock):
+async def test_history_during_period(recorder_mock, hass, hass_ws_client):
     """Test history_during_period."""
     now = dt_util.utcnow()
 
@@ -1047,7 +1047,7 @@ async def test_history_during_period(hass, hass_ws_client, recorder_mock):
 
 
 async def test_history_during_period_impossible_conditions(
-    hass, hass_ws_client, recorder_mock
+    recorder_mock, hass, hass_ws_client
 ):
     """Test history_during_period returns when condition cannot be true."""
     await async_setup_component(hass, "history", {})
@@ -1109,7 +1109,7 @@ async def test_history_during_period_impossible_conditions(
     "time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"]
 )
 async def test_history_during_period_significant_domain(
-    time_zone, hass, hass_ws_client, recorder_mock
+    time_zone, recorder_mock, hass, hass_ws_client
 ):
     """Test history_during_period with climate domain."""
     hass.config.set_time_zone(time_zone)
@@ -1274,7 +1274,7 @@ async def test_history_during_period_significant_domain(
 
 
 async def test_history_during_period_bad_start_time(
-    hass, hass_ws_client, recorder_mock
+    recorder_mock, hass, hass_ws_client
 ):
     """Test history_during_period bad state time."""
     await async_setup_component(
@@ -1296,7 +1296,7 @@ async def test_history_during_period_bad_start_time(
     assert response["error"]["code"] == "invalid_start_time"
 
 
-async def test_history_during_period_bad_end_time(hass, hass_ws_client, recorder_mock):
+async def test_history_during_period_bad_end_time(recorder_mock, hass, hass_ws_client):
     """Test history_during_period bad end time."""
     now = dt_util.utcnow()
 
@@ -1321,7 +1321,7 @@ async def test_history_during_period_bad_end_time(hass, hass_ws_client, recorder
 
 
 async def test_history_during_period_with_use_include_order(
-    hass, hass_ws_client, recorder_mock
+    recorder_mock, hass, hass_ws_client
 ):
     """Test history_during_period."""
     now = dt_util.utcnow()
diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py
index 5de74f71d1e..b384b7c730b 100644
--- a/tests/components/history_stats/test_sensor.py
+++ b/tests/components/history_stats/test_sensor.py
@@ -136,7 +136,7 @@ class TestHistoryStatsSensor(unittest.TestCase):
         self.hass.start()
 
 
-async def test_invalid_date_for_start(hass, recorder_mock):
+async def test_invalid_date_for_start(recorder_mock, hass):
     """Verify with an invalid date for start."""
     await async_setup_component(
         hass,
@@ -161,7 +161,7 @@ async def test_invalid_date_for_start(hass, recorder_mock):
     assert hass.states.get("sensor.test") is None
 
 
-async def test_invalid_date_for_end(hass, recorder_mock):
+async def test_invalid_date_for_end(recorder_mock, hass):
     """Verify with an invalid date for end."""
     await async_setup_component(
         hass,
@@ -186,7 +186,7 @@ async def test_invalid_date_for_end(hass, recorder_mock):
     assert hass.states.get("sensor.test") is None
 
 
-async def test_invalid_entity_in_template(hass, recorder_mock):
+async def test_invalid_entity_in_template(recorder_mock, hass):
     """Verify with an invalid entity in the template."""
     await async_setup_component(
         hass,
@@ -211,7 +211,7 @@ async def test_invalid_entity_in_template(hass, recorder_mock):
     assert hass.states.get("sensor.test") is None
 
 
-async def test_invalid_entity_returning_none_in_template(hass, recorder_mock):
+async def test_invalid_entity_returning_none_in_template(recorder_mock, hass):
     """Verify with an invalid entity returning none in the template."""
     await async_setup_component(
         hass,
@@ -236,7 +236,7 @@ async def test_invalid_entity_returning_none_in_template(hass, recorder_mock):
     assert hass.states.get("sensor.test") is None
 
 
-async def test_reload(hass, recorder_mock):
+async def test_reload(recorder_mock, hass):
     """Verify we can reload history_stats sensors."""
     hass.state = ha.CoreState.not_running
     hass.states.async_set("binary_sensor.test_id", "on")
@@ -279,7 +279,7 @@ async def test_reload(hass, recorder_mock):
     assert hass.states.get("sensor.second_test")
 
 
-async def test_measure_multiple(hass, recorder_mock):
+async def test_measure_multiple(recorder_mock, hass):
     """Test the history statistics sensor measure for multiple ."""
     start_time = dt_util.utcnow() - timedelta(minutes=60)
     t0 = start_time + timedelta(minutes=20)
@@ -361,7 +361,7 @@ async def test_measure_multiple(hass, recorder_mock):
     assert hass.states.get("sensor.sensor4").state == "50.0"
 
 
-async def test_measure(hass, recorder_mock):
+async def test_measure(recorder_mock, hass):
     """Test the history statistics sensor measure."""
     start_time = dt_util.utcnow() - timedelta(minutes=60)
     t0 = start_time + timedelta(minutes=20)
@@ -440,7 +440,7 @@ async def test_measure(hass, recorder_mock):
     assert hass.states.get("sensor.sensor4").state == "83.3"
 
 
-async def test_async_on_entire_period(hass, recorder_mock):
+async def test_async_on_entire_period(recorder_mock, hass):
     """Test the history statistics sensor measuring as on the entire period."""
     start_time = dt_util.utcnow() - timedelta(minutes=60)
     t0 = start_time + timedelta(minutes=20)
@@ -520,7 +520,7 @@ async def test_async_on_entire_period(hass, recorder_mock):
     assert hass.states.get("sensor.on_sensor4").state == "100.0"
 
 
-async def test_async_off_entire_period(hass, recorder_mock):
+async def test_async_off_entire_period(recorder_mock, hass):
     """Test the history statistics sensor measuring as off the entire period."""
     start_time = dt_util.utcnow() - timedelta(minutes=60)
     t0 = start_time + timedelta(minutes=20)
@@ -602,8 +602,8 @@ async def test_async_off_entire_period(hass, recorder_mock):
 
 
 async def test_async_start_from_history_and_switch_to_watching_state_changes_single(
-    hass,
     recorder_mock,
+    hass,
 ):
     """Test we startup from history and switch to watching state changes."""
     hass.config.set_time_zone("UTC")
@@ -702,8 +702,8 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin
 
 
 async def test_async_start_from_history_and_switch_to_watching_state_changes_single_expanding_window(
-    hass,
     recorder_mock,
+    hass,
 ):
     """Test we startup from history and switch to watching state changes with an expanding end time."""
     hass.config.set_time_zone("UTC")
@@ -801,8 +801,8 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin
 
 
 async def test_async_start_from_history_and_switch_to_watching_state_changes_multiple(
-    hass,
     recorder_mock,
+    hass,
 ):
     """Test we startup from history and switch to watching state changes."""
     hass.config.set_time_zone("UTC")
@@ -938,7 +938,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul
     assert hass.states.get("sensor.sensor4").state == "87.5"
 
 
-async def test_does_not_work_into_the_future(hass, recorder_mock):
+async def test_does_not_work_into_the_future(recorder_mock, hass):
     """Test history cannot tell the future.
 
     Verifies we do not regress https://github.com/home-assistant/core/pull/20589
@@ -1078,7 +1078,7 @@ async def test_does_not_work_into_the_future(hass, recorder_mock):
     assert hass.states.get("sensor.sensor1").state == "0.0"
 
 
-async def test_reload_before_start_event(hass, recorder_mock):
+async def test_reload_before_start_event(recorder_mock, hass):
     """Verify we can reload history_stats sensors before the start event."""
     hass.state = ha.CoreState.not_running
     hass.states.async_set("binary_sensor.test_id", "on")
@@ -1119,7 +1119,7 @@ async def test_reload_before_start_event(hass, recorder_mock):
     assert hass.states.get("sensor.second_test")
 
 
-async def test_measure_sliding_window(hass, recorder_mock):
+async def test_measure_sliding_window(recorder_mock, hass):
     """Test the history statistics sensor with a moving end and a moving start."""
     start_time = dt_util.utcnow() - timedelta(minutes=60)
     t0 = start_time + timedelta(minutes=20)
@@ -1212,7 +1212,7 @@ async def test_measure_sliding_window(hass, recorder_mock):
     assert hass.states.get("sensor.sensor4").state == "41.7"
 
 
-async def test_measure_from_end_going_backwards(hass, recorder_mock):
+async def test_measure_from_end_going_backwards(recorder_mock, hass):
     """Test the history statistics sensor with a moving end and a duration to find the start."""
     start_time = dt_util.utcnow() - timedelta(minutes=60)
     t0 = start_time + timedelta(minutes=20)
@@ -1304,7 +1304,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock):
     assert hass.states.get("sensor.sensor4").state == "83.3"
 
 
-async def test_measure_cet(hass, recorder_mock):
+async def test_measure_cet(recorder_mock, hass):
     """Test the history statistics sensor measure with a non-UTC timezone."""
     hass.config.set_time_zone("Europe/Berlin")
     start_time = dt_util.utcnow() - timedelta(minutes=60)
@@ -1385,7 +1385,7 @@ async def test_measure_cet(hass, recorder_mock):
 
 
 @pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"])
-async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock):
+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)
@@ -1498,7 +1498,7 @@ async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock)
         assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0"
 
 
-async def test_device_classes(hass, recorder_mock):
+async def test_device_classes(recorder_mock, hass):
     """Test the device classes."""
     await async_setup_component(
         hass,
diff --git a/tests/components/humidifier/test_recorder.py b/tests/components/humidifier/test_recorder.py
index 28859e6133f..16f3b136180 100644
--- a/tests/components/humidifier/test_recorder.py
+++ b/tests/components/humidifier/test_recorder.py
@@ -20,7 +20,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test humidifier registered attributes to be excluded."""
     await async_setup_component(
         hass, humidifier.DOMAIN, {humidifier.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/input_boolean/test_recorder.py b/tests/components/input_boolean/test_recorder.py
index e7f68379343..68573866965 100644
--- a/tests/components/input_boolean/test_recorder.py
+++ b/tests/components/input_boolean/test_recorder.py
@@ -16,7 +16,7 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant, recorder_mock, enable_custom_integrations: None
+    recorder_mock, hass: HomeAssistant, enable_custom_integrations: None
 ):
     """Test attributes to be excluded."""
     assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}})
diff --git a/tests/components/input_button/test_recorder.py b/tests/components/input_button/test_recorder.py
index e469536549a..53b2b9615f6 100644
--- a/tests/components/input_button/test_recorder.py
+++ b/tests/components/input_button/test_recorder.py
@@ -16,7 +16,7 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant, recorder_mock, enable_custom_integrations: None
+    recorder_mock, hass: HomeAssistant, enable_custom_integrations: None
 ):
     """Test attributes to be excluded."""
     assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}})
diff --git a/tests/components/input_datetime/test_recorder.py b/tests/components/input_datetime/test_recorder.py
index bbdd0446e56..d5c95d5951f 100644
--- a/tests/components/input_datetime/test_recorder.py
+++ b/tests/components/input_datetime/test_recorder.py
@@ -16,7 +16,7 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant, recorder_mock, enable_custom_integrations: None
+    recorder_mock, hass: HomeAssistant, enable_custom_integrations: None
 ):
     """Test attributes to be excluded."""
     assert await async_setup_component(
diff --git a/tests/components/input_number/test_recorder.py b/tests/components/input_number/test_recorder.py
index f736d450e7a..325234e069b 100644
--- a/tests/components/input_number/test_recorder.py
+++ b/tests/components/input_number/test_recorder.py
@@ -22,7 +22,7 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant, recorder_mock, enable_custom_integrations: None
+    recorder_mock, hass: HomeAssistant, enable_custom_integrations: None
 ):
     """Test attributes to be excluded."""
     assert await async_setup_component(
diff --git a/tests/components/input_select/test_recorder.py b/tests/components/input_select/test_recorder.py
index 2931132bafc..457fc0feccc 100644
--- a/tests/components/input_select/test_recorder.py
+++ b/tests/components/input_select/test_recorder.py
@@ -16,7 +16,7 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant, recorder_mock, enable_custom_integrations: None
+    recorder_mock, hass: HomeAssistant, enable_custom_integrations: None
 ):
     """Test attributes to be excluded."""
     assert await async_setup_component(
diff --git a/tests/components/input_text/test_recorder.py b/tests/components/input_text/test_recorder.py
index 928399cd939..9557f656465 100644
--- a/tests/components/input_text/test_recorder.py
+++ b/tests/components/input_text/test_recorder.py
@@ -23,7 +23,7 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant, recorder_mock, enable_custom_integrations: None
+    recorder_mock, hass: HomeAssistant, enable_custom_integrations: None
 ):
     """Test attributes to be excluded."""
     assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"test": {}}})
diff --git a/tests/components/light/test_recorder.py b/tests/components/light/test_recorder.py
index b6d26306317..0ff092545f4 100644
--- a/tests/components/light/test_recorder.py
+++ b/tests/components/light/test_recorder.py
@@ -21,7 +21,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test light registered attributes to be excluded."""
     await async_setup_component(
         hass, light.DOMAIN, {light.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py
index 31ca1610250..fb7ac217867 100644
--- a/tests/components/logbook/test_init.py
+++ b/tests/components/logbook/test_init.py
@@ -58,7 +58,7 @@ EMPTY_CONFIG = logbook.CONFIG_SCHEMA({logbook.DOMAIN: {}})
 
 
 @pytest.fixture
-async def hass_(hass, recorder_mock):
+async def hass_(recorder_mock, hass):
     """Set up things to be run when tests are started."""
     assert await async_setup_component(hass, logbook.DOMAIN, EMPTY_CONFIG)
     return hass
@@ -123,7 +123,7 @@ async def test_service_call_create_logbook_entry(hass_):
     assert last_call.data.get(logbook.ATTR_DOMAIN) == "logbook"
 
 
-async def test_service_call_create_logbook_entry_invalid_entity_id(hass, recorder_mock):
+async def test_service_call_create_logbook_entry_invalid_entity_id(recorder_mock, hass):
     """Test if service call create log book entry with an invalid entity id."""
     await async_setup_component(hass, "logbook", {})
     await hass.async_block_till_done()
@@ -352,7 +352,7 @@ def create_state_changed_event_from_old_new(
     return LazyEventPartialState(row, {})
 
 
-async def test_logbook_view(hass, hass_client, recorder_mock):
+async def test_logbook_view(recorder_mock, hass, hass_client):
     """Test the logbook view."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -361,7 +361,7 @@ async def test_logbook_view(hass, hass_client, recorder_mock):
     assert response.status == HTTPStatus.OK
 
 
-async def test_logbook_view_invalid_start_date_time(hass, hass_client, recorder_mock):
+async def test_logbook_view_invalid_start_date_time(recorder_mock, hass, hass_client):
     """Test the logbook view with an invalid date time."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -370,7 +370,7 @@ async def test_logbook_view_invalid_start_date_time(hass, hass_client, recorder_
     assert response.status == HTTPStatus.BAD_REQUEST
 
 
-async def test_logbook_view_invalid_end_date_time(hass, hass_client, recorder_mock):
+async def test_logbook_view_invalid_end_date_time(recorder_mock, hass, hass_client):
     """Test the logbook view."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -381,7 +381,7 @@ async def test_logbook_view_invalid_end_date_time(hass, hass_client, recorder_mo
     assert response.status == HTTPStatus.BAD_REQUEST
 
 
-async def test_logbook_view_period_entity(hass, hass_client, recorder_mock, set_utc):
+async def test_logbook_view_period_entity(recorder_mock, hass, hass_client, set_utc):
     """Test the logbook view with period and entity."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -462,7 +462,7 @@ async def test_logbook_view_period_entity(hass, hass_client, recorder_mock, set_
     assert response_json[0]["entity_id"] == entity_id_test
 
 
-async def test_logbook_describe_event(hass, hass_client, recorder_mock):
+async def test_logbook_describe_event(recorder_mock, hass, hass_client):
     """Test teaching logbook about a new event."""
 
     def _describe(event):
@@ -498,7 +498,7 @@ async def test_logbook_describe_event(hass, hass_client, recorder_mock):
     assert event["domain"] == "test_domain"
 
 
-async def test_exclude_described_event(hass, hass_client, recorder_mock):
+async def test_exclude_described_event(recorder_mock, hass, hass_client):
     """Test exclusions of events that are described by another integration."""
     name = "My Automation Rule"
     entity_id = "automation.excluded_rule"
@@ -561,7 +561,7 @@ async def test_exclude_described_event(hass, hass_client, recorder_mock):
     assert event["entity_id"] == "automation.included_rule"
 
 
-async def test_logbook_view_end_time_entity(hass, hass_client, recorder_mock):
+async def test_logbook_view_end_time_entity(recorder_mock, hass, hass_client):
     """Test the logbook view with end_time and entity."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -616,7 +616,7 @@ async def test_logbook_view_end_time_entity(hass, hass_client, recorder_mock):
     assert response_json[0]["entity_id"] == entity_id_test
 
 
-async def test_logbook_entity_filter_with_automations(hass, hass_client, recorder_mock):
+async def test_logbook_entity_filter_with_automations(recorder_mock, hass, hass_client):
     """Test the logbook view with end_time and entity with automations and scripts."""
     await asyncio.gather(
         *[
@@ -692,7 +692,7 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client, recorde
 
 
 async def test_logbook_entity_no_longer_in_state_machine(
-    hass, hass_client, recorder_mock
+    recorder_mock, hass, hass_client
 ):
     """Test the logbook view with an entity that hass been removed from the state machine."""
     await async_setup_component(hass, "logbook", {})
@@ -730,7 +730,7 @@ async def test_logbook_entity_no_longer_in_state_machine(
 
 
 async def test_filter_continuous_sensor_values(
-    hass, hass_client, recorder_mock, set_utc
+    recorder_mock, hass, hass_client, set_utc
 ):
     """Test remove continuous sensor events from logbook."""
     await async_setup_component(hass, "logbook", {})
@@ -770,7 +770,7 @@ async def test_filter_continuous_sensor_values(
     assert response_json[1]["entity_id"] == entity_id_third
 
 
-async def test_exclude_new_entities(hass, hass_client, recorder_mock, set_utc):
+async def test_exclude_new_entities(recorder_mock, hass, hass_client, set_utc):
     """Test if events are excluded on first update."""
     await asyncio.gather(
         *[
@@ -807,7 +807,7 @@ async def test_exclude_new_entities(hass, hass_client, recorder_mock, set_utc):
     assert response_json[1]["message"] == "started"
 
 
-async def test_exclude_removed_entities(hass, hass_client, recorder_mock, set_utc):
+async def test_exclude_removed_entities(recorder_mock, hass, hass_client, set_utc):
     """Test if events are excluded on last update."""
     await asyncio.gather(
         *[
@@ -851,7 +851,7 @@ async def test_exclude_removed_entities(hass, hass_client, recorder_mock, set_ut
     assert response_json[2]["entity_id"] == entity_id2
 
 
-async def test_exclude_attribute_changes(hass, hass_client, recorder_mock, set_utc):
+async def test_exclude_attribute_changes(recorder_mock, hass, hass_client, set_utc):
     """Test if events of attribute changes are filtered."""
     await asyncio.gather(
         *[
@@ -891,7 +891,7 @@ async def test_exclude_attribute_changes(hass, hass_client, recorder_mock, set_u
     assert response_json[2]["entity_id"] == "light.kitchen"
 
 
-async def test_logbook_entity_context_id(hass, recorder_mock, hass_client):
+async def test_logbook_entity_context_id(recorder_mock, hass, hass_client):
     """Test the logbook view with end_time and entity with automations and scripts."""
     await asyncio.gather(
         *[
@@ -1042,7 +1042,7 @@ async def test_logbook_entity_context_id(hass, recorder_mock, hass_client):
 
 
 async def test_logbook_context_id_automation_script_started_manually(
-    hass, recorder_mock, hass_client
+    recorder_mock, hass, hass_client
 ):
     """Test the logbook populates context_ids for scripts and automations started manually."""
     await asyncio.gather(
@@ -1132,7 +1132,7 @@ async def test_logbook_context_id_automation_script_started_manually(
     assert json_dict[4]["context_domain"] == "script"
 
 
-async def test_logbook_entity_context_parent_id(hass, hass_client, recorder_mock):
+async def test_logbook_entity_context_parent_id(recorder_mock, hass, hass_client):
     """Test the logbook view links events via context parent_id."""
     await asyncio.gather(
         *[
@@ -1311,7 +1311,7 @@ async def test_logbook_entity_context_parent_id(hass, hass_client, recorder_mock
     assert json_dict[8]["context_user_id"] == "485cacf93ef84d25a99ced3126b921d2"
 
 
-async def test_logbook_context_from_template(hass, hass_client, recorder_mock):
+async def test_logbook_context_from_template(recorder_mock, hass, hass_client):
     """Test the logbook view with end_time and entity with automations and scripts."""
     await asyncio.gather(
         *[
@@ -1398,7 +1398,7 @@ async def test_logbook_context_from_template(hass, hass_client, recorder_mock):
     assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
 
 
-async def test_logbook_(hass, hass_client, recorder_mock):
+async def test_logbook_(recorder_mock, hass, hass_client):
     """Test the logbook view with a single entity and ."""
     await async_setup_component(hass, "logbook", {})
     assert await async_setup_component(
@@ -1467,7 +1467,7 @@ async def test_logbook_(hass, hass_client, recorder_mock):
     assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
 
 
-async def test_logbook_many_entities_multiple_calls(hass, hass_client, recorder_mock):
+async def test_logbook_many_entities_multiple_calls(recorder_mock, hass, hass_client):
     """Test the logbook view with a many entities called multiple times."""
     await async_setup_component(hass, "logbook", {})
     await async_setup_component(hass, "automation", {})
@@ -1537,7 +1537,7 @@ async def test_logbook_many_entities_multiple_calls(hass, hass_client, recorder_
     assert len(json_dict) == 0
 
 
-async def test_custom_log_entry_discoverable_via_(hass, hass_client, recorder_mock):
+async def test_custom_log_entry_discoverable_via_(recorder_mock, hass, hass_client):
     """Test if a custom log entry is later discoverable via ."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -1572,7 +1572,7 @@ async def test_custom_log_entry_discoverable_via_(hass, hass_client, recorder_mo
     assert json_dict[0]["entity_id"] == "switch.test_switch"
 
 
-async def test_logbook_multiple_entities(hass, hass_client, recorder_mock):
+async def test_logbook_multiple_entities(recorder_mock, hass, hass_client):
     """Test the logbook view with a multiple entities."""
     await async_setup_component(hass, "logbook", {})
     assert await async_setup_component(
@@ -1696,7 +1696,7 @@ async def test_logbook_multiple_entities(hass, hass_client, recorder_mock):
     assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
 
 
-async def test_logbook_invalid_entity(hass, hass_client, recorder_mock):
+async def test_logbook_invalid_entity(recorder_mock, hass, hass_client):
     """Test the logbook view with requesting an invalid entity."""
     await async_setup_component(hass, "logbook", {})
     await hass.async_block_till_done()
@@ -1714,7 +1714,7 @@ async def test_logbook_invalid_entity(hass, hass_client, recorder_mock):
     assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR
 
 
-async def test_icon_and_state(hass, hass_client, recorder_mock):
+async def test_icon_and_state(recorder_mock, hass, hass_client):
     """Test to ensure state and custom icons are returned."""
     await asyncio.gather(
         *[
@@ -1757,7 +1757,7 @@ async def test_icon_and_state(hass, hass_client, recorder_mock):
     assert response_json[2]["state"] == STATE_OFF
 
 
-async def test_fire_logbook_entries(hass, hass_client, recorder_mock):
+async def test_fire_logbook_entries(recorder_mock, hass, hass_client):
     """Test many logbook entry calls."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -1793,7 +1793,7 @@ async def test_fire_logbook_entries(hass, hass_client, recorder_mock):
     assert len(response_json) == 11
 
 
-async def test_exclude_events_domain(hass, hass_client, recorder_mock):
+async def test_exclude_events_domain(recorder_mock, hass, hass_client):
     """Test if events are filtered if domain is excluded in config."""
     entity_id = "switch.bla"
     entity_id2 = "sensor.blu"
@@ -1827,7 +1827,7 @@ async def test_exclude_events_domain(hass, hass_client, recorder_mock):
     _assert_entry(entries[1], name="blu", entity_id=entity_id2)
 
 
-async def test_exclude_events_domain_glob(hass, hass_client, recorder_mock):
+async def test_exclude_events_domain_glob(recorder_mock, hass, hass_client):
     """Test if events are filtered if domain or glob is excluded in config."""
     entity_id = "switch.bla"
     entity_id2 = "sensor.blu"
@@ -1870,7 +1870,7 @@ async def test_exclude_events_domain_glob(hass, hass_client, recorder_mock):
     _assert_entry(entries[1], name="blu", entity_id=entity_id2)
 
 
-async def test_include_events_entity(hass, hass_client, recorder_mock):
+async def test_include_events_entity(recorder_mock, hass, hass_client):
     """Test if events are filtered if entity is included in config."""
     entity_id = "sensor.bla"
     entity_id2 = "sensor.blu"
@@ -1910,7 +1910,7 @@ async def test_include_events_entity(hass, hass_client, recorder_mock):
     _assert_entry(entries[1], name="blu", entity_id=entity_id2)
 
 
-async def test_exclude_events_entity(hass, hass_client, recorder_mock):
+async def test_exclude_events_entity(recorder_mock, hass, hass_client):
     """Test if events are filtered if entity is excluded in config."""
     entity_id = "sensor.bla"
     entity_id2 = "sensor.blu"
@@ -1944,7 +1944,7 @@ async def test_exclude_events_entity(hass, hass_client, recorder_mock):
     _assert_entry(entries[1], name="blu", entity_id=entity_id2)
 
 
-async def test_include_events_domain(hass, hass_client, recorder_mock):
+async def test_include_events_domain(recorder_mock, hass, hass_client):
     """Test if events are filtered if domain is included in config."""
     assert await async_setup_component(hass, "alexa", {})
     entity_id = "switch.bla"
@@ -1986,7 +1986,7 @@ async def test_include_events_domain(hass, hass_client, recorder_mock):
     _assert_entry(entries[2], name="blu", entity_id=entity_id2)
 
 
-async def test_include_events_domain_glob(hass, hass_client, recorder_mock):
+async def test_include_events_domain_glob(recorder_mock, hass, hass_client):
     """Test if events are filtered if domain or glob is included in config."""
     assert await async_setup_component(hass, "alexa", {})
     entity_id = "switch.bla"
@@ -2043,7 +2043,7 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock):
     _assert_entry(entries[3], name="included", entity_id=entity_id3)
 
 
-async def test_include_exclude_events_no_globs(hass, hass_client, recorder_mock):
+async def test_include_exclude_events_no_globs(recorder_mock, hass, hass_client):
     """Test if events are filtered if include and exclude is configured."""
     entity_id = "switch.bla"
     entity_id2 = "sensor.blu"
@@ -2100,7 +2100,7 @@ async def test_include_exclude_events_no_globs(hass, hass_client, recorder_mock)
 
 
 async def test_include_exclude_events_with_glob_filters(
-    hass, hass_client, recorder_mock
+    recorder_mock, hass, hass_client
 ):
     """Test if events are filtered if include and exclude is configured."""
     entity_id = "switch.bla"
@@ -2165,7 +2165,7 @@ async def test_include_exclude_events_with_glob_filters(
     _assert_entry(entries[6], name="included", entity_id=entity_id5, state="30")
 
 
-async def test_empty_config(hass, hass_client, recorder_mock):
+async def test_empty_config(recorder_mock, hass, hass_client):
     """Test we can handle an empty entity filter."""
     entity_id = "sensor.blu"
 
@@ -2197,7 +2197,7 @@ async def test_empty_config(hass, hass_client, recorder_mock):
     _assert_entry(entries[1], name="blu", entity_id=entity_id)
 
 
-async def test_context_filter(hass, hass_client, recorder_mock):
+async def test_context_filter(recorder_mock, hass, hass_client):
     """Test we can filter by context."""
     assert await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -2269,7 +2269,7 @@ def _assert_entry(
         assert state == entry["state"]
 
 
-async def test_get_events(hass, hass_ws_client, recorder_mock):
+async def test_get_events(recorder_mock, hass, hass_ws_client):
     """Test logbook get_events."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -2387,7 +2387,7 @@ async def test_get_events(hass, hass_ws_client, recorder_mock):
     assert isinstance(results[0]["when"], float)
 
 
-async def test_get_events_future_start_time(hass, hass_ws_client, recorder_mock):
+async def test_get_events_future_start_time(recorder_mock, hass, hass_ws_client):
     """Test get_events with a future start time."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -2410,7 +2410,7 @@ async def test_get_events_future_start_time(hass, hass_ws_client, recorder_mock)
     assert len(results) == 0
 
 
-async def test_get_events_bad_start_time(hass, hass_ws_client, recorder_mock):
+async def test_get_events_bad_start_time(recorder_mock, hass, hass_ws_client):
     """Test get_events bad start time."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -2428,7 +2428,7 @@ async def test_get_events_bad_start_time(hass, hass_ws_client, recorder_mock):
     assert response["error"]["code"] == "invalid_start_time"
 
 
-async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock):
+async def test_get_events_bad_end_time(recorder_mock, hass, hass_ws_client):
     """Test get_events bad end time."""
     now = dt_util.utcnow()
     await async_setup_component(hass, "logbook", {})
@@ -2448,7 +2448,7 @@ async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock):
     assert response["error"]["code"] == "invalid_end_time"
 
 
-async def test_get_events_invalid_filters(hass, hass_ws_client, recorder_mock):
+async def test_get_events_invalid_filters(recorder_mock, hass, hass_ws_client):
     """Test get_events invalid filters."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -2476,7 +2476,7 @@ async def test_get_events_invalid_filters(hass, hass_ws_client, recorder_mock):
     assert response["error"]["code"] == "invalid_format"
 
 
-async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock):
+async def test_get_events_with_device_ids(recorder_mock, hass, hass_ws_client):
     """Test logbook get_events for device ids."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -2613,7 +2613,7 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock):
     assert isinstance(results[3]["when"], float)
 
 
-async def test_logbook_select_entities_context_id(hass, recorder_mock, hass_client):
+async def test_logbook_select_entities_context_id(recorder_mock, hass, hass_client):
     """Test the logbook view with end_time and entity with automations and scripts."""
     await asyncio.gather(
         *[
@@ -2746,7 +2746,7 @@ async def test_logbook_select_entities_context_id(hass, recorder_mock, hass_clie
     assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474"
 
 
-async def test_get_events_with_context_state(hass, hass_ws_client, recorder_mock):
+async def test_get_events_with_context_state(recorder_mock, hass, hass_ws_client):
     """Test logbook get_events with a context state."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -2809,7 +2809,7 @@ async def test_get_events_with_context_state(hass, hass_ws_client, recorder_mock
     assert "context_event_type" not in results[3]
 
 
-async def test_logbook_with_empty_config(hass, recorder_mock):
+async def test_logbook_with_empty_config(recorder_mock, hass):
     """Test we handle a empty configuration."""
     assert await async_setup_component(
         hass,
@@ -2822,7 +2822,7 @@ async def test_logbook_with_empty_config(hass, recorder_mock):
     await hass.async_block_till_done()
 
 
-async def test_logbook_with_non_iterable_entity_filter(hass, recorder_mock):
+async def test_logbook_with_non_iterable_entity_filter(recorder_mock, hass):
     """Test we handle a non-iterable entity filter."""
     assert await async_setup_component(
         hass,
diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py
index ec27b1baceb..fb0defca93c 100644
--- a/tests/components/logbook/test_websocket_api.py
+++ b/tests/components/logbook/test_websocket_api.py
@@ -123,7 +123,7 @@ async def _async_mock_devices_with_logbook_platform(hass):
     return [device, device2]
 
 
-async def test_get_events(hass, hass_ws_client, recorder_mock):
+async def test_get_events(recorder_mock, hass, hass_ws_client):
     """Test logbook get_events."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -241,7 +241,7 @@ async def test_get_events(hass, hass_ws_client, recorder_mock):
     assert isinstance(results[0]["when"], float)
 
 
-async def test_get_events_entities_filtered_away(hass, hass_ws_client, recorder_mock):
+async def test_get_events_entities_filtered_away(recorder_mock, hass, hass_ws_client):
     """Test logbook get_events all entities filtered away."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -303,7 +303,7 @@ async def test_get_events_entities_filtered_away(hass, hass_ws_client, recorder_
     assert len(results) == 0
 
 
-async def test_get_events_future_start_time(hass, hass_ws_client, recorder_mock):
+async def test_get_events_future_start_time(recorder_mock, hass, hass_ws_client):
     """Test get_events with a future start time."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -326,7 +326,7 @@ async def test_get_events_future_start_time(hass, hass_ws_client, recorder_mock)
     assert len(results) == 0
 
 
-async def test_get_events_bad_start_time(hass, hass_ws_client, recorder_mock):
+async def test_get_events_bad_start_time(recorder_mock, hass, hass_ws_client):
     """Test get_events bad start time."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -344,7 +344,7 @@ async def test_get_events_bad_start_time(hass, hass_ws_client, recorder_mock):
     assert response["error"]["code"] == "invalid_start_time"
 
 
-async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock):
+async def test_get_events_bad_end_time(recorder_mock, hass, hass_ws_client):
     """Test get_events bad end time."""
     now = dt_util.utcnow()
     await async_setup_component(hass, "logbook", {})
@@ -364,7 +364,7 @@ async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock):
     assert response["error"]["code"] == "invalid_end_time"
 
 
-async def test_get_events_invalid_filters(hass, hass_ws_client, recorder_mock):
+async def test_get_events_invalid_filters(recorder_mock, hass, hass_ws_client):
     """Test get_events invalid filters."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -392,7 +392,7 @@ async def test_get_events_invalid_filters(hass, hass_ws_client, recorder_mock):
     assert response["error"]["code"] == "invalid_format"
 
 
-async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock):
+async def test_get_events_with_device_ids(recorder_mock, hass, hass_ws_client):
     """Test logbook get_events for device ids."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -504,7 +504,7 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock):
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream_excluded_entities(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with excluded entities."""
     now = dt_util.utcnow()
@@ -689,7 +689,7 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream_included_entities(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with included entities."""
     test_entities = (
@@ -897,7 +897,7 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream inherts filters from recorder."""
     now = dt_util.utcnow()
@@ -1088,7 +1088,7 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream."""
     now = dt_util.utcnow()
@@ -1391,7 +1391,7 @@ async def test_subscribe_unsubscribe_logbook_stream(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream_entities(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with specific entities."""
     now = dt_util.utcnow()
@@ -1489,7 +1489,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with specific entities and an end_time."""
     now = dt_util.utcnow()
@@ -1596,7 +1596,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream_entities_past_only(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with specific entities in the past."""
     now = dt_util.utcnow()
@@ -1664,7 +1664,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream_big_query(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream and ask for a large time frame.
 
@@ -1764,7 +1764,7 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_unsubscribe_logbook_stream_device(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with a device."""
     now = dt_util.utcnow()
@@ -1856,7 +1856,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device(
     assert hass.bus.async_listeners() == init_listeners
 
 
-async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock):
+async def test_event_stream_bad_start_time(recorder_mock, hass, hass_ws_client):
     """Test event_stream bad start time."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -1876,7 +1876,7 @@ async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock):
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_logbook_stream_match_multiple_entities(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test logbook stream with a described integration that uses multiple entities."""
     now = dt_util.utcnow()
@@ -1971,7 +1971,7 @@ async def test_logbook_stream_match_multiple_entities(
     assert hass.bus.async_listeners() == init_listeners
 
 
-async def test_event_stream_bad_end_time(hass, hass_ws_client, recorder_mock):
+async def test_event_stream_bad_end_time(recorder_mock, hass, hass_ws_client):
     """Test event_stream bad end time."""
     await async_setup_component(hass, "logbook", {})
     await async_recorder_block_till_done(hass)
@@ -2004,8 +2004,8 @@ async def test_event_stream_bad_end_time(hass, hass_ws_client, recorder_mock):
 
 
 async def test_live_stream_with_one_second_commit_interval(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     hass_ws_client,
 ):
     """Test the recorder with a 1s commit interval."""
@@ -2095,7 +2095,7 @@ async def test_live_stream_with_one_second_commit_interval(
 
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
-async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client):
+async def test_subscribe_disconnected(recorder_mock, hass, hass_ws_client):
     """Test subscribe/unsubscribe logbook stream gets disconnected."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -2150,7 +2150,7 @@ async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client):
 
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
-async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_client):
+async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_client):
     """Test we unsubscribe if the stream consumer fails or is canceled."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -2203,7 +2203,7 @@ async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_clie
 
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
-async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplog):
+async def test_recorder_is_far_behind(recorder_mock, hass, hass_ws_client, caplog):
     """Test we still start live streaming if the recorder is far behind."""
     now = dt_util.utcnow()
     await asyncio.gather(
@@ -2279,7 +2279,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_all_entities_are_continuous(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with entities that are always filtered."""
     now = dt_util.utcnow()
@@ -2337,7 +2337,7 @@ async def test_subscribe_all_entities_are_continuous(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_all_entities_have_uom_multiple(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test logbook stream with specific request for multiple entities that are always filtered."""
     now = dt_util.utcnow()
@@ -2394,7 +2394,7 @@ async def test_subscribe_all_entities_have_uom_multiple(
 
 @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
+    recorder_mock, hass, hass_ws_client
 ):
     """Test logbook stream with uom filtered entities and non-filtered entities."""
     now = dt_util.utcnow()
@@ -2499,7 +2499,7 @@ async def test_subscribe_entities_some_have_uom_multiple(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_logbook_stream_ignores_forced_updates(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test logbook live stream ignores forced updates."""
     now = dt_util.utcnow()
@@ -2613,7 +2613,7 @@ async def test_logbook_stream_ignores_forced_updates(
 
 @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
 async def test_subscribe_all_entities_are_continuous_with_device(
-    hass, recorder_mock, hass_ws_client
+    recorder_mock, hass, hass_ws_client
 ):
     """Test subscribe/unsubscribe logbook stream with entities that are always filtered and a device."""
     now = dt_util.utcnow()
diff --git a/tests/components/media_player/test_recorder.py b/tests/components/media_player/test_recorder.py
index dd1329be81e..34397a58c0c 100644
--- a/tests/components/media_player/test_recorder.py
+++ b/tests/components/media_player/test_recorder.py
@@ -22,7 +22,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test media_player registered attributes to be excluded."""
     await async_setup_component(
         hass, media_player.DOMAIN, {media_player.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/number/test_recorder.py b/tests/components/number/test_recorder.py
index f51d3933b5d..9713dc81d85 100644
--- a/tests/components/number/test_recorder.py
+++ b/tests/components/number/test_recorder.py
@@ -16,7 +16,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test number registered attributes to be excluded."""
     await async_setup_component(
         hass, number.DOMAIN, {number.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py
index 40d2930ce93..d143bbef00d 100644
--- a/tests/components/plant/test_init.py
+++ b/tests/components/plant/test_init.py
@@ -144,7 +144,7 @@ async def test_state_problem_if_unavailable(hass):
     assert state.attributes[plant.READING_MOISTURE] == STATE_UNAVAILABLE
 
 
-async def test_load_from_db(hass, recorder_mock):
+async def test_load_from_db(recorder_mock, hass):
     """Test bootstrapping the brightness history from the database.
 
     This test can should only be executed if the loading of the history
diff --git a/tests/components/recorder/test_backup.py b/tests/components/recorder/test_backup.py
index e829c2aa13b..511520faa71 100644
--- a/tests/components/recorder/test_backup.py
+++ b/tests/components/recorder/test_backup.py
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import HomeAssistantError
 
 
-async def test_async_pre_backup(hass: HomeAssistant, recorder_mock) -> None:
+async def test_async_pre_backup(recorder_mock, hass: HomeAssistant) -> None:
     """Test pre backup."""
     with patch(
         "homeassistant.components.recorder.core.Recorder.lock_database"
@@ -20,7 +20,7 @@ async def test_async_pre_backup(hass: HomeAssistant, recorder_mock) -> None:
 
 
 async def test_async_pre_backup_with_timeout(
-    hass: HomeAssistant, recorder_mock
+    recorder_mock, hass: HomeAssistant
 ) -> None:
     """Test pre backup with timeout."""
     with patch(
@@ -32,7 +32,7 @@ async def test_async_pre_backup_with_timeout(
 
 
 async def test_async_pre_backup_with_migration(
-    hass: HomeAssistant, recorder_mock
+    recorder_mock, hass: HomeAssistant
 ) -> None:
     """Test pre backup with migration."""
     with patch(
@@ -42,7 +42,7 @@ async def test_async_pre_backup_with_migration(
         await async_pre_backup(hass)
 
 
-async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None:
+async def test_async_post_backup(recorder_mock, hass: HomeAssistant) -> None:
     """Test post backup."""
     with patch(
         "homeassistant.components.recorder.core.Recorder.unlock_database"
@@ -51,7 +51,7 @@ async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None:
         assert unlock_mock.called
 
 
-async def test_async_post_backup_failure(hass: HomeAssistant, recorder_mock) -> None:
+async def test_async_post_backup_failure(recorder_mock, hass: HomeAssistant) -> None:
     """Test post backup failure."""
     with patch(
         "homeassistant.components.recorder.core.Recorder.unlock_database",
diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py
index 62bb1b3fa8d..89a271dac02 100644
--- a/tests/components/recorder/test_filters_with_entityfilter.py
+++ b/tests/components/recorder/test_filters_with_entityfilter.py
@@ -71,7 +71,7 @@ async def _async_get_states_and_events_with_filter(
     return filtered_states_entity_ids, filtered_events_entity_ids
 
 
-async def test_included_and_excluded_simple_case_no_domains(hass, recorder_mock):
+async def test_included_and_excluded_simple_case_no_domains(recorder_mock, hass):
     """Test filters with included and excluded without domains."""
     filter_accept = {"sensor.kitchen4", "switch.kitchen"}
     filter_reject = {
@@ -127,7 +127,7 @@ async def test_included_and_excluded_simple_case_no_domains(hass, recorder_mock)
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_included_and_excluded_simple_case_no_globs(hass, recorder_mock):
+async def test_included_and_excluded_simple_case_no_globs(recorder_mock, hass):
     """Test filters with included and excluded without globs."""
     filter_accept = {"switch.bla", "sensor.blu", "sensor.keep"}
     filter_reject = {"sensor.bli"}
@@ -168,7 +168,7 @@ async def test_included_and_excluded_simple_case_no_globs(hass, recorder_mock):
 
 
 async def test_included_and_excluded_simple_case_without_underscores(
-    hass, recorder_mock
+    recorder_mock, hass
 ):
     """Test filters with included and excluded without underscores."""
     filter_accept = {"light.any", "sensor.kitchen4", "switch.kitchen"}
@@ -221,7 +221,7 @@ async def test_included_and_excluded_simple_case_without_underscores(
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_included_and_excluded_simple_case_with_underscores(hass, recorder_mock):
+async def test_included_and_excluded_simple_case_with_underscores(recorder_mock, hass):
     """Test filters with included and excluded with underscores."""
     filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"}
     filter_reject = {"switch.other", "cover.any", "sensor.weather_5", "light.kitchen"}
@@ -273,7 +273,7 @@ async def test_included_and_excluded_simple_case_with_underscores(hass, recorder
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_included_and_excluded_complex_case(hass, recorder_mock):
+async def test_included_and_excluded_complex_case(recorder_mock, hass):
     """Test filters with included and excluded with a complex filter."""
     filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"}
     filter_reject = {
@@ -330,7 +330,7 @@ async def test_included_and_excluded_complex_case(hass, recorder_mock):
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_included_entities_and_excluded_domain(hass, recorder_mock):
+async def test_included_entities_and_excluded_domain(recorder_mock, hass):
     """Test filters with included entities and excluded domain."""
     filter_accept = {
         "media_player.test",
@@ -376,7 +376,7 @@ async def test_included_entities_and_excluded_domain(hass, recorder_mock):
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_same_domain_included_excluded(hass, recorder_mock):
+async def test_same_domain_included_excluded(recorder_mock, hass):
     """Test filters with the same domain included and excluded."""
     filter_accept = {
         "media_player.test",
@@ -422,7 +422,7 @@ async def test_same_domain_included_excluded(hass, recorder_mock):
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_same_entity_included_excluded(hass, recorder_mock):
+async def test_same_entity_included_excluded(recorder_mock, hass):
     """Test filters with the same entity included and excluded."""
     filter_accept = {
         "media_player.test",
@@ -468,7 +468,7 @@ async def test_same_entity_included_excluded(hass, recorder_mock):
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_same_entity_included_excluded_include_domain_wins(hass, recorder_mock):
+async def test_same_entity_included_excluded_include_domain_wins(recorder_mock, hass):
     """Test filters with domain and entities and the include domain wins."""
     filter_accept = {
         "media_player.test2",
@@ -516,7 +516,7 @@ async def test_same_entity_included_excluded_include_domain_wins(hass, recorder_
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_specificly_included_entity_always_wins(hass, recorder_mock):
+async def test_specificly_included_entity_always_wins(recorder_mock, hass):
     """Test specificlly included entity always wins."""
     filter_accept = {
         "media_player.test2",
@@ -564,7 +564,7 @@ async def test_specificly_included_entity_always_wins(hass, recorder_mock):
     assert not filtered_events_entity_ids.intersection(filter_reject)
 
 
-async def test_specificly_included_entity_always_wins_over_glob(hass, recorder_mock):
+async def test_specificly_included_entity_always_wins_over_glob(recorder_mock, hass):
     """Test specificlly included entity always wins over a glob."""
     filter_accept = {
         "sensor.apc900va_status",
diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py
index f18ba0768ca..a7b170f0838 100644
--- a/tests/components/recorder/test_history.py
+++ b/tests/components/recorder/test_history.py
@@ -650,8 +650,8 @@ def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]:
 
 
 async def test_state_changes_during_period_query_during_migration_to_schema_25(
-    hass: ha.HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: ha.HomeAssistant,
 ):
     """Test we can query data prior to schema 25 and during migration to schema 25."""
     instance = await async_setup_recorder_instance(hass, {})
@@ -700,8 +700,8 @@ async def test_state_changes_during_period_query_during_migration_to_schema_25(
 
 
 async def test_get_states_query_during_migration_to_schema_25(
-    hass: ha.HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: ha.HomeAssistant,
 ):
     """Test we can query data prior to schema 25 and during migration to schema 25."""
     instance = await async_setup_recorder_instance(hass, {})
@@ -746,8 +746,8 @@ async def test_get_states_query_during_migration_to_schema_25(
 
 
 async def test_get_states_query_during_migration_to_schema_25_multiple_entities(
-    hass: ha.HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: ha.HomeAssistant,
 ):
     """Test we can query data prior to schema 25 and during migration to schema 25."""
     instance = await async_setup_recorder_instance(hass, {})
@@ -795,8 +795,8 @@ async def test_get_states_query_during_migration_to_schema_25_multiple_entities(
 
 
 async def test_get_full_significant_states_handles_empty_last_changed(
-    hass: ha.HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: ha.HomeAssistant,
 ):
     """Test getting states when last_changed is null."""
     await async_setup_recorder_instance(hass, {})
diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py
index bcbf27faf18..b47212b1045 100644
--- a/tests/components/recorder/test_init.py
+++ b/tests/components/recorder/test_init.py
@@ -92,7 +92,7 @@ def _default_recorder(hass):
 
 
 async def test_shutdown_before_startup_finishes(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant, tmp_path
 ):
     """Test shutdown before recorder starts is clean."""
 
@@ -124,8 +124,8 @@ async def test_shutdown_before_startup_finishes(
 
 
 async def test_canceled_before_startup_finishes(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     caplog: pytest.LogCaptureFixture,
 ):
     """Test recorder shuts down when its startup future is canceled out from under it."""
@@ -145,7 +145,7 @@ async def test_canceled_before_startup_finishes(
     )
 
 
-async def test_shutdown_closes_connections(hass, recorder_mock):
+async def test_shutdown_closes_connections(recorder_mock, hass):
     """Test shutdown closes connections."""
 
     hass.state = CoreState.not_running
@@ -171,7 +171,7 @@ async def test_shutdown_closes_connections(hass, recorder_mock):
 
 
 async def test_state_gets_saved_when_set_before_start_event(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test we can record an event when starting with not running."""
 
@@ -197,7 +197,7 @@ async def test_state_gets_saved_when_set_before_start_event(
         assert db_states[0].event_id is None
 
 
-async def test_saving_state(hass: HomeAssistant, recorder_mock):
+async def test_saving_state(recorder_mock, hass: HomeAssistant):
     """Test saving and restoring a state."""
     entity_id = "test.recorder"
     state = "restoring_from_db"
@@ -220,7 +220,7 @@ async def test_saving_state(hass: HomeAssistant, recorder_mock):
 
 
 async def test_saving_many_states(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test we expire after many commits."""
     instance = await async_setup_recorder_instance(
@@ -248,7 +248,7 @@ async def test_saving_many_states(
 
 
 async def test_saving_state_with_intermixed_time_changes(
-    hass: HomeAssistant, recorder_mock
+    recorder_mock, hass: HomeAssistant
 ):
     """Test saving states with intermixed time changes."""
     entity_id = "test.recorder"
@@ -348,7 +348,7 @@ def test_saving_state_with_sqlalchemy_exception(hass, hass_recorder, caplog):
 
 
 async def test_force_shutdown_with_queue_of_writes_that_generate_exceptions(
-    hass, async_setup_recorder_instance, caplog
+    async_setup_recorder_instance, hass, caplog
 ):
     """Test forcing shutdown."""
     instance = await async_setup_recorder_instance(hass)
@@ -1370,8 +1370,8 @@ def test_entity_id_filter(hass_recorder):
 
 
 async def test_database_lock_and_unlock(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     tmp_path,
 ):
     """Test writing events during lock getting written after unlocking."""
@@ -1412,8 +1412,8 @@ async def test_database_lock_and_unlock(
 
 
 async def test_database_lock_and_overflow(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     tmp_path,
 ):
     """Test writing events during lock leading to overflow the queue causes the database to unlock."""
@@ -1450,7 +1450,7 @@ async def test_database_lock_and_overflow(
         assert not instance.unlock_database()
 
 
-async def test_database_lock_timeout(hass, recorder_mock):
+async def test_database_lock_timeout(recorder_mock, hass):
     """Test locking database timeout when recorder stopped."""
     hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
 
@@ -1473,7 +1473,7 @@ async def test_database_lock_timeout(hass, recorder_mock):
             block_task.event.set()
 
 
-async def test_database_lock_without_instance(hass, recorder_mock):
+async def test_database_lock_without_instance(recorder_mock, hass):
     """Test database lock doesn't fail if instance is not initialized."""
     hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
 
@@ -1494,8 +1494,8 @@ async def test_in_memory_database(hass, caplog):
 
 
 async def test_database_connection_keep_alive(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     caplog: pytest.LogCaptureFixture,
 ):
     """Test we keep alive socket based dialects."""
@@ -1514,8 +1514,8 @@ async def test_database_connection_keep_alive(
 
 
 async def test_database_connection_keep_alive_disabled_on_sqlite(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     caplog: pytest.LogCaptureFixture,
 ):
     """Test we do not do keep alive for sqlite."""
@@ -1590,7 +1590,7 @@ def test_deduplication_state_attributes_inside_commit_interval(hass_recorder, ca
         assert first_attributes_id == last_attributes_id
 
 
-async def test_async_block_till_done(hass, async_setup_recorder_instance):
+async def test_async_block_till_done(async_setup_recorder_instance, hass):
     """Test we can block until recordering is done."""
     instance = await async_setup_recorder_instance(hass)
     await async_wait_recording_done(hass)
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index c6c447c01c9..8f5cde707f7 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -53,7 +53,7 @@ def mock_use_sqlite(request):
 
 
 async def test_purge_old_states(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test deleting old states."""
     instance = await async_setup_recorder_instance(hass)
@@ -135,7 +135,7 @@ async def test_purge_old_states(
 
 
 async def test_purge_old_states_encouters_database_corruption(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test database image image is malformed while deleting old states."""
     await async_setup_recorder_instance(hass)
@@ -165,8 +165,8 @@ async def test_purge_old_states_encouters_database_corruption(
 
 
 async def test_purge_old_states_encounters_temporary_mysql_error(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     caplog,
 ):
     """Test retry on specific mysql operational errors."""
@@ -196,8 +196,8 @@ async def test_purge_old_states_encounters_temporary_mysql_error(
 
 
 async def test_purge_old_states_encounters_operational_error(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     caplog,
 ):
     """Test error on operational errors that are not mysql does not retry."""
@@ -222,7 +222,7 @@ async def test_purge_old_states_encounters_operational_error(
 
 
 async def test_purge_old_events(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test deleting old events."""
     instance = await async_setup_recorder_instance(hass)
@@ -259,7 +259,7 @@ async def test_purge_old_events(
 
 
 async def test_purge_old_recorder_runs(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test deleting old recorder runs keeps current run."""
     instance = await async_setup_recorder_instance(hass)
@@ -295,7 +295,7 @@ async def test_purge_old_recorder_runs(
 
 
 async def test_purge_old_statistics_runs(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test deleting old statistics runs keeps the latest run."""
     instance = await async_setup_recorder_instance(hass)
@@ -320,8 +320,8 @@ async def test_purge_old_statistics_runs(
 
 @pytest.mark.parametrize("use_sqlite", (True, False), indirect=True)
 async def test_purge_method(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     caplog: pytest.LogCaptureFixture,
     use_sqlite: bool,
 ):
@@ -436,8 +436,8 @@ async def test_purge_method(
 
 @pytest.mark.parametrize("use_sqlite", (True, False), indirect=True)
 async def test_purge_edge_case(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     use_sqlite: bool,
 ):
     """Test states and events are purged even if they occurred shortly before purge_before."""
@@ -503,8 +503,8 @@ async def test_purge_edge_case(
 
 
 async def test_purge_cutoff_date(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
 ):
     """Test states and events are purged only if they occurred before "now() - keep_days"."""
 
@@ -651,8 +651,8 @@ async def test_purge_cutoff_date(
 
 @pytest.mark.parametrize("use_sqlite", (True, False), indirect=True)
 async def test_purge_filtered_states(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     use_sqlite: bool,
 ):
     """Test filtered states are purged."""
@@ -837,8 +837,8 @@ async def test_purge_filtered_states(
 
 @pytest.mark.parametrize("use_sqlite", (True, False), indirect=True)
 async def test_purge_filtered_states_to_empty(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     use_sqlite: bool,
 ):
     """Test filtered states are purged all the way to an empty db."""
@@ -890,8 +890,8 @@ async def test_purge_filtered_states_to_empty(
 
 @pytest.mark.parametrize("use_sqlite", (True, False), indirect=True)
 async def test_purge_without_state_attributes_filtered_states_to_empty(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     use_sqlite: bool,
 ):
     """Test filtered legacy states without state attributes are purged all the way to an empty db."""
@@ -964,8 +964,8 @@ async def test_purge_without_state_attributes_filtered_states_to_empty(
 
 
 async def test_purge_filtered_events(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
 ):
     """Test filtered events are purged."""
     config: ConfigType = {"exclude": {"event_types": ["EVENT_PURGE"]}}
@@ -1052,8 +1052,8 @@ async def test_purge_filtered_events(
 
 
 async def test_purge_filtered_events_state_changed(
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
 ):
     """Test filtered state_changed events are purged. This should also remove all states."""
     config: ConfigType = {"exclude": {"event_types": [EVENT_STATE_CHANGED]}}
@@ -1155,7 +1155,7 @@ async def test_purge_filtered_events_state_changed(
 
 
 async def test_purge_entities(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test purging of specific entities."""
     await async_setup_recorder_instance(hass)
@@ -1527,7 +1527,7 @@ def _add_state_and_state_changed_event(
 
 
 async def test_purge_many_old_events(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test deleting old events."""
     instance = await async_setup_recorder_instance(hass)
@@ -1580,7 +1580,7 @@ async def test_purge_many_old_events(
 
 
 async def test_purge_can_mix_legacy_and_new_format(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test purging with legacy a new events."""
     instance = await async_setup_recorder_instance(hass)
diff --git a/tests/components/recorder/test_run_history.py b/tests/components/recorder/test_run_history.py
index ff4a5e5d701..7504404f779 100644
--- a/tests/components/recorder/test_run_history.py
+++ b/tests/components/recorder/test_run_history.py
@@ -8,7 +8,7 @@ from homeassistant.components.recorder.models import process_timestamp
 from homeassistant.util import dt as dt_util
 
 
-async def test_run_history(hass, recorder_mock):
+async def test_run_history(recorder_mock, hass):
     """Test the run history gives the correct run."""
     instance = recorder.get_instance(hass)
     now = dt_util.utcnow()
diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py
index 6d96b97b89c..fb5cf8dba8b 100644
--- a/tests/components/recorder/test_statistics.py
+++ b/tests/components/recorder/test_statistics.py
@@ -448,9 +448,9 @@ def test_statistics_duplicated(hass_recorder, caplog):
     ),
 )
 async def test_import_statistics(
+    recorder_mock,
     hass,
     hass_ws_client,
-    recorder_mock,
     caplog,
     source,
     statistic_id,
diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py
index b465ee89ebe..93c0157191a 100644
--- a/tests/components/recorder/test_system_health.py
+++ b/tests/components/recorder/test_system_health.py
@@ -14,7 +14,7 @@ from .common import async_wait_recording_done
 from tests.common import SetupRecorderInstanceT, get_system_health_info
 
 
-async def test_recorder_system_health(hass, recorder_mock):
+async def test_recorder_system_health(recorder_mock, hass):
     """Test recorder system health."""
     assert await async_setup_component(hass, "system_health", {})
     await async_wait_recording_done(hass)
@@ -32,7 +32,7 @@ async def test_recorder_system_health(hass, recorder_mock):
 @pytest.mark.parametrize(
     "dialect_name", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL]
 )
-async def test_recorder_system_health_alternate_dbms(hass, recorder_mock, dialect_name):
+async def test_recorder_system_health_alternate_dbms(recorder_mock, hass, dialect_name):
     """Test recorder system health."""
     assert await async_setup_component(hass, "system_health", {})
     await async_wait_recording_done(hass)
@@ -57,7 +57,7 @@ async def test_recorder_system_health_alternate_dbms(hass, recorder_mock, dialec
     "dialect_name", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL]
 )
 async def test_recorder_system_health_db_url_missing_host(
-    hass, recorder_mock, dialect_name
+    recorder_mock, hass, dialect_name
 ):
     """Test recorder system health with a db_url without a hostname."""
     assert await async_setup_component(hass, "system_health", {})
@@ -85,7 +85,7 @@ async def test_recorder_system_health_db_url_missing_host(
 
 
 async def test_recorder_system_health_crashed_recorder_runs_table(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test recorder system health with crashed recorder runs table."""
     with patch("homeassistant.components.recorder.run_history.RunHistory.load_from_db"):
diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py
index 70f4eb0da70..fe31e3a9a8d 100644
--- a/tests/components/recorder/test_util.py
+++ b/tests/components/recorder/test_util.py
@@ -651,8 +651,8 @@ def test_periodic_db_cleanups(hass_recorder):
 @patch("homeassistant.components.recorder.pool.check_loop")
 async def test_write_lock_db(
     skip_check_loop,
-    hass: HomeAssistant,
     async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
     tmp_path,
 ):
     """Test database write lock."""
diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py
index 6058fd6a2e5..b644df48864 100644
--- a/tests/components/recorder/test_websocket_api.py
+++ b/tests/components/recorder/test_websocket_api.py
@@ -122,7 +122,7 @@ VOLUME_SENSOR_M3_ATTRIBUTES_TOTAL = {
 }
 
 
-async def test_statistics_during_period(hass, hass_ws_client, recorder_mock):
+async def test_statistics_during_period(recorder_mock, hass, hass_ws_client):
     """Test statistics_during_period."""
     now = dt_util.utcnow()
 
@@ -200,9 +200,9 @@ async def test_statistics_during_period(hass, hass_ws_client, recorder_mock):
     ],
 )
 async def test_statistics_during_period_unit_conversion(
+    recorder_mock,
     hass,
     hass_ws_client,
-    recorder_mock,
     attributes,
     state,
     value,
@@ -293,9 +293,9 @@ async def test_statistics_during_period_unit_conversion(
     ],
 )
 async def test_sum_statistics_during_period_unit_conversion(
+    recorder_mock,
     hass,
     hass_ws_client,
-    recorder_mock,
     attributes,
     state,
     value,
@@ -386,7 +386,7 @@ async def test_sum_statistics_during_period_unit_conversion(
     ],
 )
 async def test_statistics_during_period_invalid_unit_conversion(
-    hass, hass_ws_client, recorder_mock, custom_units
+    recorder_mock, hass, hass_ws_client, custom_units
 ):
     """Test statistics_during_period."""
     now = dt_util.utcnow()
@@ -425,7 +425,7 @@ async def test_statistics_during_period_invalid_unit_conversion(
 
 
 async def test_statistics_during_period_in_the_past(
-    hass, hass_ws_client, recorder_mock
+    recorder_mock, hass, hass_ws_client
 ):
     """Test statistics_during_period in the past."""
     hass.config.set_time_zone("UTC")
@@ -548,7 +548,7 @@ async def test_statistics_during_period_in_the_past(
 
 
 async def test_statistics_during_period_bad_start_time(
-    hass, hass_ws_client, recorder_mock
+    recorder_mock, hass, hass_ws_client
 ):
     """Test statistics_during_period."""
     client = await hass_ws_client()
@@ -566,7 +566,7 @@ async def test_statistics_during_period_bad_start_time(
 
 
 async def test_statistics_during_period_bad_end_time(
-    hass, hass_ws_client, recorder_mock
+    recorder_mock, hass, hass_ws_client
 ):
     """Test statistics_during_period."""
     now = dt_util.utcnow()
@@ -614,9 +614,9 @@ async def test_statistics_during_period_bad_end_time(
     ],
 )
 async def test_list_statistic_ids(
+    recorder_mock,
     hass,
     hass_ws_client,
-    recorder_mock,
     units,
     attributes,
     display_unit,
@@ -724,7 +724,7 @@ async def test_list_statistic_ids(
         assert response["result"] == []
 
 
-async def test_validate_statistics(hass, hass_ws_client, recorder_mock):
+async def test_validate_statistics(recorder_mock, hass, hass_ws_client):
     """Test validate_statistics can be called."""
     id = 1
 
@@ -746,7 +746,7 @@ async def test_validate_statistics(hass, hass_ws_client, recorder_mock):
     await assert_validation_result(client, {})
 
 
-async def test_clear_statistics(hass, hass_ws_client, recorder_mock):
+async def test_clear_statistics(recorder_mock, hass, hass_ws_client):
     """Test removing statistics."""
     now = dt_util.utcnow()
 
@@ -873,7 +873,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock):
     "new_unit, new_unit_class", [("dogs", None), (None, None), ("W", "power")]
 )
 async def test_update_statistics_metadata(
-    hass, hass_ws_client, recorder_mock, new_unit, new_unit_class
+    recorder_mock, hass, hass_ws_client, new_unit, new_unit_class
 ):
     """Test removing statistics."""
     now = dt_util.utcnow()
@@ -964,7 +964,7 @@ async def test_update_statistics_metadata(
     }
 
 
-async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock):
+async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client):
     """Test change unit of recorded statistics."""
     now = dt_util.utcnow()
 
@@ -1083,7 +1083,7 @@ async def test_change_statistics_unit(hass, hass_ws_client, recorder_mock):
 
 
 async def test_change_statistics_unit_errors(
-    hass, hass_ws_client, recorder_mock, caplog
+    recorder_mock, hass, hass_ws_client, caplog
 ):
     """Test change unit of recorded statistics."""
     now = dt_util.utcnow()
@@ -1200,7 +1200,7 @@ async def test_change_statistics_unit_errors(
     await assert_statistics(expected_statistics)
 
 
-async def test_recorder_info(hass, hass_ws_client, recorder_mock):
+async def test_recorder_info(recorder_mock, hass, hass_ws_client):
     """Test getting recorder status."""
     client = await hass_ws_client()
 
@@ -1329,7 +1329,7 @@ async def test_backup_start_no_recorder(
 
 
 async def test_backup_start_timeout(
-    hass, hass_ws_client, hass_supervisor_access_token, recorder_mock
+    recorder_mock, hass, hass_ws_client, hass_supervisor_access_token
 ):
     """Test getting backup start when recorder is not present."""
     client = await hass_ws_client(hass, hass_supervisor_access_token)
@@ -1348,7 +1348,7 @@ async def test_backup_start_timeout(
 
 
 async def test_backup_end(
-    hass, hass_ws_client, hass_supervisor_access_token, recorder_mock
+    recorder_mock, hass, hass_ws_client, hass_supervisor_access_token
 ):
     """Test backup start."""
     client = await hass_ws_client(hass, hass_supervisor_access_token)
@@ -1366,7 +1366,7 @@ async def test_backup_end(
 
 
 async def test_backup_end_without_start(
-    hass, hass_ws_client, hass_supervisor_access_token, recorder_mock
+    recorder_mock, hass, hass_ws_client, hass_supervisor_access_token
 ):
     """Test backup start."""
     client = await hass_ws_client(hass, hass_supervisor_access_token)
@@ -1400,7 +1400,7 @@ async def test_backup_end_without_start(
     ],
 )
 async def test_get_statistics_metadata(
-    hass, hass_ws_client, recorder_mock, units, attributes, unit, unit_class
+    recorder_mock, hass, hass_ws_client, units, attributes, unit, unit_class
 ):
     """Test get_statistics_metadata."""
     now = dt_util.utcnow()
@@ -1545,7 +1545,7 @@ async def test_get_statistics_metadata(
     ),
 )
 async def test_import_statistics(
-    hass, hass_ws_client, recorder_mock, caplog, source, statistic_id
+    recorder_mock, hass, hass_ws_client, caplog, source, statistic_id
 ):
     """Test importing statistics."""
     client = await hass_ws_client()
@@ -1772,7 +1772,7 @@ async def test_import_statistics(
     ),
 )
 async def test_adjust_sum_statistics_energy(
-    hass, hass_ws_client, recorder_mock, caplog, source, statistic_id
+    recorder_mock, hass, hass_ws_client, caplog, source, statistic_id
 ):
     """Test adjusting statistics."""
     client = await hass_ws_client()
@@ -1968,7 +1968,7 @@ async def test_adjust_sum_statistics_energy(
     ),
 )
 async def test_adjust_sum_statistics_gas(
-    hass, hass_ws_client, recorder_mock, caplog, source, statistic_id
+    recorder_mock, hass, hass_ws_client, caplog, source, statistic_id
 ):
     """Test adjusting statistics."""
     client = await hass_ws_client()
@@ -2168,9 +2168,9 @@ async def test_adjust_sum_statistics_gas(
     ),
 )
 async def test_adjust_sum_statistics_errors(
+    recorder_mock,
     hass,
     hass_ws_client,
-    recorder_mock,
     caplog,
     state_unit,
     statistic_unit,
diff --git a/tests/components/schedule/test_recorder.py b/tests/components/schedule/test_recorder.py
index a105f388fc2..f6879da90d6 100644
--- a/tests/components/schedule/test_recorder.py
+++ b/tests/components/schedule/test_recorder.py
@@ -16,8 +16,8 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant,
     recorder_mock: None,
+    hass: HomeAssistant,
     enable_custom_integrations: None,
 ) -> None:
     """Test attributes to be excluded."""
diff --git a/tests/components/script/test_recorder.py b/tests/components/script/test_recorder.py
index a023212b82b..ecbe554d9de 100644
--- a/tests/components/script/test_recorder.py
+++ b/tests/components/script/test_recorder.py
@@ -27,7 +27,7 @@ def calls(hass):
     return async_mock_service(hass, "test", "automation")
 
 
-async def test_exclude_attributes(hass, recorder_mock, calls):
+async def test_exclude_attributes(recorder_mock, hass, calls):
     """Test automation registered attributes to be excluded."""
     await hass.async_block_till_done()
     calls = []
diff --git a/tests/components/select/test_recorder.py b/tests/components/select/test_recorder.py
index 083caef3444..0598438029b 100644
--- a/tests/components/select/test_recorder.py
+++ b/tests/components/select/test_recorder.py
@@ -16,7 +16,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test select registered attributes to be excluded."""
     await async_setup_component(
         hass, select.DOMAIN, {select.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py
index e7f8139d7d3..a49882a0eb3 100644
--- a/tests/components/sensor/test_recorder.py
+++ b/tests/components/sensor/test_recorder.py
@@ -430,9 +430,9 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes)
     ],
 )
 async def test_compile_hourly_sum_statistics_amount(
+    recorder_mock,
     hass,
     hass_ws_client,
-    recorder_mock,
     caplog,
     units,
     state_class,
@@ -3328,7 +3328,7 @@ def record_states(hass, zero, entity_id, attributes, seq=None):
     ],
 )
 async def test_validate_unit_change_convertible(
-    hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit
+    recorder_mock, hass, hass_ws_client, units, attributes, unit, unit2, supported_unit
 ):
     """Test validate_statistics.
 
@@ -3443,7 +3443,7 @@ async def test_validate_unit_change_convertible(
     ],
 )
 async def test_validate_statistics_unit_ignore_device_class(
-    hass, hass_ws_client, recorder_mock, units, attributes
+    recorder_mock, hass, hass_ws_client, units, attributes
 ):
     """Test validate_statistics.
 
@@ -3514,7 +3514,7 @@ async def test_validate_statistics_unit_ignore_device_class(
     ],
 )
 async def test_validate_statistics_unit_change_no_device_class(
-    hass, hass_ws_client, recorder_mock, units, attributes, unit, unit2, supported_unit
+    recorder_mock, hass, hass_ws_client, units, attributes, unit, unit2, supported_unit
 ):
     """Test validate_statistics.
 
@@ -3629,7 +3629,7 @@ async def test_validate_statistics_unit_change_no_device_class(
     ],
 )
 async def test_validate_statistics_unsupported_state_class(
-    hass, hass_ws_client, recorder_mock, units, attributes, unit
+    recorder_mock, hass, hass_ws_client, units, attributes, unit
 ):
     """Test validate_statistics."""
     id = 1
@@ -3693,7 +3693,7 @@ async def test_validate_statistics_unsupported_state_class(
     ],
 )
 async def test_validate_statistics_sensor_no_longer_recorded(
-    hass, hass_ws_client, recorder_mock, units, attributes, unit
+    recorder_mock, hass, hass_ws_client, units, attributes, unit
 ):
     """Test validate_statistics."""
     id = 1
@@ -3754,7 +3754,7 @@ async def test_validate_statistics_sensor_no_longer_recorded(
     ],
 )
 async def test_validate_statistics_sensor_not_recorded(
-    hass, hass_ws_client, recorder_mock, units, attributes, unit
+    recorder_mock, hass, hass_ws_client, units, attributes, unit
 ):
     """Test validate_statistics."""
     id = 1
@@ -3812,7 +3812,7 @@ async def test_validate_statistics_sensor_not_recorded(
     ],
 )
 async def test_validate_statistics_sensor_removed(
-    hass, hass_ws_client, recorder_mock, units, attributes, unit
+    recorder_mock, hass, hass_ws_client, units, attributes, unit
 ):
     """Test validate_statistics."""
     id = 1
@@ -3871,7 +3871,7 @@ async def test_validate_statistics_sensor_removed(
     ],
 )
 async def test_validate_statistics_unit_change_no_conversion(
-    hass, recorder_mock, hass_ws_client, attributes, unit1, unit2
+    recorder_mock, hass, hass_ws_client, attributes, unit1, unit2
 ):
     """Test validate_statistics."""
     id = 1
@@ -3988,7 +3988,7 @@ 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):
+async def test_validate_statistics_other_domain(recorder_mock, hass, hass_ws_client):
     """Test sensor does not raise issues for statistics for other domains."""
     id = 1
 
diff --git a/tests/components/siren/test_recorder.py b/tests/components/siren/test_recorder.py
index aaf1679478a..c93a1a8c8e1 100644
--- a/tests/components/siren/test_recorder.py
+++ b/tests/components/siren/test_recorder.py
@@ -16,7 +16,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test siren registered attributes to be excluded."""
     await async_setup_component(
         hass, siren.DOMAIN, {siren.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py
index ea54745048e..e5bbc163249 100644
--- a/tests/components/sql/test_config_flow.py
+++ b/tests/components/sql/test_config_flow.py
@@ -21,7 +21,7 @@ from . import (
 from tests.common import MockConfigEntry
 
 
-async def test_form(hass: HomeAssistant, recorder_mock) -> None:
+async def test_form(recorder_mock, hass: HomeAssistant) -> None:
     """Test we get the form."""
 
     result = await hass.config_entries.flow.async_init(
@@ -53,7 +53,7 @@ async def test_form(hass: HomeAssistant, recorder_mock) -> None:
     assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_import_flow_success(hass: HomeAssistant, recorder_mock) -> None:
+async def test_import_flow_success(recorder_mock, hass: HomeAssistant) -> None:
     """Test a successful import of yaml."""
 
     with patch(
@@ -80,7 +80,7 @@ async def test_import_flow_success(hass: HomeAssistant, recorder_mock) -> None:
     assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_import_flow_already_exist(hass: HomeAssistant, recorder_mock) -> None:
+async def test_import_flow_already_exist(recorder_mock, hass: HomeAssistant) -> None:
     """Test import of yaml already exist."""
 
     MockConfigEntry(
@@ -103,7 +103,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant, recorder_mock) ->
     assert result3["reason"] == "already_configured"
 
 
-async def test_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None:
+async def test_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None:
     """Test config flow fails incorrect db url."""
     result4 = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -124,7 +124,7 @@ async def test_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None:
     assert result4["errors"] == {"db_url": "db_url_invalid"}
 
 
-async def test_flow_fails_invalid_query(hass: HomeAssistant, recorder_mock) -> None:
+async def test_flow_fails_invalid_query(recorder_mock, hass: HomeAssistant) -> None:
     """Test config flow fails incorrect db url."""
     result4 = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -170,7 +170,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant, recorder_mock) -> N
     }
 
 
-async def test_options_flow(hass: HomeAssistant, recorder_mock) -> None:
+async def test_options_flow(recorder_mock, hass: HomeAssistant) -> None:
     """Test options config flow."""
     entry = MockConfigEntry(
         domain=DOMAIN,
@@ -219,7 +219,7 @@ async def test_options_flow(hass: HomeAssistant, recorder_mock) -> None:
 
 
 async def test_options_flow_name_previously_removed(
-    hass: HomeAssistant, recorder_mock
+    recorder_mock, hass: HomeAssistant
 ) -> None:
     """Test options config flow where the name was missing."""
     entry = MockConfigEntry(
@@ -270,7 +270,7 @@ async def test_options_flow_name_previously_removed(
     }
 
 
-async def test_options_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None:
+async def test_options_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None:
     """Test options flow fails incorrect db url."""
     entry = MockConfigEntry(
         domain=DOMAIN,
@@ -313,7 +313,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant, recorder_mock) ->
 
 
 async def test_options_flow_fails_invalid_query(
-    hass: HomeAssistant, recorder_mock
+    recorder_mock, hass: HomeAssistant
 ) -> None:
     """Test options flow fails incorrect query and template."""
     entry = MockConfigEntry(
@@ -369,7 +369,7 @@ async def test_options_flow_fails_invalid_query(
     }
 
 
-async def test_options_flow_db_url_empty(hass: HomeAssistant, recorder_mock) -> None:
+async def test_options_flow_db_url_empty(recorder_mock, hass: HomeAssistant) -> None:
     """Test options config flow with leaving db_url empty."""
     entry = MockConfigEntry(
         domain=DOMAIN,
diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py
index 56255216f52..453d07fb69f 100644
--- a/tests/components/statistics/test_sensor.py
+++ b/tests/components/statistics/test_sensor.py
@@ -1010,7 +1010,7 @@ async def test_invalid_state_characteristic(hass: HomeAssistant):
     assert state is None
 
 
-async def test_initialize_from_database(hass: HomeAssistant, recorder_mock):
+async def test_initialize_from_database(recorder_mock, hass: HomeAssistant):
     """Test initializing the statistics from the recorder database."""
     # enable and pre-fill the recorder
     await hass.async_block_till_done()
@@ -1049,7 +1049,7 @@ async def test_initialize_from_database(hass: HomeAssistant, recorder_mock):
     assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
 
 
-async def test_initialize_from_database_with_maxage(hass: HomeAssistant, recorder_mock):
+async def test_initialize_from_database_with_maxage(recorder_mock, hass: HomeAssistant):
     """Test initializing the statistics from the database."""
     now = dt_util.utcnow()
     mock_data = {
@@ -1109,7 +1109,7 @@ async def test_initialize_from_database_with_maxage(hass: HomeAssistant, recorde
     ) + timedelta(hours=1)
 
 
-async def test_reload(hass: HomeAssistant, recorder_mock):
+async def test_reload(recorder_mock, hass: HomeAssistant):
     """Verify we can reload statistics sensors."""
 
     await async_setup_component(
diff --git a/tests/components/sun/test_recorder.py b/tests/components/sun/test_recorder.py
index 547bf44ec5f..d8766beaa2b 100644
--- a/tests/components/sun/test_recorder.py
+++ b/tests/components/sun/test_recorder.py
@@ -26,7 +26,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test sun attributes to be excluded."""
     await async_setup_component(hass, DOMAIN, {})
     await hass.async_block_till_done()
diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py
index 1d42c9826e9..acd1d2a842e 100644
--- a/tests/components/tibber/test_config_flow.py
+++ b/tests/components/tibber/test_config_flow.py
@@ -15,7 +15,7 @@ def tibber_setup_fixture():
         yield
 
 
-async def test_show_config_form(hass, recorder_mock):
+async def test_show_config_form(recorder_mock, hass):
     """Test show configuration form."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -25,7 +25,7 @@ async def test_show_config_form(hass, recorder_mock):
     assert result["step_id"] == "user"
 
 
-async def test_create_entry(hass, recorder_mock):
+async def test_create_entry(recorder_mock, hass):
     """Test create entry from user input."""
     test_data = {
         CONF_ACCESS_TOKEN: "valid",
@@ -49,7 +49,7 @@ async def test_create_entry(hass, recorder_mock):
     assert result["data"] == test_data
 
 
-async def test_flow_entry_already_exists(hass, recorder_mock, config_entry):
+async def test_flow_entry_already_exists(recorder_mock, hass, config_entry):
     """Test user input for config_entry that already exists."""
     test_data = {
         CONF_ACCESS_TOKEN: "valid",
diff --git a/tests/components/tibber/test_diagnostics.py b/tests/components/tibber/test_diagnostics.py
index c9210412820..38b5eb91a2f 100644
--- a/tests/components/tibber/test_diagnostics.py
+++ b/tests/components/tibber/test_diagnostics.py
@@ -8,7 +8,7 @@ from .test_common import mock_get_homes
 from tests.components.diagnostics import get_diagnostics_for_config_entry
 
 
-async def test_entry_diagnostics(hass, hass_client, recorder_mock, config_entry):
+async def test_entry_diagnostics(recorder_mock, hass, hass_client, config_entry):
     """Test config entry diagnostics."""
     with patch(
         "tibber.Tibber.update_info",
diff --git a/tests/components/tibber/test_statistics.py b/tests/components/tibber/test_statistics.py
index 8e0d24ffa9d..745c434237b 100644
--- a/tests/components/tibber/test_statistics.py
+++ b/tests/components/tibber/test_statistics.py
@@ -10,7 +10,7 @@ from .test_common import CONSUMPTION_DATA_1, PRODUCTION_DATA_1, mock_get_homes
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_async_setup_entry(hass, recorder_mock):
+async def test_async_setup_entry(recorder_mock, hass):
     """Test setup Tibber."""
     tibber_connection = AsyncMock()
     tibber_connection.name = "tibber"
diff --git a/tests/components/update/test_recorder.py b/tests/components/update/test_recorder.py
index d1263a720af..46588ecf45d 100644
--- a/tests/components/update/test_recorder.py
+++ b/tests/components/update/test_recorder.py
@@ -21,7 +21,7 @@ from tests.components.recorder.common import async_wait_recording_done
 
 
 async def test_exclude_attributes(
-    hass: HomeAssistant, recorder_mock, enable_custom_integrations: None
+    recorder_mock, hass: HomeAssistant, enable_custom_integrations: None
 ):
     """Test update attributes to be excluded."""
     platform = getattr(hass.components, f"test.{DOMAIN}")
diff --git a/tests/components/vacuum/test_recorder.py b/tests/components/vacuum/test_recorder.py
index 040cc9105aa..1ca796ba364 100644
--- a/tests/components/vacuum/test_recorder.py
+++ b/tests/components/vacuum/test_recorder.py
@@ -16,7 +16,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test vacuum registered attributes to be excluded."""
     await async_setup_component(
         hass, vacuum.DOMAIN, {vacuum.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/water_heater/test_recorder.py b/tests/components/water_heater/test_recorder.py
index b6670152e3f..549cafc7445 100644
--- a/tests/components/water_heater/test_recorder.py
+++ b/tests/components/water_heater/test_recorder.py
@@ -20,7 +20,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass, recorder_mock):
+async def test_exclude_attributes(recorder_mock, hass):
     """Test water_heater registered attributes to be excluded."""
     await async_setup_component(
         hass, water_heater.DOMAIN, {water_heater.DOMAIN: {"platform": "demo"}}
diff --git a/tests/components/weather/test_recorder.py b/tests/components/weather/test_recorder.py
index ef1998f734c..7e113ba9b83 100644
--- a/tests/components/weather/test_recorder.py
+++ b/tests/components/weather/test_recorder.py
@@ -15,7 +15,7 @@ from tests.common import async_fire_time_changed
 from tests.components.recorder.common import async_wait_recording_done
 
 
-async def test_exclude_attributes(hass: HomeAssistant, recorder_mock) -> None:
+async def test_exclude_attributes(recorder_mock, hass: HomeAssistant) -> None:
     """Test weather attributes to be excluded."""
     await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "demo"}})
     hass.config.units = METRIC_SYSTEM
diff --git a/tests/conftest.py b/tests/conftest.py
index 8ce266f6b26..abba0e39c19 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -312,9 +312,17 @@ def aiohttp_client(
 
 
 @pytest.fixture
-def hass(loop, load_registries, hass_storage, request):
+def hass_fixture_setup():
+    """Fixture whichis truthy if the hass fixture has been setup."""
+    return []
+
+
+@pytest.fixture
+def hass(hass_fixture_setup, loop, load_registries, hass_storage, request):
     """Fixture to provide a test instance of Home Assistant."""
 
+    hass_fixture_setup.append(True)
+
     orig_tz = dt_util.DEFAULT_TIME_ZONE
 
     def exc_handle(loop, context):
@@ -912,9 +920,10 @@ async def _async_init_recorder_component(hass, add_config=None):
 
 @pytest.fixture
 async def async_setup_recorder_instance(
-    enable_nightly_purge, enable_statistics
+    hass_fixture_setup, enable_nightly_purge, enable_statistics
 ) -> AsyncGenerator[SetupRecorderInstanceT, None]:
     """Yield callable to setup recorder instance."""
+    assert not hass_fixture_setup
 
     nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None
     stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None
diff --git a/tests/helpers/test_recorder.py b/tests/helpers/test_recorder.py
index 9410117acb4..9c11a372cbe 100644
--- a/tests/helpers/test_recorder.py
+++ b/tests/helpers/test_recorder.py
@@ -9,7 +9,7 @@ from tests.common import SetupRecorderInstanceT
 
 
 async def test_async_migration_in_progress(
-    hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT
+    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
 ):
     """Test async_migration_in_progress wraps the recorder."""
     with patch(
-- 
GitLab


From d1facab71c02a73590c604596ce9d63d06b129d0 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 19 Oct 2022 10:05:55 +0200
Subject: [PATCH 593/985] Add guard in recorder retry function (#80585)

---
 homeassistant/components/recorder/util.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py
index bd95e1f50c3..8ee9a4e0401 100644
--- a/homeassistant/components/recorder/util.py
+++ b/homeassistant/components/recorder/util.py
@@ -509,6 +509,7 @@ def retryable_database_job(
                 assert instance.engine is not None
                 if (
                     instance.engine.dialect.name == SupportedDialect.MYSQL
+                    and err.orig
                     and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS
                 ):
                     _LOGGER.info(
-- 
GitLab


From eca21ceff004f0e43f896f75955e263feeebe096 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 19 Oct 2022 10:26:17 +0200
Subject: [PATCH 594/985] =?UTF-8?q?Make=20all=20datetime=20columns=20in=20?=
 =?UTF-8?q?recorder=20DB=20=C2=B5s=20precision=20(#80584)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 homeassistant/components/recorder/db_schema.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py
index d76f89068d0..d2373d96aeb 100644
--- a/homeassistant/components/recorder/db_schema.py
+++ b/homeassistant/components/recorder/db_schema.py
@@ -511,10 +511,10 @@ class RecorderRuns(Base):  # type: ignore[misc,valid-type]
     __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))
+    start = Column(DATETIME_TYPE, default=dt_util.utcnow)
+    end = Column(DATETIME_TYPE)
     closed_incorrect = Column(Boolean, default=False)
-    created = Column(DateTime(timezone=True), default=dt_util.utcnow)
+    created = Column(DATETIME_TYPE, default=dt_util.utcnow)
 
     def __repr__(self) -> str:
         """Return string representation of instance for debugging."""
@@ -561,7 +561,7 @@ class SchemaChanges(Base):  # type: ignore[misc,valid-type]
     __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)
+    changed = Column(DATETIME_TYPE, default=dt_util.utcnow)
 
     def __repr__(self) -> str:
         """Return string representation of instance for debugging."""
@@ -578,7 +578,7 @@ class StatisticsRuns(Base):  # type: ignore[misc,valid-type]
 
     __tablename__ = TABLE_STATISTICS_RUNS
     run_id = Column(Integer, Identity(), primary_key=True)
-    start = Column(DateTime(timezone=True), index=True)
+    start = Column(DATETIME_TYPE, index=True)
 
     def __repr__(self) -> str:
         """Return string representation of instance for debugging."""
-- 
GitLab


From 0c8884fd5178fb07f838d97790ea7665b3cadb30 Mon Sep 17 00:00:00 2001
From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com>
Date: Wed, 19 Oct 2022 11:26:27 +0200
Subject: [PATCH 595/985] Add sensor, selector and switch for Plugwise Anna +
 Loria combination (#80558)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
---
 homeassistant/components/plugwise/select.py   | 10 +++++++++
 homeassistant/components/plugwise/sensor.py   |  7 ++++++
 .../components/plugwise/strings.select.json   |  6 +++++
 homeassistant/components/plugwise/switch.py   |  6 +++++
 .../plugwise/translations/select.en.json      | 22 ++++++++++++-------
 tests/components/plugwise/test_sensor.py      |  4 ++++
 6 files changed, 47 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py
index a6f49380678..73157c6a962 100644
--- a/homeassistant/components/plugwise/select.py
+++ b/homeassistant/components/plugwise/select.py
@@ -54,6 +54,16 @@ SELECT_TYPES = (
         options_key="regulation_modes",
         device_class="plugwise__regulation_mode",
     ),
+    PlugwiseSelectEntityDescription(
+        key="select_dhw_mode",
+        name="DHW mode",
+        icon="mdi:shower",
+        entity_category=EntityCategory.CONFIG,
+        command=lambda api, loc, opt: api.set_dhw_mode(opt),
+        current_option_key="dhw_mode",
+        options_key="dhw_modes",
+        device_class="plugwise__dhw_mode",
+    ),
 )
 
 
diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py
index 3ee0437e8dd..0e65f617c25 100644
--- a/homeassistant/components/plugwise/sensor.py
+++ b/homeassistant/components/plugwise/sensor.py
@@ -263,6 +263,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         device_class=SensorDeviceClass.HUMIDITY,
         state_class=SensorStateClass.MEASUREMENT,
     ),
+    SensorEntityDescription(
+        key="dhw_temperature",
+        name="DHW temperature",
+        native_unit_of_measurement=TEMP_CELSIUS,
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+    ),
 )
 
 
diff --git a/homeassistant/components/plugwise/strings.select.json b/homeassistant/components/plugwise/strings.select.json
index 1c278f44315..92098e3bc35 100644
--- a/homeassistant/components/plugwise/strings.select.json
+++ b/homeassistant/components/plugwise/strings.select.json
@@ -6,6 +6,12 @@
       "cooling": "Cooling",
       "heating": "Heating",
       "off": "Off"
+    },
+    "plugwise__dhw_mode": {
+      "off": "Off",
+      "auto": "Auto",
+      "boost": "Boost",
+      "comfort": "Comfort"
     }
   }
 }
diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py
index c2942308b75..a8e5597efbf 100644
--- a/homeassistant/components/plugwise/switch.py
+++ b/homeassistant/components/plugwise/switch.py
@@ -36,6 +36,12 @@ SWITCHES: tuple[SwitchEntityDescription, ...] = (
         name="Relay",
         device_class=SwitchDeviceClass.SWITCH,
     ),
+    SwitchEntityDescription(
+        key="cooling_ena_switch",
+        name="Cooling",
+        icon="mdi:snowflake-thermometer",
+        entity_category=EntityCategory.CONFIG,
+    ),
 )
 
 
diff --git a/homeassistant/components/plugwise/translations/select.en.json b/homeassistant/components/plugwise/translations/select.en.json
index b71301ba047..c54a933921e 100644
--- a/homeassistant/components/plugwise/translations/select.en.json
+++ b/homeassistant/components/plugwise/translations/select.en.json
@@ -1,11 +1,17 @@
 {
     "state": {
-        "plugwise__regulation_mode": {
-            "bleeding_cold": "Bleeding cold",
-            "bleeding_hot": "Bleeding hot",
-            "cooling": "Cooling",
-            "heating": "Heating",
-            "off": "Off"
-        }
+      "plugwise__regulation_mode": {
+        "bleeding_cold": "Bleeding cold",
+        "bleeding_hot": "Bleeding hot",
+        "cooling": "Cooling",
+        "heating": "Heating",
+        "off": "Off"
+      },
+      "plugwise__dhw_mode": {
+        "off": "Off",
+        "auto": "Auto",
+        "boost": "Boost",
+        "comfort": "Comfort"
+      }
     }
-}
\ No newline at end of file
+  }
\ No newline at end of file
diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py
index 2b4baccc7c9..9039c5a476e 100644
--- a/tests/components/plugwise/test_sensor.py
+++ b/tests/components/plugwise/test_sensor.py
@@ -47,6 +47,10 @@ async def test_anna_as_smt_climate_sensor_entities(
     assert state
     assert float(state.state) == 29.1
 
+    state = hass.states.get("sensor.opentherm_dhw_temperature")
+    assert state
+    assert float(state.state) == 46.3
+
     state = hass.states.get("sensor.anna_illuminance")
     assert state
     assert float(state.state) == 86.0
-- 
GitLab


From c4bbc439a56e85ca9c29f215a4971a105aab3ed9 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Wed, 19 Oct 2022 12:41:43 +0200
Subject: [PATCH 596/985] Integrations v2.1: Differentiating hubs, devices and
 services (#80524)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
---
 .../components/adguard/manifest.json          |    1 +
 .../components/config/config_entries.py       |   21 +-
 .../components/esphome/manifest.json          |    1 +
 homeassistant/components/hue/manifest.json    |    1 +
 homeassistant/generated/integrations.json     | 1106 ++++++++++++++++-
 homeassistant/loader.py                       |   17 +-
 script/hassfest/config_flow.py                |   13 +-
 script/hassfest/manifest.py                   |   12 +-
 script/hassfest/model.py                      |    2 +-
 .../components/bluetooth/test_config_flow.py  |    3 -
 .../components/config/test_config_entries.py  |  302 ++++-
 11 files changed, 1375 insertions(+), 104 deletions(-)

diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json
index cf1210b0884..91e1393c734 100644
--- a/homeassistant/components/adguard/manifest.json
+++ b/homeassistant/components/adguard/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["adguardhome==0.5.1"],
   "codeowners": ["@frenck"],
   "iot_class": "local_polling",
+  "integration_type": "service",
   "loggers": ["adguardhome"]
 }
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index 6f2320a2e93..50bcad45db7 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -15,6 +15,7 @@ from homeassistant.components import websocket_api
 from homeassistant.components.http import HomeAssistantView
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import DependencyError, Unauthorized
+import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.data_entry_flow import (
     FlowManagerIndexView,
     FlowManagerResourceView,
@@ -64,7 +65,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
             domain = request.query["domain"]
         type_filter = None
         if "type" in request.query:
-            type_filter = request.query["type"]
+            type_filter = [request.query["type"]]
         return self.json(await async_matching_config_entries(hass, type_filter, domain))
 
 
@@ -405,7 +406,7 @@ async def ignore_config_flow(
 @websocket_api.websocket_command(
     {
         vol.Required("type"): "config_entries/get",
-        vol.Optional("type_filter"): str,
+        vol.Optional("type_filter"): vol.All(cv.ensure_list, [str]),
         vol.Optional("domain"): str,
     }
 )
@@ -427,7 +428,7 @@ async def config_entries_get(
 @websocket_api.websocket_command(
     {
         vol.Required("type"): "config_entries/subscribe",
-        vol.Optional("type_filter"): str,
+        vol.Optional("type_filter"): vol.All(cv.ensure_list, [str]),
     }
 )
 @websocket_api.async_response
@@ -445,7 +446,7 @@ async def config_entries_subscribe(
         """Forward config entry state events to websocket."""
         if type_filter:
             integration = await async_get_integration(hass, entry.domain)
-            if integration.integration_type != type_filter:
+            if integration.integration_type not in type_filter:
                 return
 
         connection.send_message(
@@ -475,7 +476,7 @@ async def config_entries_subscribe(
 
 
 async def async_matching_config_entries(
-    hass: HomeAssistant, type_filter: str | None, domain: str | None
+    hass: HomeAssistant, type_filter: list[str] | None, domain: str | None
 ) -> list[dict[str, Any]]:
     """Return matching config entries by type and/or domain."""
     kwargs = {}
@@ -483,7 +484,7 @@ async def async_matching_config_entries(
         kwargs["domain"] = domain
     entries = hass.config_entries.async_entries(**kwargs)
 
-    if type_filter is None:
+    if not type_filter:
         return [entry_json(entry) for entry in entries]
 
     integrations = {}
@@ -499,13 +500,17 @@ async def async_matching_config_entries(
         elif not isinstance(integration_or_exc, IntegrationNotFound):
             raise integration_or_exc
 
+    # Filter out entries that don't match the type filter
+    # when only helpers are requested, also filter out entries
+    # from unknown integrations. This prevent them from showing
+    # up in the helpers UI.
     entries = [
         entry
         for entry in entries
-        if (type_filter != "helper" and entry.domain not in integrations)
+        if (type_filter != ["helper"] and entry.domain not in integrations)
         or (
             entry.domain in integrations
-            and integrations[entry.domain].integration_type == type_filter
+            and integrations[entry.domain].integration_type in type_filter
         )
     ]
 
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index 3ffceaae971..75aad44894f 100644
--- a/homeassistant/components/esphome/manifest.json
+++ b/homeassistant/components/esphome/manifest.json
@@ -9,5 +9,6 @@
   "codeowners": ["@OttoWinter", "@jesserockz"],
   "after_dependencies": ["bluetooth", "zeroconf", "tag"],
   "iot_class": "local_push",
+  "integration_type": "device",
   "loggers": ["aioesphomeapi", "noiseprotocol"]
 }
diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json
index 3854b861c98..d726f773b9b 100644
--- a/homeassistant/components/hue/manifest.json
+++ b/homeassistant/components/hue/manifest.json
@@ -25,5 +25,6 @@
   "codeowners": ["@balloob", "@marcelveldt"],
   "quality_scale": "platinum",
   "iot_class": "local_push",
+  "integration_type": "hub",
   "loggers": ["aiohue"]
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 9e8441dd242..f4c3c9cc76b 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -3,71 +3,85 @@
     "abode": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Abode"
     },
     "accuweather": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "AccuWeather"
     },
     "acer_projector": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Acer Projector"
     },
     "acmeda": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Rollease Acmeda Automate"
     },
     "actiontec": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Actiontec"
     },
     "adax": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Adax"
     },
     "adguard": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "service",
       "name": "AdGuard Home"
     },
     "ads": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "ADS"
     },
     "advantage_air": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Advantage Air"
     },
     "aemet": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "AEMET OpenData"
     },
     "aftership": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "AfterShip"
     },
     "agent_dvr": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Agent DVR"
     },
     "airly": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Airly"
     },
     "airnow": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "AirNow"
     },
     "airthings": {
@@ -76,11 +90,13 @@
         "airthings": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Airthings"
         },
         "airthings_ble": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Airthings BLE"
         }
       }
@@ -88,41 +104,49 @@
     "airtouch4": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "AirTouch 4"
     },
     "airvisual": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "AirVisual"
     },
     "airzone": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Airzone"
     },
     "aladdin_connect": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Aladdin Connect"
     },
     "alarmdecoder": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "AlarmDecoder"
     },
     "alert": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Alert"
     },
     "almond": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Almond"
     },
     "alpha_vantage": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Alpha Vantage"
     },
     "amazon": {
@@ -131,21 +155,25 @@
         "alexa": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Amazon Alexa"
         },
         "amazon_polly": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Amazon Polly"
         },
         "aws": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Amazon Web Services (AWS)"
         },
         "route53": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "AWS Route53"
         }
       }
@@ -153,56 +181,67 @@
     "amberelectric": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Amber Electric"
     },
     "ambiclimate": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Ambiclimate"
     },
     "ambient_station": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Ambient Weather Station"
     },
     "amcrest": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Amcrest"
     },
     "ampio": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Ampio Smart Smog System"
     },
     "android_ip_webcam": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Android IP Webcam"
     },
     "androidtv": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Android TV"
     },
     "anel_pwrctrl": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Anel NET-PwrCtrl"
     },
     "anthemav": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Anthem A/V Receivers"
     },
     "apache_kafka": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Apache Kafka"
     },
     "apcupsd": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "APC UPS Daemon"
     },
     "apple": {
@@ -211,30 +250,36 @@
         "apple_tv": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Apple TV"
         },
         "homekit_controller": {
           "config_flow": true,
-          "iot_class": "local_push"
+          "iot_class": "local_push",
+          "integration_type": "hub"
         },
         "homekit": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "HomeKit"
         },
         "ibeacon": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "iBeacon Tracker"
         },
         "icloud": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Apple iCloud"
         },
         "itunes": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Apple iTunes"
         }
       }
@@ -242,36 +287,43 @@
     "apprise": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Apprise"
     },
     "aprs": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "APRS"
     },
     "aqualogic": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "AquaLogic"
     },
     "aquostv": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Sharp Aquos TV"
     },
     "arcam_fmj": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Arcam FMJ Receivers"
     },
     "arest": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "aREST"
     },
     "arris_tg2492lg": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Arris TG2492LG"
     },
     "aruba": {
@@ -280,11 +332,13 @@
         "aruba": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Aruba"
         },
         "cppm_tracker": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Aruba ClearPass"
         }
       }
@@ -292,11 +346,13 @@
     "arwn": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ambient Radio Weather Network"
     },
     "aseko_pool_live": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Aseko Pool Live"
     },
     "asterisk": {
@@ -305,11 +361,13 @@
         "asterisk_cdr": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Asterisk Call Detail Records"
         },
         "asterisk_mbox": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Asterisk Voicemail"
         }
       }
@@ -317,21 +375,25 @@
     "asuswrt": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "ASUSWRT"
     },
     "atag": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Atag"
     },
     "aten_pe": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "ATEN Rack PDU"
     },
     "atome": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Atome Linky"
     },
     "august": {
@@ -340,226 +402,271 @@
         "august": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "August"
         },
         "yalexs_ble": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Yale Access Bluetooth"
         }
       }
     },
     "aurora": {
       "config_flow": true,
-      "iot_class": "cloud_polling"
+      "iot_class": "cloud_polling",
+      "integration_type": "hub"
     },
     "aurora_abb_powerone": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Aurora ABB PowerOne Solar PV"
     },
     "aussie_broadband": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Aussie Broadband"
     },
     "avion": {
       "config_flow": false,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "Avi-on"
     },
     "awair": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Awair"
     },
     "axis": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Axis"
     },
     "baf": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Big Ass Fans"
     },
     "baidu": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Baidu"
     },
     "balboa": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Balboa Spa Client"
     },
     "bayesian": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Bayesian"
     },
     "bbox": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Bbox"
     },
     "beewi_smartclim": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "BeeWi SmartClim BLE sensor"
     },
     "bitcoin": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Bitcoin"
     },
     "bizkaibus": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Bizkaibus"
     },
     "blackbird": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Monoprice Blackbird Matrix Switch"
     },
     "blebox": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "BleBox devices"
     },
     "blink": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Blink"
     },
     "blinksticklight": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "BlinkStick"
     },
     "blockchain": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Blockchain.com"
     },
     "bloomsky": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "BloomSky"
     },
     "bluemaestro": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "BlueMaestro"
     },
     "bluesound": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Bluesound"
     },
     "bluetooth": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Bluetooth"
     },
     "bluetooth_le_tracker": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Bluetooth LE Tracker"
     },
     "bluetooth_tracker": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Bluetooth Tracker"
     },
     "bmw_connected_drive": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "BMW Connected Drive"
     },
     "bond": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Bond"
     },
     "bosch_shc": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Bosch SHC"
     },
     "broadlink": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Broadlink"
     },
     "brother": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Brother Printer"
     },
     "brottsplatskartan": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Brottsplatskartan"
     },
     "browser": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Browser"
     },
     "brunt": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Brunt Blind Engine"
     },
     "bsblan": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "BSB-Lan"
     },
     "bt_home_hub_5": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "BT Home Hub 5"
     },
     "bt_smarthub": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "BT Smart Hub"
     },
     "bthome": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "BTHome"
     },
     "buienradar": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Buienradar"
     },
     "caldav": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "CalDAV"
     },
     "canary": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Canary"
     },
     "cert_expiry": {
       "config_flow": true,
-      "iot_class": "cloud_polling"
+      "iot_class": "cloud_polling",
+      "integration_type": "hub"
     },
     "channels": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Channels"
     },
     "circuit": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Unify Circuit"
     },
     "cisco": {
@@ -568,16 +675,19 @@
         "cisco_ios": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Cisco IOS"
         },
         "cisco_mobility_express": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Cisco Mobility Express"
         },
         "cisco_webex_teams": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Cisco Webex Teams"
         }
       }
@@ -585,16 +695,19 @@
     "citybikes": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "CityBikes"
     },
     "clementine": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Clementine Music Player"
     },
     "clickatell": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Clickatell"
     },
     "clicksend": {
@@ -603,11 +716,13 @@
         "clicksend": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "ClickSend SMS"
         },
         "clicksend_tts": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "ClickSend TTS"
         }
       }
@@ -615,150 +730,180 @@
     "cloud": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Home Assistant Cloud"
     },
     "cloudflare": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Cloudflare"
     },
     "cmus": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "cmus"
     },
     "co2signal": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "CO2 Signal"
     },
     "coinbase": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Coinbase"
     },
     "color_extractor": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "ColorExtractor"
     },
     "comed_hourly_pricing": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "ComEd Hourly Pricing"
     },
     "comfoconnect": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Zehnder ComfoAir Q"
     },
     "command_line": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Command Line"
     },
     "compensation": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "Compensation"
     },
     "concord232": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Concord232"
     },
     "control4": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Control4"
     },
     "coolmaster": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "CoolMasterNet"
     },
     "coronavirus": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Coronavirus (COVID-19)"
     },
     "cpuspeed": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "hub"
     },
     "crownstone": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Crownstone"
     },
     "cups": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "CUPS"
     },
     "currencylayer": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "currencylayer"
     },
     "daikin": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Daikin AC"
     },
     "danfoss_air": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Danfoss Air"
     },
     "darksky": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Dark Sky"
     },
     "datadog": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Datadog"
     },
     "ddwrt": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "DD-WRT"
     },
     "debugpy": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Remote Python Debugger"
     },
     "deconz": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "deCONZ"
     },
     "decora": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Leviton Decora"
     },
     "decora_wifi": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Leviton Decora Wi-Fi"
     },
     "delijn": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "De Lijn"
     },
     "deluge": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Deluge"
     },
     "demo": {
       "config_flow": false,
-      "iot_class": "calculated"
+      "iot_class": "calculated",
+      "integration_type": "hub"
     },
     "denon": {
       "name": "Denon",
@@ -766,16 +911,19 @@
         "denon": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Denon Network Receivers"
         },
         "denonavr": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Denon AVR Network Receivers"
         },
         "heos": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Denon HEOS"
         }
       }
@@ -783,11 +931,13 @@
     "deutsche_bahn": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Deutsche Bahn"
     },
     "device_sun_light_trigger": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "Presence-based Lights"
     },
     "devolo": {
@@ -796,11 +946,13 @@
         "devolo_home_control": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "devolo Home Control"
         },
         "devolo_home_network": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "devolo Home Network"
         }
       }
@@ -808,41 +960,49 @@
     "dexcom": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Dexcom"
     },
     "digital_ocean": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Digital Ocean"
     },
     "directv": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "DirecTV"
     },
     "discogs": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Discogs"
     },
     "discord": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Discord"
     },
     "dlib_face_detect": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Dlib Face Detect"
     },
     "dlib_face_identify": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Dlib Face Identify"
     },
     "dlink": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "D-Link Wi-Fi Smart Plugs"
     },
     "dlna": {
@@ -851,11 +1011,13 @@
         "dlna_dmr": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "DLNA Digital Media Renderer"
         },
         "dlna_dms": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "DLNA Digital Media Server"
         }
       }
@@ -863,141 +1025,169 @@
     "dnsip": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "DNS IP"
     },
     "dominos": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Dominos Pizza"
     },
     "doods": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "DOODS - Dedicated Open Object Detection Service"
     },
     "doorbird": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "DoorBird"
     },
     "dovado": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Dovado"
     },
     "downloader": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Downloader"
     },
     "dsmr": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "DSMR Slimme Meter"
     },
     "dsmr_reader": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "DSMR Reader"
     },
     "dte_energy_bridge": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "DTE Energy Bridge"
     },
     "dublin_bus_transport": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Dublin Bus"
     },
     "duckdns": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Duck DNS"
     },
     "dunehd": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Dune HD"
     },
     "dwd_weather_warnings": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Deutscher Wetterdienst (DWD) Weather Warnings"
     },
     "dweet": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "dweet.io"
     },
     "eafm": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Environment Agency Flood Gauges"
     },
     "ebox": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "EBox"
     },
     "ebusd": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "ebusd"
     },
     "ecoal_boiler": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "eSterownik eCoal.pl Boiler"
     },
     "ecobee": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "ecobee"
     },
     "econet": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Rheem EcoNet Products"
     },
     "ecovacs": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Ecovacs"
     },
     "ecowitt": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Ecowitt"
     },
     "eddystone_temperature": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Eddystone"
     },
     "edimax": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Edimax"
     },
     "edl21": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "EDL21"
     },
     "efergy": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Efergy"
     },
     "egardia": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Egardia"
     },
     "eight_sleep": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Eight Sleep"
     },
     "elgato": {
@@ -1006,11 +1196,13 @@
         "avea": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Elgato Avea"
         },
         "elgato": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Elgato Light"
         }
       }
@@ -1018,26 +1210,31 @@
     "eliqonline": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Eliqonline"
     },
     "elkm1": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Elk-M1 Control"
     },
     "elmax": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Elmax"
     },
     "elv": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "ELV PCA"
     },
     "emby": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Emby"
     },
     "emoncms": {
@@ -1046,11 +1243,13 @@
         "emoncms": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Emoncms"
         },
         "emoncms_history": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Emoncms History"
         }
       }
@@ -1058,55 +1257,66 @@
     "emonitor": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SiteSage Emonitor"
     },
     "emulated_hue": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Emulated Hue"
     },
     "emulated_kasa": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Emulated Kasa"
     },
     "emulated_roku": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "hub"
     },
     "enigma2": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Enigma2 (OpenWebif)"
     },
     "enocean": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "EnOcean"
     },
     "enphase_envoy": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Enphase Envoy"
     },
     "entur_public_transport": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Entur"
     },
     "environment_canada": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Environment Canada"
     },
     "envisalink": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Envisalink"
     },
     "ephember": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "EPH Controls"
     },
     "epson": {
@@ -1115,11 +1325,13 @@
         "epson": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Epson"
         },
         "epsonworkforce": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Epson Workforce"
         }
       }
@@ -1130,11 +1342,13 @@
         "eq3btsmart": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "eQ-3 Bluetooth Smart Thermostats"
         },
         "maxcube": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "eQ-3 MAX!"
         }
       }
@@ -1142,66 +1356,79 @@
     "escea": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Escea"
     },
     "esphome": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "device",
       "name": "ESPHome"
     },
     "etherscan": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Etherscan"
     },
     "eufy": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "eufy"
     },
     "everlights": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "EverLights"
     },
     "evil_genius_labs": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Evil Genius Labs"
     },
     "ezviz": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "EZVIZ"
     },
     "faa_delays": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "FAA Delays"
     },
     "facebook": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Facebook Messenger"
     },
     "facebox": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Facebox"
     },
     "fail2ban": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Fail2Ban"
     },
     "fastdotcom": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Fast.com"
     },
     "feedreader": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Feedreader"
     },
     "ffmpeg": {
@@ -1210,16 +1437,19 @@
         "ffmpeg": {
           "config_flow": false,
           "iot_class": null,
+          "integration_type": "hub",
           "name": "FFmpeg"
         },
         "ffmpeg_motion": {
           "config_flow": false,
           "iot_class": "calculated",
+          "integration_type": "hub",
           "name": "FFmpeg Motion"
         },
         "ffmpeg_noise": {
           "config_flow": false,
           "iot_class": "calculated",
+          "integration_type": "hub",
           "name": "FFmpeg Noise"
         }
       }
@@ -1227,170 +1457,204 @@
     "fibaro": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Fibaro"
     },
     "fido": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Fido"
     },
     "file": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "File"
     },
     "filesize": {
       "config_flow": true,
-      "iot_class": "local_polling"
+      "iot_class": "local_polling",
+      "integration_type": "hub"
     },
     "filter": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Filter"
     },
     "fints": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "FinTS"
     },
     "fireservicerota": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "FireServiceRota"
     },
     "firmata": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Firmata"
     },
     "fitbit": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Fitbit"
     },
     "fivem": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "FiveM"
     },
     "fixer": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Fixer"
     },
     "fjaraskupan": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Fj\u00e4r\u00e5skupan"
     },
     "fleetgo": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "FleetGO"
     },
     "flexit": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Flexit"
     },
     "flic": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Flic"
     },
     "flick_electric": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Flick Electric"
     },
     "flipr": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Flipr"
     },
     "flo": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Flo"
     },
     "flock": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Flock"
     },
     "flume": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Flume"
     },
     "flux": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "Flux"
     },
     "flux_led": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Magic Home"
     },
     "folder": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Folder"
     },
     "folder_watcher": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Folder Watcher"
     },
     "foobot": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Foobot"
     },
     "forecast_solar": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Forecast.Solar"
     },
     "forked_daapd": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Owntone"
     },
     "fortios": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "FortiOS"
     },
     "foscam": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Foscam"
     },
     "foursquare": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Foursquare"
     },
     "free_mobile": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Free Mobile"
     },
     "freebox": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Freebox"
     },
     "freedns": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "FreeDNS"
     },
     "freedompro": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Freedompro"
     },
     "fritzbox": {
@@ -1399,16 +1663,19 @@
         "fritz": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "AVM FRITZ!Box Tools"
         },
         "fritzbox": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "AVM FRITZ!SmartHome"
         },
         "fritzbox_callmonitor": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "AVM FRITZ!Box Call Monitor"
         }
       }
@@ -1416,75 +1683,90 @@
     "fronius": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Fronius"
     },
     "frontier_silicon": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Frontier Silicon"
     },
     "fully_kiosk": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Fully Kiosk Browser"
     },
     "futurenow": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "P5 FutureNow"
     },
     "garadget": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Garadget"
     },
     "garages_amsterdam": {
       "config_flow": true,
-      "iot_class": "cloud_polling"
+      "iot_class": "cloud_polling",
+      "integration_type": "hub"
     },
     "gdacs": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Global Disaster Alert and Coordination System (GDACS)"
     },
     "generic": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Generic Camera"
     },
     "generic_hygrostat": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Generic hygrostat"
     },
     "generic_thermostat": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Generic Thermostat"
     },
     "geniushub": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Genius Hub"
     },
     "geo_json_events": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "GeoJSON"
     },
     "geo_rss_events": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "GeoRSS"
     },
     "geocaching": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Geocaching"
     },
     "geofency": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Geofency"
     },
     "geonet": {
@@ -1493,11 +1775,13 @@
         "geonetnz_quakes": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "GeoNet NZ Quakes"
         },
         "geonetnz_volcano": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "GeoNet NZ Volcano"
         }
       }
@@ -1505,26 +1789,31 @@
     "gios": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "GIO\u015a"
     },
     "github": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "GitHub"
     },
     "gitlab_ci": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "GitLab-CI"
     },
     "gitter": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Gitter"
     },
     "glances": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Glances"
     },
     "globalcache": {
@@ -1533,11 +1822,13 @@
         "gc100": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Global Cach\u00e9 GC-100"
         },
         "itach": {
           "config_flow": false,
           "iot_class": "assumed_state",
+          "integration_type": "hub",
           "name": "Global Cach\u00e9 iTach TCP/IP to IR"
         }
       }
@@ -1545,21 +1836,25 @@
     "goalfeed": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Goalfeed"
     },
     "goalzero": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Goal Zero Yeti"
     },
     "gogogate2": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Gogogate2 and ismartgate"
     },
     "goodwe": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "GoodWe Inverter"
     },
     "google": {
@@ -1568,70 +1863,84 @@
         "google_assistant": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Google Assistant"
         },
         "google_cloud": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Google Cloud Platform"
         },
         "google_domains": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Google Domains"
         },
         "google_maps": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Google Maps"
         },
         "google_pubsub": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Google Pub/Sub"
         },
         "google_sheets": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Google Sheets"
         },
         "google_translate": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Google Translate Text-to-Speech"
         },
         "google_travel_time": {
           "config_flow": true,
-          "iot_class": "cloud_polling"
+          "iot_class": "cloud_polling",
+          "integration_type": "hub"
         },
         "google_wifi": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Google Wifi"
         },
         "google": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Google Calendar"
         },
         "nest": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Google Nest"
         },
         "cast": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Google Cast"
         },
         "hangouts": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Google Chat"
         },
         "dialogflow": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Dialogflow"
         }
       }
@@ -1639,95 +1948,120 @@
     "govee_ble": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Govee Bluetooth"
     },
     "gpsd": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "GPSD"
     },
     "gpslogger": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "GPSLogger"
     },
     "graphite": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Graphite"
     },
     "gree": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Gree Climate"
     },
     "greeneye_monitor": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "GreenEye Monitor (GEM)"
     },
     "greenwave": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Greenwave Reality"
     },
     "growatt_server": {
       "config_flow": true,
-      "iot_class": "cloud_polling"
+      "iot_class": "cloud_polling",
+      "integration_type": "hub"
     },
     "gstreamer": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "GStreamer"
     },
     "gtfs": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "General Transit Feed Specification (GTFS)"
     },
     "guardian": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Elexa Guardian"
     },
     "habitica": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Habitica"
     },
+    "hardkernel": {
+      "config_flow": false,
+      "iot_class": null,
+      "integration_type": "hardware",
+      "name": "Hardkernel"
+    },
     "harman_kardon_avr": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Harman Kardon AVR"
     },
     "hassio": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Home Assistant Supervisor"
     },
     "haveibeenpwned": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "HaveIBeenPwned"
     },
     "hddtemp": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "hddtemp"
     },
     "hdmi_cec": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "HDMI-CEC"
     },
     "heatmiser": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Heatmiser"
     },
     "here_travel_time": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "HERE Travel Time"
     },
     "hikvision": {
@@ -1736,11 +2070,13 @@
         "hikvision": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Hikvision"
         },
         "hikvisioncam": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Hikvision"
         }
       }
@@ -1748,54 +2084,76 @@
     "hisense_aehw4a1": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Hisense AEH-W4A1"
     },
     "history_stats": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "History Stats"
     },
     "hitron_coda": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Rogers Hitron CODA"
     },
     "hive": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Hive"
     },
     "hlk_sw16": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Hi-Link HLK-SW16"
     },
     "home_connect": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Home Connect"
     },
     "home_plus_control": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Legrand Home+ Control"
     },
     "homeassistant_alerts": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Home Assistant Alerts"
     },
+    "homeassistant_sky_connect": {
+      "config_flow": false,
+      "iot_class": null,
+      "integration_type": "hardware",
+      "name": "Home Assistant Sky Connect"
+    },
+    "homeassistant_yellow": {
+      "config_flow": false,
+      "iot_class": null,
+      "integration_type": "hardware",
+      "name": "Home Assistant Yellow"
+    },
     "homematic": {
       "name": "Homematic",
       "integrations": {
         "homematic": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Homematic"
         },
         "homematicip_cloud": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "HomematicIP Cloud"
         }
       }
@@ -1803,6 +2161,7 @@
     "homewizard": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "HomeWizard Energy"
     },
     "honeywell": {
@@ -1811,16 +2170,19 @@
         "lyric": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Honeywell Lyric"
         },
         "evohome": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Honeywell Total Connect Comfort (Europe)"
         },
         "honeywell": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Honeywell Total Connect Comfort (US)"
         }
       }
@@ -1828,61 +2190,73 @@
     "horizon": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Unitymedia Horizon HD Recorder"
     },
     "hp_ilo": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "HP Integrated Lights-Out (ILO)"
     },
     "html5": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "HTML5 Push Notifications"
     },
     "huawei_lte": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Huawei LTE"
     },
     "huisbaasje": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Huisbaasje"
     },
     "hunterdouglas_powerview": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Hunter Douglas PowerView"
     },
     "hvv_departures": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "HVV Departures"
     },
     "hydrawise": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Hunter Hydrawise"
     },
     "hyperion": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Hyperion"
     },
     "ialarm": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Antifurto365 iAlarm"
     },
     "iammeter": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "IamMeter"
     },
     "iaqualink": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Jandy iAqualink"
     },
     "ibm": {
@@ -1891,11 +2265,13 @@
         "watson_iot": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "IBM Watson IoT Platform"
         },
         "watson_tts": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "IBM Watson TTS"
         }
       }
@@ -1903,51 +2279,61 @@
     "idteck_prox": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "IDTECK Proximity Reader"
     },
     "ifttt": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "IFTTT"
     },
     "iglo": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "iGlo"
     },
     "ign_sismologia": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "IGN Sismolog\u00eda"
     },
     "ihc": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "IHC Controller"
     },
     "imap": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "IMAP"
     },
     "imap_email_content": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "IMAP Email Content"
     },
     "incomfort": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Intergas InComfort/Intouch Lan2RF gateway"
     },
     "influxdb": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "InfluxDB"
     },
     "inkbird": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "INKBIRD"
     },
     "inovelli": {
@@ -1960,75 +2346,90 @@
     "insteon": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Insteon"
     },
     "intellifire": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "IntelliFire"
     },
     "intent_script": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Intent Script"
     },
     "intesishome": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "IntesisHome"
     },
     "ios": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Home Assistant iOS"
     },
     "iotawatt": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "IoTaWatt"
     },
     "iperf3": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Iperf3"
     },
     "ipma": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)"
     },
     "ipp": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Internet Printing Protocol (IPP)"
     },
     "iqvia": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "IQVIA"
     },
     "irish_rail_transport": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Irish Rail Transport"
     },
     "islamic_prayer_times": {
       "config_flow": true,
-      "iot_class": "cloud_polling"
+      "iot_class": "cloud_polling",
+      "integration_type": "hub"
     },
     "iss": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "International Space Station (ISS)"
     },
     "isy994": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Universal Devices ISY994"
     },
     "izone": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "iZone"
     },
     "jasco": {
@@ -2040,176 +2441,211 @@
     "jellyfin": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Jellyfin"
     },
     "jewish_calendar": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "Jewish Calendar"
     },
     "joaoapps_join": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Joaoapps Join"
     },
     "juicenet": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "JuiceNet"
     },
     "justnimbus": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "JustNimbus"
     },
     "kaiterra": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Kaiterra"
     },
     "kaleidescape": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Kaleidescape"
     },
     "kankun": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Kankun"
     },
     "keba": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Keba Charging Station"
     },
     "keenetic_ndms2": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Keenetic NDMS2 Router"
     },
     "kef": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "KEF"
     },
     "kegtron": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Kegtron"
     },
     "keyboard": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Keyboard"
     },
     "keyboard_remote": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Keyboard Remote"
     },
     "keymitt_ble": {
       "config_flow": true,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "Keymitt MicroBot Push"
     },
     "kira": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Kira"
     },
     "kiwi": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "KIWI"
     },
     "kmtronic": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "KMtronic"
     },
     "knx": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "KNX"
     },
     "kodi": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Kodi"
     },
     "konnected": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Konnected.io"
     },
     "kostal_plenticore": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Kostal Plenticore Solar Inverter"
     },
     "kraken": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Kraken"
     },
     "kulersky": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Kuler Sky"
     },
     "kwb": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "KWB Easyfire"
     },
     "lacrosse": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "LaCrosse"
     },
     "lacrosse_view": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "LaCrosse View"
     },
     "lametric": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "LaMetric"
     },
     "landisgyr_heat_meter": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Landis+Gyr Heat Meter"
     },
     "lannouncer": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "LANnouncer"
     },
     "lastfm": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Last.fm"
     },
     "launch_library": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Launch Library"
     },
     "laundrify": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "laundrify"
     },
     "lcn": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "LCN"
     },
     "led_ble": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "LED BLE"
     },
     "leviton": {
@@ -2224,16 +2660,19 @@
         "lg_netcast": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "LG Netcast"
         },
         "lg_soundbar": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "LG Soundbars"
         },
         "webostv": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "LG webOS Smart TV"
         }
       }
@@ -2241,90 +2680,108 @@
     "lidarr": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Lidarr"
     },
     "life360": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Life360"
     },
     "lifx": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "LIFX"
     },
     "lifx_cloud": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "LIFX Cloud"
     },
     "lightwave": {
       "config_flow": false,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "Lightwave"
     },
     "limitlessled": {
       "config_flow": false,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "LimitlessLED"
     },
     "linksys_smart": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Linksys Smart Wi-Fi"
     },
     "linode": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Linode"
     },
     "linux_battery": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Linux Battery"
     },
     "lirc": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "LIRC"
     },
     "litejet": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "LiteJet"
     },
     "litterrobot": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Litter-Robot"
     },
     "llamalab_automate": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "LlamaLab Automate"
     },
     "local_file": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Local File"
     },
     "local_ip": {
       "config_flow": true,
-      "iot_class": "local_polling"
+      "iot_class": "local_polling",
+      "integration_type": "hub"
     },
     "locative": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Locative"
     },
     "logentries": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Logentries"
     },
     "logi_circle": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Logi Circle"
     },
     "logitech": {
@@ -2333,16 +2790,19 @@
         "harmony": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Logitech Harmony Hub"
         },
         "ue_smart_radio": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Logitech UE Smart Radio"
         },
         "squeezebox": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Squeezebox (Logitech Media Server)"
         }
       }
@@ -2350,26 +2810,31 @@
     "london_air": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "London Air"
     },
     "london_underground": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "London Underground"
     },
     "lookin": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "LOOKin"
     },
     "luftdaten": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sensor.Community"
     },
     "lupusec": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Lupus Electronics LUPUSEC"
     },
     "lutron": {
@@ -2378,16 +2843,19 @@
         "lutron": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Lutron"
         },
         "lutron_caseta": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Lutron Cas\u00e9ta"
         },
         "homeworks": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Lutron Homeworks"
         }
       }
@@ -2395,71 +2863,85 @@
     "lw12wifi": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "LAGUTE LW-12"
     },
     "magicseaweed": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Magicseaweed"
     },
     "mailgun": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Mailgun"
     },
     "manual": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "Manual Alarm Control Panel"
     },
     "map": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Map"
     },
     "marytts": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "MaryTTS"
     },
     "mastodon": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Mastodon"
     },
     "matrix": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Matrix"
     },
     "mazda": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Mazda Connected Services"
     },
     "meater": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Meater"
     },
     "media_extractor": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "Media Extractor"
     },
     "mediaroom": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Mediaroom"
     },
     "melcloud": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "MELCloud"
     },
     "melissa": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Melissa"
     },
     "melnor": {
@@ -2468,11 +2950,13 @@
         "melnor": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Melnor Bluetooth"
         },
         "raincloud": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Melnor RainCloud"
         }
       }
@@ -2480,46 +2964,55 @@
     "meraki": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Meraki"
     },
     "message_bird": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "MessageBird"
     },
     "met": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Meteorologisk institutt (Met.no)"
     },
     "met_eireann": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Met \u00c9ireann"
     },
     "meteo_france": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "M\u00e9t\u00e9o-France"
     },
     "meteoalarm": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "MeteoAlarm"
     },
     "meteoclimatic": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Meteoclimatic"
     },
     "metoffice": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Met Office"
     },
     "mfi": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ubiquiti mFi mPort"
     },
     "microsoft": {
@@ -2528,51 +3021,61 @@
         "azure_devops": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Azure DevOps"
         },
         "azure_event_hub": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Azure Event Hub"
         },
         "azure_service_bus": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Azure Service Bus"
         },
         "microsoft_face_detect": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Microsoft Face Detect"
         },
         "microsoft_face_identify": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Microsoft Face Identify"
         },
         "microsoft_face": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Microsoft Face"
         },
         "microsoft": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Microsoft Text-to-Speech (TTS)"
         },
         "msteams": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Microsoft Teams"
         },
         "xbox": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Xbox"
         },
         "xbox_live": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Xbox Live"
         }
       }
@@ -2580,98 +3083,118 @@
     "miflora": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Mi Flora"
     },
     "mikrotik": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Mikrotik"
     },
     "mill": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Mill"
     },
     "minecraft_server": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Minecraft Server"
     },
     "minio": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Minio"
     },
     "mitemp_bt": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor"
     },
     "mjpeg": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "MJPEG IP Camera"
     },
     "moat": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Moat"
     },
     "mobile_app": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "hub"
     },
     "mochad": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Mochad"
     },
     "modbus": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Modbus"
     },
     "modem_callerid": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Phone Modem"
     },
     "modern_forms": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Modern Forms"
     },
     "moehlenhoff_alpha2": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "hub"
     },
     "mold_indicator": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Mold Indicator"
     },
     "monoprice": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Monoprice 6-Zone Amplifier"
     },
     "moon": {
       "config_flow": true,
-      "iot_class": "local_polling"
+      "iot_class": "local_polling",
+      "integration_type": "hub"
     },
     "motion_blinds": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Motion Blinds"
     },
     "motioneye": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "motionEye"
     },
     "mpd": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Music Player Daemon (MPD)"
     },
     "mqtt": {
@@ -2680,31 +3203,37 @@
         "manual_mqtt": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Manual MQTT Alarm Control Panel"
         },
         "mqtt": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "MQTT"
         },
         "mqtt_eventstream": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "MQTT Eventstream"
         },
         "mqtt_json": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "MQTT JSON"
         },
         "mqtt_room": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "MQTT Room Presence"
         },
         "mqtt_statestream": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "MQTT Statestream"
         }
       }
@@ -2712,86 +3241,103 @@
     "mullvad": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Mullvad VPN"
     },
     "mutesync": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "mutesync"
     },
     "mvglive": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "MVG"
     },
     "mycroft": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Mycroft"
     },
     "myq": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "MyQ"
     },
     "mysensors": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "MySensors"
     },
     "mystrom": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "myStrom"
     },
     "mythicbeastsdns": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Mythic Beasts DNS"
     },
     "nad": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "NAD"
     },
     "nam": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Nettigo Air Monitor"
     },
     "namecheapdns": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Namecheap FreeDNS"
     },
     "nanoleaf": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Nanoleaf"
     },
     "neato": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Neato Botvac"
     },
     "nederlandse_spoorwegen": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Nederlandse Spoorwegen (NS)"
     },
     "ness_alarm": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Ness Alarm"
     },
     "netatmo": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Netatmo"
     },
     "netdata": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Netdata"
     },
     "netgear": {
@@ -2800,11 +3346,13 @@
         "netgear": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "NETGEAR"
         },
         "netgear_lte": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "NETGEAR LTE"
         }
       }
@@ -2812,285 +3360,342 @@
     "netio": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Netio"
     },
     "neurio_energy": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Neurio energy"
     },
     "nexia": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Nexia/American Standard/Trane"
     },
     "nextbus": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "NextBus"
     },
     "nextcloud": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Nextcloud"
     },
     "nextdns": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "NextDNS"
     },
     "nfandroidtv": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Notifications for Android TV / Fire TV"
     },
     "nibe_heatpump": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Nibe Heat Pump"
     },
     "nightscout": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Nightscout"
     },
     "niko_home_control": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Niko Home Control"
     },
     "nilu": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Norwegian Institute for Air Research (NILU)"
     },
     "nina": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "NINA"
     },
     "nissan_leaf": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Nissan Leaf"
     },
     "nmap_tracker": {
       "config_flow": true,
-      "iot_class": "local_polling"
+      "iot_class": "local_polling",
+      "integration_type": "hub"
     },
     "nmbs": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "NMBS"
     },
     "no_ip": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "No-IP.com"
     },
     "noaa_tides": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "NOAA Tides"
     },
     "nobo_hub": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Nob\u00f8 Ecohub"
     },
     "norway_air": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Om Luftkvalitet i Norge (Norway Air)"
     },
     "notify_events": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Notify.Events"
     },
     "notion": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Notion"
     },
     "nsw_fuel_station": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "NSW Fuel Station Price"
     },
     "nsw_rural_fire_service_feed": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "NSW Rural Fire Service Incidents"
     },
     "nuheat": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "NuHeat"
     },
     "nuki": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Nuki"
     },
     "numato": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Numato USB GPIO Expander"
     },
     "nut": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Network UPS Tools (NUT)"
     },
     "nws": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "National Weather Service (NWS)"
     },
     "nx584": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "NX584"
     },
     "nzbget": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "NZBGet"
     },
     "oasa_telematics": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "OASA Telematics"
     },
     "obihai": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Obihai"
     },
     "octoprint": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "OctoPrint"
     },
     "oem": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "OpenEnergyMonitor WiFi Thermostat"
     },
     "ohmconnect": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "OhmConnect"
     },
     "ombi": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ombi"
     },
     "omnilogic": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Hayward Omnilogic"
     },
     "oncue": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Oncue by Kohler"
     },
     "ondilo_ico": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Ondilo ICO"
     },
     "onewire": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "1-Wire"
     },
     "onkyo": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Onkyo"
     },
     "onvif": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "ONVIF"
     },
     "open_meteo": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Open-Meteo"
     },
     "openalpr_cloud": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "OpenALPR Cloud"
     },
     "openalpr_local": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "OpenALPR Local"
     },
     "opencv": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "OpenCV"
     },
     "openerz": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Open ERZ"
     },
     "openevse": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "OpenEVSE"
     },
     "openexchangerates": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Open Exchange Rates"
     },
     "opengarage": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "OpenGarage"
     },
     "openhardwaremonitor": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Open Hardware Monitor"
     },
     "openhome": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Linn / OpenHome"
     },
     "opensensemap": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "openSenseMap"
     },
     "opensky": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "OpenSky Network"
     },
     "opentherm_gw": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "OpenTherm Gateway"
     },
     "openuv": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "OpenUV"
     },
     "openweathermap": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "OpenWeatherMap"
     },
     "openwrt": {
@@ -3099,11 +3704,13 @@
         "luci": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "OpenWrt (luci)"
         },
         "ubus": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "OpenWrt (ubus)"
         }
       }
@@ -3111,51 +3718,61 @@
     "opnsense": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "OPNSense"
     },
     "opple": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Opple"
     },
     "oru": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Orange and Rockland Utility (ORU)"
     },
     "orvibo": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Orvibo"
     },
     "osramlightify": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Osramlightify"
     },
     "otp": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "One-Time Password (OTP)"
     },
     "overkiz": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Overkiz"
     },
     "ovo_energy": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "OVO Energy"
     },
     "owntracks": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "OwnTracks"
     },
     "p1_monitor": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "P1 Monitor"
     },
     "panasonic": {
@@ -3164,11 +3781,13 @@
         "panasonic_bluray": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Panasonic Blu-Ray Player"
         },
         "panasonic_viera": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Panasonic Viera"
         }
       }
@@ -3176,31 +3795,37 @@
     "pandora": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Pandora"
     },
     "panel_custom": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Custom Panel"
     },
     "panel_iframe": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "iframe Panel"
     },
     "peco": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "PECO Outage Counter"
     },
     "pencom": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Pencom"
     },
     "persistent_notification": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Persistent Notification"
     },
     "philips": {
@@ -3209,16 +3834,19 @@
         "dynalite": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Philips Dynalite"
         },
         "hue": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Philips Hue"
         },
         "philips_js": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Philips TV"
         }
       }
@@ -3226,189 +3854,227 @@
     "pi_hole": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Pi-hole"
     },
     "picnic": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Picnic"
     },
     "picotts": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Pico TTS"
     },
     "pilight": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Pilight"
     },
     "ping": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ping (ICMP)"
     },
     "pioneer": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Pioneer"
     },
     "pjlink": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "PJLink"
     },
     "plaato": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Plaato"
     },
     "plant": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "hub"
     },
     "plex": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Plex Media Server"
     },
     "plugwise": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Plugwise"
     },
     "plum_lightpad": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Plum Lightpad"
     },
     "pocketcasts": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Pocket Casts"
     },
     "point": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Minut Point"
     },
     "poolsense": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "PoolSense"
     },
     "profiler": {
       "config_flow": true,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Profiler"
     },
     "progettihwsw": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "ProgettiHWSW Automation"
     },
     "proliphix": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Proliphix"
     },
     "prometheus": {
       "config_flow": false,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "Prometheus"
     },
     "prosegur": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Prosegur Alarm"
     },
     "prowl": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Prowl"
     },
     "proximity": {
       "config_flow": false,
-      "iot_class": "calculated"
+      "iot_class": "calculated",
+      "integration_type": "hub"
     },
     "proxmoxve": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Proxmox VE"
     },
     "proxy": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Camera Proxy"
     },
     "prusalink": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "PrusaLink"
     },
     "pulseaudio_loopback": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "PulseAudio Loopback"
     },
     "pure_energie": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Pure Energie"
     },
     "push": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Push"
     },
     "pushbullet": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Pushbullet"
     },
     "pushover": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Pushover"
     },
     "pushsafer": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Pushsafer"
     },
     "pvoutput": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "PVOutput"
     },
     "pvpc_hourly_pricing": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Spain electricity hourly pricing (PVPC)"
     },
     "pyload": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "pyLoad"
     },
     "python_script": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Python Scripts"
     },
     "qbittorrent": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "qBittorrent"
     },
     "qingping": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Qingping"
     },
     "qld_bushfire": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Queensland Bushfire Alert"
     },
     "qnap": {
@@ -3417,11 +4083,13 @@
         "qnap": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "QNAP"
         },
         "qnap_qsw": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "QNAP QSW"
         }
       }
@@ -3429,61 +4097,73 @@
     "qrcode": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "QR Code"
     },
     "quantum_gateway": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Quantum Gateway"
     },
     "qvr_pro": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "QVR Pro"
     },
     "qwikswitch": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "QwikSwitch QSUSB"
     },
     "rachio": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Rachio"
     },
     "radarr": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Radarr"
     },
     "radio_browser": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Radio Browser"
     },
     "radiotherm": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Radio Thermostat"
     },
     "rainbird": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Rain Bird"
     },
     "rainforest_eagle": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Rainforest Eagle"
     },
     "rainmachine": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "RainMachine"
     },
     "random": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Random"
     },
     "raspberry": {
@@ -3492,162 +4172,200 @@
         "rpi_camera": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Raspberry Pi Camera"
         },
         "rpi_power": {
           "config_flow": true,
-          "iot_class": "local_polling"
+          "iot_class": "local_polling",
+          "integration_type": "hub"
         },
         "remote_rpi_gpio": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "remote_rpi_gpio"
         }
       }
     },
+    "raspberry_pi": {
+      "config_flow": false,
+      "iot_class": null,
+      "integration_type": "hardware",
+      "name": "Raspberry Pi"
+    },
     "raspyrfm": {
       "config_flow": false,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "RaspyRFM"
     },
     "rdw": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "RDW"
     },
     "recollect_waste": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "ReCollect Waste"
     },
     "recswitch": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ankuoo REC Switch"
     },
     "reddit": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Reddit"
     },
     "rejseplanen": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Rejseplanen"
     },
     "remember_the_milk": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Remember The Milk"
     },
     "renault": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Renault"
     },
     "repetier": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Repetier-Server"
     },
     "rest": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "RESTful"
     },
     "rest_command": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "RESTful Command"
     },
     "rflink": {
       "config_flow": false,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "RFLink"
     },
     "rfxtrx": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "RFXCOM RFXtrx"
     },
     "rhasspy": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Rhasspy"
     },
     "ridwell": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Ridwell"
     },
     "ring": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Ring"
     },
     "ripple": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Ripple"
     },
     "risco": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Risco"
     },
     "rituals_perfume_genie": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Rituals Perfume Genie"
     },
     "rmvtransport": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "RMV"
     },
     "rocketchat": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Rocket.Chat"
     },
     "roku": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Roku"
     },
     "roomba": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "iRobot Roomba and Braava"
     },
     "roon": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "RoonLabs music player"
     },
     "rova": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "ROVA"
     },
     "rss_feed_template": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "RSS Feed Template"
     },
     "rtorrent": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "rTorrent"
     },
     "rtsp_to_webrtc": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "RTSPtoWebRTC"
     },
     "ruckus_unleashed": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ruckus Unleashed"
     },
     "russound": {
@@ -3656,11 +4374,13 @@
         "russound_rio": {
           "config_flow": false,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Russound RIO"
         },
         "russound_rnet": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Russound RNET"
         }
       }
@@ -3668,11 +4388,13 @@
     "sabnzbd": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SABnzbd"
     },
     "saj": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SAJ Solar Inverter"
     },
     "samsung": {
@@ -3681,16 +4403,19 @@
         "familyhub": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Samsung Family Hub"
         },
         "samsungtv": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Samsung Smart TV"
         },
         "syncthru": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Samsung SyncThru Printer"
         }
       }
@@ -3698,270 +4423,324 @@
     "satel_integra": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Satel Integra"
     },
     "schluter": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Schluter"
     },
     "scrape": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Scrape"
     },
     "screenlogic": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Pentair ScreenLogic"
     },
     "scsgate": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SCSGate"
     },
     "season": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Season"
     },
     "sendgrid": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "SendGrid"
     },
     "sense": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sense"
     },
     "senseme": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "SenseME"
     },
     "sensibo": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sensibo"
     },
     "sensorpro": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "SensorPro"
     },
     "sensorpush": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "SensorPush"
     },
     "sentry": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sentry"
     },
     "senz": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "nVent RAYCHEM SENZ"
     },
     "serial": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Serial"
     },
     "serial_pm": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Serial Particulate Matter"
     },
     "sesame": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sesame Smart Lock"
     },
     "seven_segments": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Seven Segments OCR"
     },
     "seventeentrack": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "17TRACK"
     },
     "sharkiq": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Shark IQ"
     },
     "shell_command": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Shell Command"
     },
     "shelly": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Shelly"
     },
     "shiftr": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "shiftr.io"
     },
     "shodan": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Shodan"
     },
     "shopping_list": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "hub"
     },
     "sia": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "SIA Alarm Systems"
     },
     "sigfox": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sigfox"
     },
     "sighthound": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sighthound"
     },
     "signal_messenger": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Signal Messenger"
     },
     "simplepush": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Simplepush"
     },
     "simplisafe": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "SimpliSafe"
     },
     "simulated": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Simulated"
     },
     "sinch": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Sinch SMS"
     },
     "sisyphus": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Sisyphus"
     },
     "sky_hub": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Sky Hub"
     },
     "skybeacon": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Skybeacon"
     },
     "skybell": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "SkyBell"
     },
     "slack": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Slack"
     },
     "sleepiq": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "SleepIQ"
     },
     "slide": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Slide"
     },
     "slimproto": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "SlimProto (Squeezebox players)"
     },
     "sma": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SMA Solar"
     },
     "smappee": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Smappee"
     },
     "smart_meter_texas": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Smart Meter Texas"
     },
     "smartthings": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "SmartThings"
     },
     "smarttub": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "SmartTub"
     },
     "smarty": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Salda Smarty"
     },
     "smhi": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "SMHI"
     },
     "sms": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SMS notifications via GSM-modem"
     },
     "smtp": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "SMTP"
     },
     "snapcast": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Snapcast"
     },
     "snips": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Snips"
     },
     "snmp": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SNMP"
     },
     "snooz": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Snooz"
     },
     "solaredge": {
@@ -3970,11 +4749,13 @@
         "solaredge": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "SolarEdge"
         },
         "solaredge_local": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "SolarEdge Local"
         }
       }
@@ -3982,31 +4763,37 @@
     "solarlog": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Solar-Log"
     },
     "solax": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SolaX Power"
     },
     "soma": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Soma Connect"
     },
     "somfy_mylink": {
       "config_flow": true,
       "iot_class": "assumed_state",
+      "integration_type": "hub",
       "name": "Somfy MyLink"
     },
     "sonarr": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Sonarr"
     },
     "sonos": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Sonos"
     },
     "sony": {
@@ -4015,21 +4802,25 @@
         "braviatv": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Sony Bravia TV"
         },
         "ps4": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Sony PlayStation 4"
         },
         "sony_projector": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Sony Projector"
         },
         "songpal": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Sony Songpal"
         }
       }
@@ -4037,170 +4828,204 @@
     "soundtouch": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Bose SoundTouch"
     },
     "spaceapi": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Space API"
     },
     "spc": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Vanderbilt SPC"
     },
     "speedtestdotnet": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Speedtest.net"
     },
     "spider": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Itho Daalderop Spider"
     },
     "splunk": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Splunk"
     },
     "spotify": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Spotify"
     },
     "sql": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SQL"
     },
     "srp_energy": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "SRP Energy"
     },
     "starline": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "StarLine"
     },
     "starlingbank": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Starling Bank"
     },
     "startca": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Start.ca"
     },
     "statistics": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Statistics"
     },
     "statsd": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "StatsD"
     },
     "steam_online": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Steam"
     },
     "steamist": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Steamist"
     },
     "stiebel_eltron": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "STIEBEL ELTRON"
     },
     "stookalert": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "RIVM Stookalert"
     },
     "stream": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Stream"
     },
     "streamlabswater": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "StreamLabs"
     },
     "subaru": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Subaru"
     },
     "suez_water": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Suez Water"
     },
     "sun": {
       "config_flow": true,
-      "iot_class": "calculated"
+      "iot_class": "calculated",
+      "integration_type": "hub"
     },
     "supervisord": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Supervisord"
     },
     "supla": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Supla"
     },
     "surepetcare": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Sure Petcare"
     },
     "swiss_hydrological_data": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Swiss Hydrological Data"
     },
     "swiss_public_transport": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Swiss public transport"
     },
     "swisscom": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Swisscom Internet-Box"
     },
     "switchbee": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "SwitchBee"
     },
     "switchbot": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "SwitchBot"
     },
     "switcher_kis": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Switcher"
     },
     "switchmate": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Switchmate SimplySmart Home"
     },
     "syncthing": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Syncthing"
     },
     "synology": {
@@ -4209,16 +5034,19 @@
         "synology_chat": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Synology Chat"
         },
         "synology_dsm": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Synology DSM"
         },
         "synology_srm": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Synology SRM"
         }
       }
@@ -4226,70 +5054,84 @@
     "syslog": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Syslog"
     },
     "system_bridge": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "System Bridge"
     },
     "system_log": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "System Log"
     },
     "systemmonitor": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "System Monitor"
     },
     "tado": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Tado"
     },
     "tag": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "hub"
     },
     "tailscale": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Tailscale"
     },
     "tank_utility": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Tank Utility"
     },
     "tankerkoenig": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Tankerkoenig"
     },
     "tapsaff": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Taps Aff"
     },
     "tasmota": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Tasmota"
     },
     "tautulli": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Tautulli"
     },
     "tcp": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "TCP"
     },
     "ted5000": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "The Energy Detective TED5000"
     },
     "telegram": {
@@ -4298,11 +5140,13 @@
         "telegram": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Telegram"
         },
         "telegram_bot": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Telegram bot"
         }
       }
@@ -4313,11 +5157,13 @@
         "tellduslive": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Telldus Live"
         },
         "tellstick": {
           "config_flow": false,
           "iot_class": "assumed_state",
+          "integration_type": "hub",
           "name": "TellStick"
         }
       }
@@ -4325,21 +5171,25 @@
     "telnet": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Telnet"
     },
     "temper": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "TEMPer"
     },
     "template": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Template"
     },
     "tensorflow": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "TensorFlow"
     },
     "tesla": {
@@ -4348,11 +5198,13 @@
         "powerwall": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Tesla Powerwall"
         },
         "tesla_wall_connector": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Tesla Wall Connector"
         }
       }
@@ -4360,36 +5212,43 @@
     "tfiac": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Tfiac"
     },
     "thermobeacon": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "ThermoBeacon"
     },
     "thermopro": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "ThermoPro"
     },
     "thermoworks_smoke": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "ThermoWorks Smoke"
     },
     "thethingsnetwork": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "The Things Network"
     },
     "thingspeak": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "ThingSpeak"
     },
     "thinkingcleaner": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Thinking Cleaner"
     },
     "third_reality": {
@@ -4401,101 +5260,121 @@
     "thomson": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Thomson"
     },
     "tibber": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Tibber"
     },
     "tikteck": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Tikteck"
     },
     "tile": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Tile"
     },
     "tilt_ble": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Tilt Hydrometer BLE"
     },
     "time_date": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Time & Date"
     },
     "tmb": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Transports Metropolitans de Barcelona"
     },
     "todoist": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Todoist"
     },
     "tolo": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "TOLO Sauna"
     },
     "tomato": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Tomato"
     },
     "tomorrowio": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Tomorrow.io"
     },
     "toon": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Toon"
     },
     "torque": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Torque"
     },
     "totalconnect": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Total Connect"
     },
     "touchline": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Roth Touchline"
     },
     "tplink": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "TP-Link Kasa Smart"
     },
     "tplink_lte": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "TP-Link LTE"
     },
     "traccar": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Traccar"
     },
     "tractive": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Tractive"
     },
     "tradfri": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "IKEA TR\u00c5DFRI"
     },
     "trafikverket": {
@@ -4504,16 +5383,19 @@
         "trafikverket_ferry": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Trafikverket Ferry"
         },
         "trafikverket_train": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Trafikverket Train"
         },
         "trafikverket_weatherstation": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Trafikverket Weather Station"
         }
       }
@@ -4521,31 +5403,37 @@
     "transmission": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Transmission"
     },
     "transport_nsw": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Transport NSW"
     },
     "travisci": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Travis-CI"
     },
     "trend": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Trend"
     },
     "tuya": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Tuya"
     },
     "twentemilieu": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Twente Milieu"
     },
     "twilio": {
@@ -4554,16 +5442,19 @@
         "twilio": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Twilio"
         },
         "twilio_call": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Twilio Call"
         },
         "twilio_sms": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Twilio SMS"
         }
       }
@@ -4571,16 +5462,19 @@
     "twinkly": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Twinkly"
     },
     "twitch": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Twitch"
     },
     "twitter": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Twitter"
     },
     "u_tec": {
@@ -4595,21 +5489,25 @@
         "unifi": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "UniFi Network"
         },
         "unifi_direct": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "UniFi AP"
         },
         "unifiled": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "UniFi LED"
         },
         "unifiprotect": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "UniFi Protect"
         }
       }
@@ -4617,130 +5515,156 @@
     "uk_transport": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "UK Transport"
     },
     "ukraine_alarm": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Ukraine Alarm"
     },
     "universal": {
       "config_flow": false,
       "iot_class": "calculated",
+      "integration_type": "hub",
       "name": "Universal Media Player"
     },
     "upb": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Universal Powerline Bus (UPB)"
     },
     "upc_connect": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "UPC Connect Box"
     },
     "upcloud": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "UpCloud"
     },
     "upnp": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "UPnP/IGD"
     },
     "uptime": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "hub"
     },
     "uptimerobot": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "UptimeRobot"
     },
     "usgs_earthquakes_feed": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "U.S. Geological Survey Earthquake Hazards (USGS)"
     },
     "uvc": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ubiquiti UniFi Video"
     },
     "vallox": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Vallox"
     },
     "vasttrafik": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "V\u00e4sttrafik"
     },
     "velbus": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Velbus"
     },
     "velux": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Velux"
     },
     "venstar": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Venstar"
     },
     "vera": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Vera"
     },
     "verisure": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Verisure"
     },
     "versasense": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "VersaSense"
     },
     "version": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Version"
     },
     "vesync": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "VeSync"
     },
     "viaggiatreno": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Trenitalia ViaggiaTreno"
     },
     "vicare": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Viessmann ViCare"
     },
     "vilfo": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Vilfo Router"
     },
     "vivotek": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "VIVOTEK"
     },
     "vizio": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "VIZIO SmartCast"
     },
     "vlc": {
@@ -4749,11 +5673,13 @@
         "vlc": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "VLC media player"
         },
         "vlc_telnet": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "VLC media player via Telnet"
         }
       }
@@ -4761,160 +5687,192 @@
     "voicerss": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "VoiceRSS"
     },
     "volkszaehler": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Volkszaehler"
     },
     "volumio": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Volumio"
     },
     "volvooncall": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Volvo On Call"
     },
     "vulcan": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Uonet+ Vulcan"
     },
     "vultr": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Vultr"
     },
     "w800rf32": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "WGL Designs W800RF32"
     },
     "wake_on_lan": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Wake on LAN"
     },
     "wallbox": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Wallbox"
     },
     "waqi": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "World Air Quality Index (WAQI)"
     },
     "waterfurnace": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "WaterFurnace"
     },
     "watttime": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "WattTime"
     },
     "waze_travel_time": {
       "config_flow": true,
-      "iot_class": "cloud_polling"
+      "iot_class": "cloud_polling",
+      "integration_type": "hub"
     },
     "webhook": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Webhook"
     },
     "wemo": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Belkin WeMo"
     },
     "whirlpool": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Whirlpool Sixth Sense"
     },
     "whois": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Whois"
     },
     "wiffi": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Wiffi"
     },
     "wilight": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "WiLight"
     },
     "wirelesstag": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Wireless Sensor Tags"
     },
     "withings": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Withings"
     },
     "wiz": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "WiZ"
     },
     "wled": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "WLED"
     },
     "wolflink": {
       "config_flow": true,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Wolf SmartSet Service"
     },
     "workday": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Workday"
     },
     "worldclock": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Worldclock"
     },
     "worldtidesinfo": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "World Tides"
     },
     "worxlandroid": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Worx Landroid"
     },
     "ws66i": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Soundavo WS66i 6-Zone Amplifier"
     },
     "wsdot": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Washington State Department of Transportation (WSDOT)"
     },
     "x10": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Heyu X10"
     },
     "xeoma": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Xeoma"
     },
     "xiaomi": {
@@ -4923,26 +5881,31 @@
         "xiaomi_aqara": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Xiaomi Gateway (Aqara)"
         },
         "xiaomi_ble": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Xiaomi BLE"
         },
         "xiaomi_miio": {
           "config_flow": true,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Xiaomi Miio"
         },
         "xiaomi_tv": {
           "config_flow": false,
           "iot_class": "assumed_state",
+          "integration_type": "hub",
           "name": "Xiaomi TV"
         },
         "xiaomi": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Xiaomi"
         }
       }
@@ -4950,11 +5913,13 @@
     "xmpp": {
       "config_flow": false,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "Jabber (XMPP)"
     },
     "xs1": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "EZcontrol XS1"
     },
     "yale": {
@@ -4963,16 +5928,19 @@
         "august": {
           "config_flow": true,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "August"
         },
         "yale_smart_alarm": {
           "config_flow": true,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Yale Smart Living"
         },
         "yalexs_ble": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Yale Access Bluetooth"
         }
       }
@@ -4980,11 +5948,13 @@
     "yamaha": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Yamaha Network Receivers"
     },
     "yamaha_musiccast": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "MusicCast"
     },
     "yandex": {
@@ -4993,11 +5963,13 @@
         "yandex_transport": {
           "config_flow": false,
           "iot_class": "cloud_polling",
+          "integration_type": "hub",
           "name": "Yandex Transport"
         },
         "yandextts": {
           "config_flow": false,
           "iot_class": "cloud_push",
+          "integration_type": "hub",
           "name": "Yandex TTS"
         }
       }
@@ -5008,11 +5980,13 @@
         "yeelight": {
           "config_flow": true,
           "iot_class": "local_push",
+          "integration_type": "hub",
           "name": "Yeelight"
         },
         "yeelightsunflower": {
           "config_flow": false,
           "iot_class": "local_polling",
+          "integration_type": "hub",
           "name": "Yeelight Sunflower"
         }
       }
@@ -5020,71 +5994,85 @@
     "yi": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Yi Home Cameras"
     },
     "yolink": {
       "config_flow": true,
       "iot_class": "cloud_push",
+      "integration_type": "hub",
       "name": "YoLink"
     },
     "youless": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "YouLess"
     },
     "zabbix": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Zabbix"
     },
     "zamg": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Zentralanstalt f\u00fcr Meteorologie und Geodynamik (ZAMG)"
     },
     "zengge": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Zengge"
     },
     "zerproc": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Zerproc"
     },
     "zestimate": {
       "config_flow": false,
       "iot_class": "cloud_polling",
+      "integration_type": "hub",
       "name": "Zestimate"
     },
     "zha": {
       "config_flow": true,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Zigbee Home Automation"
     },
     "zhong_hong": {
       "config_flow": false,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "ZhongHong"
     },
     "ziggo_mediabox_xl": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Ziggo Mediabox XL"
     },
     "zodiac": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "Zodiac"
     },
     "zone": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "hub",
       "name": "Zone"
     },
     "zoneminder": {
       "config_flow": false,
       "iot_class": "local_polling",
+      "integration_type": "hub",
       "name": "ZoneMinder"
     },
     "zooz": {
@@ -5096,107 +6084,105 @@
     "zwave_js": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Z-Wave"
     },
     "zwave_me": {
       "config_flow": true,
       "iot_class": "local_push",
+      "integration_type": "hub",
       "name": "Z-Wave.Me"
     }
   },
-  "hardware": {
-    "hardkernel": {
-      "config_flow": false,
-      "iot_class": null,
-      "name": "Hardkernel"
-    },
-    "homeassistant_sky_connect": {
-      "config_flow": false,
-      "iot_class": null,
-      "name": "Home Assistant Sky Connect"
-    },
-    "homeassistant_yellow": {
-      "config_flow": false,
-      "iot_class": null,
-      "name": "Home Assistant Yellow"
-    },
-    "raspberry_pi": {
-      "config_flow": false,
-      "iot_class": null,
-      "name": "Raspberry Pi"
-    }
-  },
+  "hardware": {},
   "helper": {
     "counter": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "helper",
       "name": "Counter"
     },
     "derivative": {
       "config_flow": true,
-      "iot_class": "calculated"
+      "iot_class": "calculated",
+      "integration_type": "helper"
     },
     "group": {
       "config_flow": true,
-      "iot_class": "calculated"
+      "iot_class": "calculated",
+      "integration_type": "helper"
     },
     "input_boolean": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "helper"
     },
     "input_button": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "helper",
       "name": "Input Button"
     },
     "input_datetime": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "helper"
     },
     "input_number": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "helper"
     },
     "input_select": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "helper"
     },
     "input_text": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "helper"
     },
     "integration": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "helper"
     },
     "min_max": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "helper"
     },
     "schedule": {
       "config_flow": false,
-      "iot_class": null
+      "iot_class": null,
+      "integration_type": "helper"
     },
     "switch_as_x": {
       "config_flow": true,
-      "iot_class": "calculated"
+      "iot_class": "calculated",
+      "integration_type": "helper"
     },
     "threshold": {
       "config_flow": true,
-      "iot_class": "local_polling"
+      "iot_class": "local_polling",
+      "integration_type": "helper"
     },
     "timer": {
       "config_flow": false,
       "iot_class": null,
+      "integration_type": "helper",
       "name": "Timer"
     },
     "tod": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "helper"
     },
     "utility_meter": {
       "config_flow": true,
-      "iot_class": "local_push"
+      "iot_class": "local_push",
+      "integration_type": "helper"
     }
   },
   "translated_name": [
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index f2a7948f4f5..e90d0b42036 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -130,7 +130,9 @@ class Manifest(TypedDict, total=False):
     name: str
     disabled: str
     domain: str
-    integration_type: Literal["entity", "integration", "hardware", "helper", "system"]
+    integration_type: Literal[
+        "entity", "device", "hardware", "helper", "hub", "service", "system"
+    ]
     dependencies: list[str]
     after_dependencies: list[str]
     requirements: list[str]
@@ -224,7 +226,7 @@ async def async_get_custom_components(
 
 async def async_get_config_flows(
     hass: HomeAssistant,
-    type_filter: Literal["helper", "integration"] | None = None,
+    type_filter: Literal["device", "helper", "hub", "service"] | None = None,
 ) -> set[str]:
     """Return cached list of config flows."""
     # pylint: disable=import-outside-toplevel
@@ -262,9 +264,11 @@ async def async_get_integration_descriptions(
     core_flows: dict[str, Any] = json_loads(flow)
     custom_integrations = await async_get_custom_components(hass)
     custom_flows: dict[str, Any] = {
-        "integration": {},
+        "device": {},
         "hardware": {},
         "helper": {},
+        "hub": {},
+        "service": {},
     }
 
     for integration in custom_integrations.values():
@@ -272,7 +276,7 @@ async def async_get_integration_descriptions(
         if integration.integration_type in ("entity", "system"):
             continue
 
-        for integration_type in ("integration", "hardware", "helper"):
+        for integration_type in ("device", "hardware", "helper", "hub", "service"):
             if integration.domain not in core_flows[integration_type]:
                 continue
             del core_flows[integration_type][integration.domain]
@@ -281,6 +285,7 @@ async def async_get_integration_descriptions(
 
         metadata = {
             "config_flow": integration.config_flow,
+            "integration_type": integration.integration_type,
             "iot_class": integration.iot_class,
             "name": integration.name,
         }
@@ -599,9 +604,9 @@ class Integration:
     @property
     def integration_type(
         self,
-    ) -> Literal["entity", "integration", "hardware", "helper", "system"]:
+    ) -> Literal["entity", "device", "hardware", "helper", "hub", "service", "system"]:
         """Return the integration type."""
-        return self.manifest.get("integration_type", "integration")
+        return self.manifest.get("integration_type", "hub")
 
     @property
     def mqtt(self) -> list[str] | None:
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index 81cc5fae829..6905cd60d49 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -86,7 +86,10 @@ def _generate_and_validate(integrations: dict[str, Integration], config: Config)
 
         _validate_integration(config, integration)
 
-        domains[integration.integration_type].append(domain)
+        if integration.integration_type == "helper":
+            domains["helper"].append(domain)
+        else:
+            domains["integration"].append(domain)
 
     return black.format_str(BASE.format(to_string(domains)), mode=black.Mode())
 
@@ -106,6 +109,7 @@ def _populate_brand_integrations(
         metadata = {}
         metadata["config_flow"] = integration.config_flow
         metadata["iot_class"] = integration.iot_class
+        metadata["integration_type"] = integration.integration_type
         if integration.translated_name:
             integration_data["translated_name"].add(domain)
         else:
@@ -169,11 +173,16 @@ def _generate_integrations(
                 continue
             metadata["config_flow"] = integration.config_flow
             metadata["iot_class"] = integration.iot_class
+            metadata["integration_type"] = integration.integration_type
             if integration.translated_name:
                 result["translated_name"].add(domain)
             else:
                 metadata["name"] = integration.name
-            result[integration.integration_type][domain] = metadata
+
+            if integration.integration_type == "helper":
+                result["helper"][domain] = metadata
+            else:
+                result["integration"][domain] = metadata
 
     return json.dumps(
         result | {"translated_name": sorted(result["translated_name"])}, indent=2
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index de227cbb59c..616faeb995e 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -162,8 +162,16 @@ MANIFEST_SCHEMA = vol.Schema(
     {
         vol.Required("domain"): str,
         vol.Required("name"): str,
-        vol.Optional("integration_type"): vol.In(
-            ["entity", "hardware", "helper", "system"]
+        vol.Optional("integration_type", default="hub"): vol.In(
+            [
+                "device",
+                "entity",
+                "hardware",
+                "helper",
+                "hub",
+                "service",
+                "system",
+            ]
         ),
         vol.Optional("config_flow"): bool,
         vol.Optional("mqtt"): [str],
diff --git a/script/hassfest/model.py b/script/hassfest/model.py
index 6a7ab64c14f..b2459f931c6 100644
--- a/script/hassfest/model.py
+++ b/script/hassfest/model.py
@@ -177,7 +177,7 @@ class Integration:
     @property
     def integration_type(self) -> str:
         """Get integration_type."""
-        return self.manifest.get("integration_type", "integration")
+        return self.manifest.get("integration_type", "hub")
 
     @property
     def iot_class(self) -> str | None:
diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py
index 4c1e8f660b3..69619ba76a7 100644
--- a/tests/components/bluetooth/test_config_flow.py
+++ b/tests/components/bluetooth/test_config_flow.py
@@ -37,7 +37,6 @@ async def test_options_flow_disabled_not_setup(
             "id": 5,
             "type": "config_entries/get",
             "domain": "bluetooth",
-            "type_filter": "integration",
         }
     )
     response = await ws_client.receive_json()
@@ -341,7 +340,6 @@ async def test_options_flow_disabled_macos(
             "id": 5,
             "type": "config_entries/get",
             "domain": "bluetooth",
-            "type_filter": "integration",
         }
     )
     response = await ws_client.receive_json()
@@ -371,7 +369,6 @@ async def test_options_flow_enabled_linux(
             "id": 5,
             "type": "config_entries/get",
             "domain": "bluetooth",
-            "type_filter": "integration",
         }
     )
     response = await ws_client.receive_json()
diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py
index d36f7282bdb..29a2395a926 100644
--- a/tests/components/config/test_config_entries.py
+++ b/tests/components/config/test_config_entries.py
@@ -51,7 +51,15 @@ async def test_get_entries(hass, client, clear_handlers):
     mock_integration(
         hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
     )
-    mock_integration(hass, MockModule("comp3"))
+    mock_integration(
+        hass, MockModule("comp3", partial_manifest={"integration_type": "hub"})
+    )
+    mock_integration(
+        hass, MockModule("comp4", partial_manifest={"integration_type": "device"})
+    )
+    mock_integration(
+        hass, MockModule("comp5", partial_manifest={"integration_type": "service"})
+    )
 
     @HANDLERS.register("comp1")
     class Comp1ConfigFlow:
@@ -91,6 +99,16 @@ async def test_get_entries(hass, client, clear_handlers):
         source="bla3",
         disabled_by=core_ce.ConfigEntryDisabler.USER,
     ).add_to_hass(hass)
+    MockConfigEntry(
+        domain="comp4",
+        title="Test 4",
+        source="bla4",
+    ).add_to_hass(hass)
+    MockConfigEntry(
+        domain="comp5",
+        title="Test 5",
+        source="bla5",
+    ).add_to_hass(hass)
 
     resp = await client.get("/api/config/config_entries/entry")
     assert resp.status == HTTPStatus.OK
@@ -137,6 +155,32 @@ async def test_get_entries(hass, client, clear_handlers):
             "disabled_by": core_ce.ConfigEntryDisabler.USER,
             "reason": None,
         },
+        {
+            "domain": "comp4",
+            "title": "Test 4",
+            "source": "bla4",
+            "state": core_ce.ConfigEntryState.NOT_LOADED.value,
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "disabled_by": None,
+            "reason": None,
+        },
+        {
+            "domain": "comp5",
+            "title": "Test 5",
+            "source": "bla5",
+            "state": core_ce.ConfigEntryState.NOT_LOADED.value,
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "disabled_by": None,
+            "reason": None,
+        },
     ]
 
     resp = await client.get("/api/config/config_entries/entry?domain=comp3")
@@ -150,19 +194,24 @@ async def test_get_entries(hass, client, clear_handlers):
     data = await resp.json()
     assert len(data) == 0
 
-    resp = await client.get(
-        "/api/config/config_entries/entry?domain=comp3&type=integration"
-    )
+    resp = await client.get("/api/config/config_entries/entry?type=hub")
+    assert resp.status == HTTPStatus.OK
+    data = await resp.json()
+    assert len(data) == 2
+    assert data[0]["domain"] == "comp1"
+    assert data[1]["domain"] == "comp3"
+
+    resp = await client.get("/api/config/config_entries/entry?type=device")
     assert resp.status == HTTPStatus.OK
     data = await resp.json()
     assert len(data) == 1
+    assert data[0]["domain"] == "comp4"
 
-    resp = await client.get("/api/config/config_entries/entry?type=integration")
+    resp = await client.get("/api/config/config_entries/entry?type=service")
     assert resp.status == HTTPStatus.OK
     data = await resp.json()
-    assert len(data) == 2
-    assert data[0]["domain"] == "comp1"
-    assert data[1]["domain"] == "comp3"
+    assert len(data) == 1
+    assert data[0]["domain"] == "comp5"
 
 
 async def test_remove_entry(hass, client):
@@ -1123,7 +1172,16 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
     mock_integration(
         hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
     )
-    mock_integration(hass, MockModule("comp3"))
+    mock_integration(
+        hass, MockModule("comp3", partial_manifest={"integration_type": "hub"})
+    )
+    mock_integration(
+        hass, MockModule("comp4", partial_manifest={"integration_type": "device"})
+    )
+    mock_integration(
+        hass, MockModule("comp5", partial_manifest={"integration_type": "service"})
+    )
+
     entry = MockConfigEntry(
         domain="comp1",
         title="Test 1",
@@ -1143,6 +1201,16 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
         source="bla3",
         disabled_by=core_ce.ConfigEntryDisabler.USER,
     ).add_to_hass(hass)
+    MockConfigEntry(
+        domain="comp4",
+        title="Test 4",
+        source="bla4",
+    ).add_to_hass(hass)
+    MockConfigEntry(
+        domain="comp5",
+        title="Test 5",
+        source="bla5",
+    ).add_to_hass(hass)
 
     ws_client = await hass_ws_client(hass)
 
@@ -1197,6 +1265,34 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
             "supports_unload": False,
             "title": "Test 3",
         },
+        {
+            "disabled_by": None,
+            "domain": "comp4",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla4",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 4",
+        },
+        {
+            "disabled_by": None,
+            "domain": "comp5",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla5",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 5",
+        },
     ]
 
     await ws_client.send_json(
@@ -1204,7 +1300,7 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
             "id": 6,
             "type": "config_entries/get",
             "domain": "comp1",
-            "type_filter": "integration",
+            "type_filter": "hub",
         }
     )
     response = await ws_client.receive_json()
@@ -1225,22 +1321,102 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
             "title": "Test 1",
         }
     ]
-    # Verify we skip broken integrations
 
+    await ws_client.send_json(
+        {
+            "id": 7,
+            "type": "config_entries/get",
+            "type_filter": ["service", "device"],
+        }
+    )
+    response = await ws_client.receive_json()
+    assert response["id"] == 7
+    assert response["result"] == [
+        {
+            "disabled_by": None,
+            "domain": "comp4",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla4",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 4",
+        },
+        {
+            "disabled_by": None,
+            "domain": "comp5",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla5",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 5",
+        },
+    ]
+
+    await ws_client.send_json(
+        {
+            "id": 8,
+            "type": "config_entries/get",
+            "type_filter": "hub",
+        }
+    )
+    response = await ws_client.receive_json()
+    assert response["id"] == 8
+    assert response["result"] == [
+        {
+            "disabled_by": None,
+            "domain": "comp1",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 1",
+        },
+        {
+            "disabled_by": "user",
+            "domain": "comp3",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla3",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 3",
+        },
+    ]
+
+    # Verify we skip broken integrations
     with patch(
         "homeassistant.components.config.config_entries.async_get_integration",
         side_effect=IntegrationNotFound("any"),
     ):
         await ws_client.send_json(
             {
-                "id": 7,
+                "id": 9,
                 "type": "config_entries/get",
-                "type_filter": "integration",
+                "type_filter": "hub",
             }
         )
         response = await ws_client.receive_json()
 
-    assert response["id"] == 7
+    assert response["id"] == 9
     assert response["result"] == [
         {
             "disabled_by": None,
@@ -1284,8 +1460,53 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
             "supports_unload": False,
             "title": "Test 3",
         },
+        {
+            "disabled_by": None,
+            "domain": "comp4",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla4",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 4",
+        },
+        {
+            "disabled_by": None,
+            "domain": "comp5",
+            "entry_id": ANY,
+            "pref_disable_new_entities": False,
+            "pref_disable_polling": False,
+            "reason": None,
+            "source": "bla5",
+            "state": "not_loaded",
+            "supports_options": False,
+            "supports_remove_device": False,
+            "supports_unload": False,
+            "title": "Test 5",
+        },
     ]
 
+    # Verify we don't send config entries when only helpers are requested
+    with patch(
+        "homeassistant.components.config.config_entries.async_get_integration",
+        side_effect=IntegrationNotFound("any"),
+    ):
+        await ws_client.send_json(
+            {
+                "id": 10,
+                "type": "config_entries/get",
+                "type_filter": ["helper"],
+            }
+        )
+        response = await ws_client.receive_json()
+
+    assert response["id"] == 10
+    assert response["result"] == []
+
     # Verify we raise if something really goes wrong
 
     with patch(
@@ -1294,14 +1515,14 @@ async def test_get_entries_ws(hass, hass_ws_client, clear_handlers):
     ):
         await ws_client.send_json(
             {
-                "id": 8,
+                "id": 11,
                 "type": "config_entries/get",
-                "type_filter": "integration",
+                "type_filter": ["device", "hub", "service"],
             }
         )
         response = await ws_client.receive_json()
 
-    assert response["id"] == 8
+    assert response["id"] == 11
     assert response["success"] is False
 
 
@@ -1312,7 +1533,9 @@ async def test_subscribe_entries_ws(hass, hass_ws_client, clear_handlers):
     mock_integration(
         hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
     )
-    mock_integration(hass, MockModule("comp3"))
+    mock_integration(
+        hass, MockModule("comp3", partial_manifest={"integration_type": "device"})
+    )
     entry = MockConfigEntry(
         domain="comp1",
         title="Test 1",
@@ -1476,7 +1699,12 @@ async def test_subscribe_entries_ws_filtered(hass, hass_ws_client, clear_handler
     mock_integration(
         hass, MockModule("comp2", partial_manifest={"integration_type": "helper"})
     )
-    mock_integration(hass, MockModule("comp3"))
+    mock_integration(
+        hass, MockModule("comp3", partial_manifest={"integration_type": "device"})
+    )
+    mock_integration(
+        hass, MockModule("comp4", partial_manifest={"integration_type": "service"})
+    )
     entry = MockConfigEntry(
         domain="comp1",
         title="Test 1",
@@ -1491,12 +1719,19 @@ async def test_subscribe_entries_ws_filtered(hass, hass_ws_client, clear_handler
         reason="Unsupported API",
     )
     entry2.add_to_hass(hass)
-    MockConfigEntry(
+    entry3 = MockConfigEntry(
         domain="comp3",
         title="Test 3",
         source="bla3",
         disabled_by=core_ce.ConfigEntryDisabler.USER,
-    ).add_to_hass(hass)
+    )
+    entry3.add_to_hass(hass)
+    entry4 = MockConfigEntry(
+        domain="comp4",
+        title="Test 4",
+        source="bla4",
+    )
+    entry4.add_to_hass(hass)
 
     ws_client = await hass_ws_client(hass)
 
@@ -1504,7 +1739,7 @@ async def test_subscribe_entries_ws_filtered(hass, hass_ws_client, clear_handler
         {
             "id": 5,
             "type": "config_entries/subscribe",
-            "type_filter": "integration",
+            "type_filter": ["hub", "device"],
         }
     )
     response = await ws_client.receive_json()
@@ -1551,6 +1786,8 @@ async def test_subscribe_entries_ws_filtered(hass, hass_ws_client, clear_handler
         },
     ]
     assert hass.config_entries.async_update_entry(entry, title="changed")
+    assert hass.config_entries.async_update_entry(entry3, title="changed too")
+    assert hass.config_entries.async_update_entry(entry4, title="changed but ignored")
     response = await ws_client.receive_json()
     assert response["id"] == 5
     assert response["event"] == [
@@ -1572,6 +1809,27 @@ async def test_subscribe_entries_ws_filtered(hass, hass_ws_client, clear_handler
             "type": "updated",
         }
     ]
+    response = await ws_client.receive_json()
+    assert response["id"] == 5
+    assert response["event"] == [
+        {
+            "entry": {
+                "disabled_by": "user",
+                "domain": "comp3",
+                "entry_id": ANY,
+                "pref_disable_new_entities": False,
+                "pref_disable_polling": False,
+                "reason": None,
+                "source": "bla3",
+                "state": "not_loaded",
+                "supports_options": False,
+                "supports_remove_device": False,
+                "supports_unload": False,
+                "title": "changed too",
+            },
+            "type": "updated",
+        }
+    ]
     await hass.config_entries.async_remove(entry.entry_id)
     await hass.config_entries.async_remove(entry2.entry_id)
     response = await ws_client.receive_json()
-- 
GitLab


From f4951a4f31217568cd467901db9aa2524fd2b697 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 19 Oct 2022 13:04:28 +0200
Subject: [PATCH 597/985] Add CI job which runs recorder tests on MariaDB
 (#80586)

Co-authored-by: Franck Nijhof <git@frenck.dev>
---
 .github/workflows/ci.yaml                     | 103 ++++++++++++++++++
 tests/common.py                               |   4 +-
 tests/components/recorder/test_history.py     |  37 ++++++-
 tests/components/recorder/test_init.py        |  11 +-
 tests/components/recorder/test_purge.py       |  16 ++-
 .../components/recorder/test_system_health.py |  14 ++-
 tests/components/recorder/test_util.py        |  26 ++++-
 .../components/recorder/test_websocket_api.py |  12 +-
 tests/conftest.py                             |  53 ++++++++-
 9 files changed, 250 insertions(+), 26 deletions(-)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 97c87e9b545..53221be7cb8 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -861,6 +861,109 @@ jobs:
         run: |
           ./script/check_dirty
 
+  pytest-mariadb:
+    runs-on: ubuntu-20.04
+    services:
+      mariadb:
+        image: mariadb:10.9.3
+        ports:
+          - 3306:3306
+        env:
+          MYSQL_ROOT_PASSWORD: password
+        options: --health-cmd="mysqladmin ping -uroot -ppassword" --health-interval=5s --health-timeout=2s --health-retries=3
+    if: |
+      (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
+      && github.event.inputs.lint-only != 'true'
+      && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob)
+    needs:
+      - info
+      - base
+      - gen-requirements-all
+      - hassfest
+      - lint-black
+      - lint-other
+      - lint-isort
+      - mypy
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
+    name: >-
+      Run tests Python ${{ matrix.python-version }} (mariadb)
+    steps:
+      - name: Install additional OS dependencies
+        run: |
+          sudo apt-get update
+          sudo apt-get -y install \
+            bluez \
+            ffmpeg \
+            libmariadb-dev-compat
+      - name: Check out code from GitHub
+        uses: actions/checkout@v3.1.0
+      - name: Set up Python ${{ matrix.python-version }}
+        id: python
+        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.11
+        with:
+          path: venv
+          key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
+            needs.info.outputs.python_cache_key }}
+      - name: Fail job if Python cache restore failed
+        if: steps.cache-venv.outputs.cache-hit != 'true'
+        run: |
+          echo "Failed to restore Python virtual environment from cache"
+          exit 1
+      - name: Register Python problem matcher
+        run: |
+          echo "::add-matcher::.github/workflows/matchers/python.json"
+      - name: Install Pytest Annotation plugin
+        run: |
+          . venv/bin/activate
+          # Ideally this should be part of our dependencies
+          # However this plugin is fairly new and doesn't run correctly
+          # on a non-GitHub environment.
+          pip install pytest-github-actions-annotate-failures==0.1.3
+      - name: Register pytest slow test problem matcher
+        run: |
+          echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
+      - name: Install SQL Python libraries
+        run: |
+          . venv/bin/activate
+          pip install mysqlclient sqlalchemy_utils
+      - name: Run pytest (partially)
+        timeout-minutes: 10
+        shell: bash
+        run: |
+          . venv/bin/activate
+          python --version
+
+          python3 -X dev -m pytest \
+            -qq \
+            --timeout=9 \
+            -n 1 \
+            --cov="homeassistant.components.recorder" \
+            --cov-report=xml \
+            --cov-report=term-missing \
+            -o console_output_style=count \
+            --durations=0 \
+            --durations-min=10 \
+            -p no:sugar \
+            --dburl=mysql://root:password@127.0.0.1/homeassistant-test \
+            tests/components/recorder
+      - name: Upload coverage artifact
+        uses: actions/upload-artifact@v3.1.0
+        with:
+          name: coverage-${{ matrix.python-version }}-mariadb
+          path: coverage.xml
+      - name: Check dirty
+        run: |
+          ./script/check_dirty
+
   coverage:
     name: Upload test coverage to Codecov
     runs-on: ubuntu-20.04
diff --git a/tests/common.py b/tests/common.py
index 4ff65049b74..14f3cdd47c2 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -925,11 +925,11 @@ def assert_setup_component(count, domain=None):
 SetupRecorderInstanceT = Callable[..., Awaitable[recorder.Recorder]]
 
 
-def init_recorder_component(hass, add_config=None):
+def init_recorder_component(hass, add_config=None, db_url="sqlite://"):
     """Initialize the recorder."""
     config = dict(add_config) if add_config else {}
     if recorder.CONF_DB_URL not in config:
-        config[recorder.CONF_DB_URL] = "sqlite://"  # In memory DB
+        config[recorder.CONF_DB_URL] = db_url
         if recorder.CONF_COMMIT_INTERVAL not in config:
             config[recorder.CONF_COMMIT_INTERVAL] = 0
 
diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py
index a7b170f0838..6362b83f78a 100644
--- a/tests/components/recorder/test_history.py
+++ b/tests/components/recorder/test_history.py
@@ -254,6 +254,9 @@ def test_state_changes_during_period_descending(hass_recorder):
 
     start = dt_util.utcnow()
     point = start + timedelta(seconds=1)
+    point2 = start + timedelta(seconds=1, microseconds=2)
+    point3 = start + timedelta(seconds=1, microseconds=3)
+    point4 = start + timedelta(seconds=1, microseconds=4)
     end = point + timedelta(seconds=1)
 
     with patch(
@@ -265,12 +268,19 @@ def test_state_changes_during_period_descending(hass_recorder):
     with patch(
         "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point
     ):
-        states = [
-            set_state("idle"),
-            set_state("Netflix"),
-            set_state("Plex"),
-            set_state("YouTube"),
-        ]
+        states = [set_state("idle")]
+    with patch(
+        "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point2
+    ):
+        states.append(set_state("Netflix"))
+    with patch(
+        "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point3
+    ):
+        states.append(set_state("Plex"))
+    with patch(
+        "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point4
+    ):
+        states.append(set_state("YouTube"))
 
     with patch(
         "homeassistant.components.recorder.core.dt_util.utcnow", return_value=end
@@ -652,8 +662,13 @@ def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]:
 async def test_state_changes_during_period_query_during_migration_to_schema_25(
     async_setup_recorder_instance: SetupRecorderInstanceT,
     hass: ha.HomeAssistant,
+    recorder_db_url: str,
 ):
     """Test we can query data prior to schema 25 and during migration to schema 25."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test doesn't run on MySQL / MariaDB; we can't drop table state_attributes
+        return
+
     instance = await async_setup_recorder_instance(hass, {})
 
     start = dt_util.utcnow()
@@ -702,8 +717,13 @@ async def test_state_changes_during_period_query_during_migration_to_schema_25(
 async def test_get_states_query_during_migration_to_schema_25(
     async_setup_recorder_instance: SetupRecorderInstanceT,
     hass: ha.HomeAssistant,
+    recorder_db_url: str,
 ):
     """Test we can query data prior to schema 25 and during migration to schema 25."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test doesn't run on MySQL / MariaDB; we can't drop table state_attributes
+        return
+
     instance = await async_setup_recorder_instance(hass, {})
 
     start = dt_util.utcnow()
@@ -748,8 +768,13 @@ async def test_get_states_query_during_migration_to_schema_25(
 async def test_get_states_query_during_migration_to_schema_25_multiple_entities(
     async_setup_recorder_instance: SetupRecorderInstanceT,
     hass: ha.HomeAssistant,
+    recorder_db_url: str,
 ):
     """Test we can query data prior to schema 25 and during migration to schema 25."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test doesn't run on MySQL / MariaDB; we can't drop table state_attributes
+        return
+
     instance = await async_setup_recorder_instance(hass, {})
 
     start = dt_util.utcnow()
diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py
index b47212b1045..ca4cbc9a4f9 100644
--- a/tests/components/recorder/test_init.py
+++ b/tests/components/recorder/test_init.py
@@ -1450,8 +1450,12 @@ async def test_database_lock_and_overflow(
         assert not instance.unlock_database()
 
 
-async def test_database_lock_timeout(recorder_mock, hass):
+async def test_database_lock_timeout(recorder_mock, hass, recorder_db_url):
     """Test locking database timeout when recorder stopped."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite: Locking is not implemented for other engines
+        return
+
     hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
 
     instance = get_instance(hass)
@@ -1517,8 +1521,13 @@ async def test_database_connection_keep_alive_disabled_on_sqlite(
     async_setup_recorder_instance: SetupRecorderInstanceT,
     hass: HomeAssistant,
     caplog: pytest.LogCaptureFixture,
+    recorder_db_url: str,
 ):
     """Test we do not do keep alive for sqlite."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite, keepalive runs on other engines
+        return
+
     instance = await async_setup_recorder_instance(hass)
     hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
     await instance.async_recorder_ready.wait()
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index 8f5cde707f7..f135ae8af43 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -135,9 +135,16 @@ async def test_purge_old_states(
 
 
 async def test_purge_old_states_encouters_database_corruption(
-    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
+    async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
+    recorder_db_url: str,
 ):
     """Test database image image is malformed while deleting old states."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite, wiping the database on error only happens
+        # with SQLite.
+        return
+
     await async_setup_recorder_instance(hass)
 
     await _add_test_states(hass)
@@ -364,7 +371,7 @@ async def test_purge_method(
         assert recorder_runs.count() == 7
         runs_before_purge = recorder_runs.all()
 
-        statistics_runs = session.query(StatisticsRuns)
+        statistics_runs = session.query(StatisticsRuns).order_by(StatisticsRuns.run_id)
         assert statistics_runs.count() == 7
         statistic_runs_before_purge = statistics_runs.all()
 
@@ -431,7 +438,10 @@ async def test_purge_method(
     await hass.services.async_call("recorder", "purge", service_data=service_data)
     await hass.async_block_till_done()
     await async_wait_purge_done(hass)
-    assert "Vacuuming SQL DB to free space" in caplog.text
+    assert (
+        "Vacuuming SQL DB to free space" in caplog.text
+        or "Optimizing SQL DB to free space" in caplog.text
+    )
 
 
 @pytest.mark.parametrize("use_sqlite", (True, False), indirect=True)
diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py
index 93c0157191a..0bb440a2dc8 100644
--- a/tests/components/recorder/test_system_health.py
+++ b/tests/components/recorder/test_system_health.py
@@ -14,8 +14,12 @@ from .common import async_wait_recording_done
 from tests.common import SetupRecorderInstanceT, get_system_health_info
 
 
-async def test_recorder_system_health(recorder_mock, hass):
+async def test_recorder_system_health(recorder_mock, hass, recorder_db_url):
     """Test recorder system health."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite
+        return
+
     assert await async_setup_component(hass, "system_health", {})
     await async_wait_recording_done(hass)
     info = await get_system_health_info(hass, "recorder")
@@ -85,9 +89,15 @@ async def test_recorder_system_health_db_url_missing_host(
 
 
 async def test_recorder_system_health_crashed_recorder_runs_table(
-    async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
+    async_setup_recorder_instance: SetupRecorderInstanceT,
+    hass: HomeAssistant,
+    recorder_db_url: str,
 ):
     """Test recorder system health with crashed recorder runs table."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite
+        return
+
     with patch("homeassistant.components.recorder.run_history.RunHistory.load_from_db"):
         assert await async_setup_component(hass, "system_health", {})
         instance = await async_setup_recorder_instance(hass)
diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py
index fe31e3a9a8d..9000379c17d 100644
--- a/tests/components/recorder/test_util.py
+++ b/tests/components/recorder/test_util.py
@@ -40,8 +40,14 @@ def test_session_scope_not_setup(hass_recorder):
             pass
 
 
-def test_recorder_bad_commit(hass_recorder):
+def test_recorder_bad_commit(hass_recorder, recorder_db_url):
     """Bad _commit should retry 3 times."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite: mysql does not raise an OperationalError
+        # which triggers retries for the bad query below, it raises ProgrammingError
+        # on which we give up
+        return
+
     hass = hass_recorder()
 
     def work(session):
@@ -542,8 +548,12 @@ def test_warn_unsupported_dialect(caplog, dialect, message):
     assert message in caplog.text
 
 
-def test_basic_sanity_check(hass_recorder):
+def test_basic_sanity_check(hass_recorder, recorder_db_url):
     """Test the basic sanity checks with a missing table."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite
+        return
+
     hass = hass_recorder()
 
     cursor = util.get_instance(hass).engine.raw_connection().cursor()
@@ -556,8 +566,12 @@ def test_basic_sanity_check(hass_recorder):
         util.basic_sanity_check(cursor)
 
 
-def test_combined_checks(hass_recorder, caplog):
+def test_combined_checks(hass_recorder, caplog, recorder_db_url):
     """Run Checks on the open database."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite
+        return
+
     hass = hass_recorder()
     instance = util.get_instance(hass)
     instance.db_retry_wait = 0
@@ -635,8 +649,12 @@ def test_end_incomplete_runs(hass_recorder, caplog):
     assert "Ended unfinished session" in caplog.text
 
 
-def test_periodic_db_cleanups(hass_recorder):
+def test_periodic_db_cleanups(hass_recorder, recorder_db_url):
     """Test periodic db cleanups."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite
+        return
+
     hass = hass_recorder()
     with patch.object(util.get_instance(hass).engine, "connect") as connect_mock:
         util.periodic_db_cleanups(util.get_instance(hass))
diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py
index b644df48864..eeffc7ea7dd 100644
--- a/tests/components/recorder/test_websocket_api.py
+++ b/tests/components/recorder/test_websocket_api.py
@@ -1329,9 +1329,13 @@ async def test_backup_start_no_recorder(
 
 
 async def test_backup_start_timeout(
-    recorder_mock, hass, hass_ws_client, hass_supervisor_access_token
+    recorder_mock, hass, hass_ws_client, hass_supervisor_access_token, recorder_db_url
 ):
     """Test getting backup start when recorder is not present."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite: Locking is not implemented for other engines
+        return
+
     client = await hass_ws_client(hass, hass_supervisor_access_token)
 
     # Ensure there are no queued events
@@ -1366,9 +1370,13 @@ async def test_backup_end(
 
 
 async def test_backup_end_without_start(
-    recorder_mock, hass, hass_ws_client, hass_supervisor_access_token
+    recorder_mock, hass, hass_ws_client, hass_supervisor_access_token, recorder_db_url
 ):
     """Test backup start."""
+    if recorder_db_url.startswith("mysql://"):
+        # This test is specific for SQLite: Locking is not implemented for other engines
+        return
+
     client = await hass_ws_client(hass, hass_supervisor_access_token)
 
     # Ensure there are no queued events
diff --git a/tests/conftest.py b/tests/conftest.py
index abba0e39c19..1047293ee16 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -79,6 +79,11 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
 asyncio.set_event_loop_policy = lambda policy: None
 
 
+def pytest_addoption(parser):
+    """Register custom pytest options."""
+    parser.addoption("--dburl", action="store", default="sqlite://")
+
+
 def pytest_configure(config):
     """Register marker for tests that log exceptions."""
     config.addinivalue_line(
@@ -108,8 +113,19 @@ def pytest_runtest_setup():
     def adapt_datetime(val):
         return val.isoformat(" ")
 
+    # Setup HAFakeDatetime converter for sqlite3
     sqlite3.register_adapter(HAFakeDatetime, adapt_datetime)
 
+    # Setup HAFakeDatetime converter for pymysql
+    try:
+        import MySQLdb.converters as MySQLdb_converters
+    except ImportError:
+        pass
+    else:
+        MySQLdb_converters.conversions[
+            HAFakeDatetime
+        ] = MySQLdb_converters.DateTime2literal
+
 
 def ha_datetime_to_fakedatetime(datetime):
     """Convert datetime to FakeDatetime.
@@ -865,7 +881,29 @@ def recorder_config():
 
 
 @pytest.fixture
-def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage):
+def recorder_db_url(pytestconfig):
+    """Prepare a default database for tests and return a connection URL."""
+    db_url: str = pytestconfig.getoption("dburl")
+    if db_url.startswith("mysql://"):
+        import sqlalchemy_utils
+
+        charset = "utf8mb4' COLLATE = 'utf8mb4_unicode_ci"
+        assert not sqlalchemy_utils.database_exists(db_url)
+        sqlalchemy_utils.create_database(db_url, encoding=charset)
+    elif db_url.startswith("postgresql://"):
+        pass
+    yield db_url
+    if db_url.startswith("mysql://"):
+        sqlalchemy_utils.drop_database(db_url)
+
+
+@pytest.fixture
+def hass_recorder(
+    recorder_db_url,
+    enable_nightly_purge,
+    enable_statistics,
+    hass_storage,
+):
     """Home Assistant fixture with in-memory recorder."""
     original_tz = dt_util.DEFAULT_TIME_ZONE
 
@@ -884,7 +922,7 @@ def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage):
 
         def setup_recorder(config=None):
             """Set up with params."""
-            init_recorder_component(hass, config)
+            init_recorder_component(hass, config, recorder_db_url)
             hass.start()
             hass.block_till_done()
             hass.data[recorder.DATA_INSTANCE].block_till_done()
@@ -897,11 +935,11 @@ def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage):
     dt_util.DEFAULT_TIME_ZONE = original_tz
 
 
-async def _async_init_recorder_component(hass, add_config=None):
+async def _async_init_recorder_component(hass, add_config=None, db_url=None):
     """Initialize the recorder asynchronously."""
     config = dict(add_config) if add_config else {}
     if recorder.CONF_DB_URL not in config:
-        config[recorder.CONF_DB_URL] = "sqlite://"  # In memory DB
+        config[recorder.CONF_DB_URL] = db_url
         if recorder.CONF_COMMIT_INTERVAL not in config:
             config[recorder.CONF_COMMIT_INTERVAL] = 0
 
@@ -920,7 +958,10 @@ async def _async_init_recorder_component(hass, add_config=None):
 
 @pytest.fixture
 async def async_setup_recorder_instance(
-    hass_fixture_setup, enable_nightly_purge, enable_statistics
+    recorder_db_url,
+    hass_fixture_setup,
+    enable_nightly_purge,
+    enable_statistics,
 ) -> AsyncGenerator[SetupRecorderInstanceT, None]:
     """Yield callable to setup recorder instance."""
     assert not hass_fixture_setup
@@ -941,7 +982,7 @@ async def async_setup_recorder_instance(
             hass: HomeAssistant, config: ConfigType | None = None
         ) -> recorder.Recorder:
             """Setup and return recorder instance."""  # noqa: D401
-            await _async_init_recorder_component(hass, config)
+            await _async_init_recorder_component(hass, config, recorder_db_url)
             await hass.async_block_till_done()
             instance = hass.data[recorder.DATA_INSTANCE]
             # The recorder's worker is not started until Home Assistant is running
-- 
GitLab


From 67d1dde69fbacf33f2c39ea14d89f2afa425ed18 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 13:31:08 +0200
Subject: [PATCH 598/985] Rename IMPERIAL_SYSTEM to US_CUSTOMARY_SYSTEM
 (#80253)

* Rename IMPERIAL_SYSTEM

* Deprecate is_metric property and adjust tests

* Adjust unit_system config validation

* Add yaml tests

* Add tests for private name

* Fix incorrect rebase

* Adjust docstring

* Add store migration

* Update unit_system.py

* Minimise test tweaks

* Fix tests

* Add conversion to migration

* Rename new key and adjust tests

* Adjust websocket_detect_config

* Move original_unit_system tracking to subclass
---
 homeassistant/components/config/core.py |  2 +-
 homeassistant/core.py                   | 39 ++++++++++++++++--
 homeassistant/util/unit_system.py       | 28 ++++++++++---
 tests/test_config.py                    | 53 +++++++++++++++++++++----
 tests/util/test_unit_system.py          | 25 +++++++++---
 5 files changed, 125 insertions(+), 22 deletions(-)

diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py
index 5ffb2d4db99..c748395e95f 100644
--- a/homeassistant/components/config/core.py
+++ b/homeassistant/components/config/core.py
@@ -94,7 +94,7 @@ async def websocket_detect_config(
         info["unit_system"] = unit_system._CONF_UNIT_SYSTEM_METRIC
     else:
         # pylint: disable-next=protected-access
-        info["unit_system"] = unit_system._CONF_UNIT_SYSTEM_IMPERIAL
+        info["unit_system"] = unit_system._CONF_UNIT_SYSTEM_US_CUSTOMARY
 
     if location_info.latitude:
         info["latitude"] = location_info.latitude
diff --git a/homeassistant/core.py b/homeassistant/core.py
index 73fc18fd3b7..8f9287aedac 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -81,7 +81,13 @@ from .util.async_ import (
 )
 from .util.read_only_dict import ReadOnlyDict
 from .util.timeout import TimeoutManager
-from .util.unit_system import METRIC_SYSTEM, UnitSystem, get_unit_system
+from .util.unit_system import (
+    _CONF_UNIT_SYSTEM_IMPERIAL,
+    _CONF_UNIT_SYSTEM_US_CUSTOMARY,
+    METRIC_SYSTEM,
+    UnitSystem,
+    get_unit_system,
+)
 
 # Typing imports that create a circular dependency
 if TYPE_CHECKING:
@@ -107,6 +113,7 @@ CALLBACK_TYPE = Callable[[], None]  # pylint: disable=invalid-name
 
 CORE_STORAGE_KEY = "core.config"
 CORE_STORAGE_VERSION = 1
+CORE_STORAGE_MINOR_VERSION = 2
 
 DOMAIN = "homeassistant"
 
@@ -1986,7 +1993,7 @@ class Config:
             latitude=data.get("latitude"),
             longitude=data.get("longitude"),
             elevation=data.get("elevation"),
-            unit_system=data.get("unit_system"),
+            unit_system=data.get("unit_system_v2"),
             location_name=data.get("location_name"),
             time_zone=data.get("time_zone"),
             external_url=data.get("external_url", _UNDEF),
@@ -2002,7 +2009,7 @@ class Config:
             "elevation": self.elevation,
             # We don't want any integrations to use the name of the unit system
             # so we are using the private attribute here
-            "unit_system": self.units._name,  # pylint: disable=protected-access
+            "unit_system_v2": self.units._name,  # pylint: disable=protected-access
             "location_name": self.location_name,
             "time_zone": self.time_zone,
             "external_url": self.external_url,
@@ -2027,4 +2034,30 @@ class Config:
                 CORE_STORAGE_KEY,
                 private=True,
                 atomic_writes=True,
+                minor_version=CORE_STORAGE_MINOR_VERSION,
             )
+            self._original_unit_system: str | None = None  # from old store 1.1
+
+        async def _async_migrate_func(
+            self,
+            old_major_version: int,
+            old_minor_version: int,
+            old_data: dict[str, Any],
+        ) -> dict[str, Any]:
+            """Migrate to the new version."""
+            data = old_data
+            if old_major_version == 1 and old_minor_version < 2:
+                # In 1.2, we remove support for "imperial", replaced by "us_customary"
+                # Using a new key to allow rollback
+                self._original_unit_system = data.get("unit_system")
+                data["unit_system_v2"] = self._original_unit_system
+                if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL:
+                    data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY
+            if old_major_version > 1:
+                raise NotImplementedError
+            return data
+
+        async def async_save(self, data: dict[str, Any]) -> None:
+            if self._original_unit_system:
+                data["unit_system"] = self._original_unit_system
+            return await super().async_save(data)
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 8dad108ccbe..31fec50c8f7 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -44,6 +44,7 @@ from .unit_conversion import (
 
 _CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial"
 _CONF_UNIT_SYSTEM_METRIC: Final = "metric"
+_CONF_UNIT_SYSTEM_US_CUSTOMARY: Final = "us_customary"
 
 LENGTH_UNITS = DistanceConverter.VALID_UNITS
 
@@ -130,6 +131,9 @@ class UnitSystem:
             "Please adjust to use instance check instead.",
             error_if_core=False,
         )
+        if self is IMPERIAL_SYSTEM:
+            # kept for compatibility reasons, with associated warning above
+            return _CONF_UNIT_SYSTEM_IMPERIAL
         return self._name
 
     @property
@@ -213,15 +217,26 @@ class UnitSystem:
 
 def get_unit_system(key: str) -> UnitSystem:
     """Get unit system based on key."""
-    if key == _CONF_UNIT_SYSTEM_IMPERIAL:
-        return IMPERIAL_SYSTEM
+    if key == _CONF_UNIT_SYSTEM_US_CUSTOMARY:
+        return US_CUSTOMARY_SYSTEM
     if key == _CONF_UNIT_SYSTEM_METRIC:
         return METRIC_SYSTEM
     raise ValueError(f"`{key}` is not a valid unit system key")
 
 
+def _deprecated_unit_system(value: str) -> str:
+    """Convert deprecated unit system."""
+
+    if value == _CONF_UNIT_SYSTEM_IMPERIAL:
+        # need to add warning in 2023.1
+        return _CONF_UNIT_SYSTEM_US_CUSTOMARY
+    return value
+
+
 validate_unit_system = vol.All(
-    vol.Lower, vol.Any(_CONF_UNIT_SYSTEM_METRIC, _CONF_UNIT_SYSTEM_IMPERIAL)
+    vol.Lower,
+    _deprecated_unit_system,
+    vol.Any(_CONF_UNIT_SYSTEM_METRIC, _CONF_UNIT_SYSTEM_US_CUSTOMARY),
 )
 
 METRIC_SYSTEM = UnitSystem(
@@ -235,8 +250,8 @@ METRIC_SYSTEM = UnitSystem(
     LENGTH_MILLIMETERS,
 )
 
-IMPERIAL_SYSTEM = UnitSystem(
-    _CONF_UNIT_SYSTEM_IMPERIAL,
+US_CUSTOMARY_SYSTEM = UnitSystem(
+    _CONF_UNIT_SYSTEM_US_CUSTOMARY,
     TEMP_FAHRENHEIT,
     LENGTH_MILES,
     SPEED_MILES_PER_HOUR,
@@ -245,3 +260,6 @@ IMPERIAL_SYSTEM = UnitSystem(
     PRESSURE_PSI,
     LENGTH_INCHES,
 )
+
+IMPERIAL_SYSTEM = US_CUSTOMARY_SYSTEM
+"""IMPERIAL_SYSTEM is deprecated. Please use US_CUSTOMARY_SYSTEM instead."""
diff --git a/tests/test_config.py b/tests/test_config.py
index 15c5f84bc42..0a125d8f121 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -27,11 +27,17 @@ from homeassistant.const import (
     CONF_UNIT_SYSTEM_METRIC,
     __version__,
 )
-from homeassistant.core import ConfigSource, HomeAssistantError
+from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError
 from homeassistant.helpers import config_validation as cv
 import homeassistant.helpers.check_config as check_config
 from homeassistant.helpers.entity import Entity
 from homeassistant.loader import async_get_integration
+from homeassistant.util.unit_system import (
+    _CONF_UNIT_SYSTEM_US_CUSTOMARY,
+    METRIC_SYSTEM,
+    US_CUSTOMARY_SYSTEM,
+    UnitSystem,
+)
 from homeassistant.util.yaml import SECRET_YAML
 
 from tests.common import get_test_config_dir, patch_yaml_files
@@ -439,7 +445,7 @@ async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_stor
     assert hass.config.config_source is ConfigSource.STORAGE
 
 
-async def test_updating_configuration(hass, hass_storage):
+async def test_igration_and_updating_configuration(hass, hass_storage):
     """Test updating configuration stores the new configuration."""
     core_data = {
         "data": {
@@ -448,7 +454,7 @@ async def test_updating_configuration(hass, hass_storage):
             "location_name": "Home",
             "longitude": 13,
             "time_zone": "Europe/Copenhagen",
-            "unit_system": "metric",
+            "unit_system": "imperial",
             "external_url": "https://www.example.com",
             "internal_url": "http://example.local",
             "currency": "BTC",
@@ -463,10 +469,14 @@ async def test_updating_configuration(hass, hass_storage):
     )
     await hass.config.async_update(latitude=50, currency="USD")
 
-    new_core_data = copy.deepcopy(core_data)
-    new_core_data["data"]["latitude"] = 50
-    new_core_data["data"]["currency"] = "USD"
-    assert hass_storage["core.config"] == new_core_data
+    expected_new_core_data = copy.deepcopy(core_data)
+    # From async_update above
+    expected_new_core_data["data"]["latitude"] = 50
+    expected_new_core_data["data"]["currency"] = "USD"
+    # 1.1 -> 1.2 store migration with migrated unit system
+    expected_new_core_data["data"]["unit_system_v2"] = "us_customary"
+    expected_new_core_data["minor_version"] = 2
+    assert hass_storage["core.config"] == expected_new_core_data
     assert hass.config.latitude == 50
     assert hass.config.currency == "USD"
 
@@ -593,6 +603,35 @@ async def test_loading_configuration_from_packages(hass):
         )
 
 
+@pytest.mark.parametrize(
+    "unit_system_name, expected_unit_system",
+    [
+        (CONF_UNIT_SYSTEM_METRIC, METRIC_SYSTEM),
+        (CONF_UNIT_SYSTEM_IMPERIAL, US_CUSTOMARY_SYSTEM),
+        (_CONF_UNIT_SYSTEM_US_CUSTOMARY, US_CUSTOMARY_SYSTEM),
+    ],
+)
+async def test_loading_configuration_unit_system(
+    hass: HomeAssistant, unit_system_name: str, expected_unit_system: UnitSystem
+) -> None:
+    """Test backward compatibility when loading core config."""
+    await config_util.async_process_ha_core_config(
+        hass,
+        {
+            "latitude": 60,
+            "longitude": 50,
+            "elevation": 25,
+            "name": "Huis",
+            "unit_system": unit_system_name,
+            "time_zone": "America/New_York",
+            "external_url": "https://www.example.com",
+            "internal_url": "http://example.local",
+        },
+    )
+
+    assert hass.config.units is expected_unit_system
+
+
 @patch("homeassistant.helpers.check_config.async_check_ha_config_file")
 async def test_check_ha_config_file_correct(mock_check, hass):
     """Check that restart propagates to stop."""
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 3019d6d0ff6..2e558a7bd5d 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -22,8 +22,10 @@ from homeassistant.exceptions import HomeAssistantError
 from homeassistant.util.unit_system import (
     _CONF_UNIT_SYSTEM_IMPERIAL,
     _CONF_UNIT_SYSTEM_METRIC,
+    _CONF_UNIT_SYSTEM_US_CUSTOMARY,
     IMPERIAL_SYSTEM,
     METRIC_SYSTEM,
+    US_CUSTOMARY_SYSTEM,
     UnitSystem,
     get_unit_system,
 )
@@ -320,17 +322,26 @@ def test_is_metric(
 
 
 @pytest.mark.parametrize(
-    "unit_system, expected_name",
+    "unit_system, expected_name, expected_private_name",
     [
-        (METRIC_SYSTEM, _CONF_UNIT_SYSTEM_METRIC),
-        (IMPERIAL_SYSTEM, _CONF_UNIT_SYSTEM_IMPERIAL),
+        (METRIC_SYSTEM, _CONF_UNIT_SYSTEM_METRIC, _CONF_UNIT_SYSTEM_METRIC),
+        (IMPERIAL_SYSTEM, _CONF_UNIT_SYSTEM_IMPERIAL, _CONF_UNIT_SYSTEM_US_CUSTOMARY),
+        (
+            US_CUSTOMARY_SYSTEM,
+            _CONF_UNIT_SYSTEM_IMPERIAL,
+            _CONF_UNIT_SYSTEM_US_CUSTOMARY,
+        ),
     ],
 )
 def test_deprecated_name(
-    caplog: pytest.LogCaptureFixture, unit_system: UnitSystem, expected_name: str
+    caplog: pytest.LogCaptureFixture,
+    unit_system: UnitSystem,
+    expected_name: str,
+    expected_private_name: str,
 ) -> None:
     """Test the name is deprecated."""
     assert unit_system.name == expected_name
+    assert unit_system._name == expected_private_name
     assert (
         "Detected code that accesses the `name` property of the unit system."
         in caplog.text
@@ -341,7 +352,7 @@ def test_deprecated_name(
     "key, expected_system",
     [
         (_CONF_UNIT_SYSTEM_METRIC, METRIC_SYSTEM),
-        (_CONF_UNIT_SYSTEM_IMPERIAL, IMPERIAL_SYSTEM),
+        (_CONF_UNIT_SYSTEM_US_CUSTOMARY, US_CUSTOMARY_SYSTEM),
     ],
 )
 def test_get_unit_system(key: str, expected_system: UnitSystem) -> None:
@@ -349,7 +360,9 @@ def test_get_unit_system(key: str, expected_system: UnitSystem) -> None:
     assert get_unit_system(key) is expected_system
 
 
-@pytest.mark.parametrize("key", [None, "", "invalid_custom"])
+@pytest.mark.parametrize(
+    "key", [None, "", "invalid_custom", _CONF_UNIT_SYSTEM_IMPERIAL]
+)
 def test_get_unit_system_invalid(key: str) -> None:
     """Test get_unit_system with an invalid key."""
     with pytest.raises(ValueError, match=f"`{key}` is not a valid unit system key"):
-- 
GitLab


From dc2a87b9aea27c19d411f054007b70066cf36e5e Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 13:49:01 +0200
Subject: [PATCH 599/985] Fix invalid type hint in scrape (#80543)

---
 homeassistant/components/scrape/sensor.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index 88c9b564b29..d34080c1712 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -82,7 +82,7 @@ async def async_setup_platform(
     resource: str = config[CONF_RESOURCE]
     method: str = "GET"
     payload: str | None = None
-    headers: str | None = config.get(CONF_HEADERS)
+    headers: dict[str, str] | None = config.get(CONF_HEADERS)
     verify_ssl: bool = config[CONF_VERIFY_SSL]
     select: str | None = config.get(CONF_SELECT)
     attr: str | None = config.get(CONF_ATTR)
-- 
GitLab


From a70f9b8995fc65d6009c96c5a974576c09e521b2 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 14:35:23 +0200
Subject: [PATCH 600/985] Add type hints to rest integration (#80546)

---
 homeassistant/components/rest/__init__.py | 60 ++++++++++++++---------
 homeassistant/components/rest/data.py     | 35 +++++++------
 2 files changed, 55 insertions(+), 40 deletions(-)

diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py
index 96ed1ada7ae..5549abc2143 100644
--- a/homeassistant/components/rest/__init__.py
+++ b/homeassistant/components/rest/__init__.py
@@ -1,7 +1,9 @@
 """The rest component."""
+from __future__ import annotations
 
 import asyncio
 import contextlib
+from datetime import timedelta
 import logging
 
 import httpx
@@ -34,7 +36,7 @@ from homeassistant.helpers.reload import (
     async_integration_yaml_config,
     async_reload_integration_platforms,
 )
-from homeassistant.helpers.typing import ConfigType
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
 
 from .const import COORDINATOR, DOMAIN, PLATFORM_IDX, REST, REST_DATA, REST_IDX
@@ -76,21 +78,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
 
 
 @callback
-def _async_setup_shared_data(hass: HomeAssistant):
+def _async_setup_shared_data(hass: HomeAssistant) -> None:
     """Create shared data for platform config and rest coordinators."""
     hass.data[DOMAIN] = {key: [] for key in (REST_DATA, *COORDINATOR_AWARE_PLATFORMS)}
 
 
-async def _async_process_config(hass, config) -> bool:
+async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool:
     """Process rest configuration."""
     if DOMAIN not in config:
         return True
 
     refresh_tasks = []
     load_tasks = []
-    for rest_idx, conf in enumerate(config[DOMAIN]):
-        scan_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
-        resource_template = conf.get(CONF_RESOURCE_TEMPLATE)
+    rest_config: list[ConfigType] = config[DOMAIN]
+    for rest_idx, conf in enumerate(rest_config):
+        scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+        resource_template: template.Template | None = conf.get(CONF_RESOURCE_TEMPLATE)
         rest = create_rest_data_from_config(hass, conf)
         coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval)
         refresh_tasks.append(coordinator.async_refresh())
@@ -122,18 +125,25 @@ async def _async_process_config(hass, config) -> bool:
     return True
 
 
-async def async_get_config_and_coordinator(hass, platform_domain, discovery_info):
+async def async_get_config_and_coordinator(
+    hass: HomeAssistant, platform_domain: str, discovery_info: DiscoveryInfoType
+) -> tuple[ConfigType, DataUpdateCoordinator[None], RestData]:
     """Get the config and coordinator for the platform from discovery."""
     shared_data = hass.data[DOMAIN][REST_DATA][discovery_info[REST_IDX]]
-    conf = hass.data[DOMAIN][platform_domain][discovery_info[PLATFORM_IDX]]
-    coordinator = shared_data[COORDINATOR]
-    rest = shared_data[REST]
+    conf: ConfigType = hass.data[DOMAIN][platform_domain][discovery_info[PLATFORM_IDX]]
+    coordinator: DataUpdateCoordinator[None] = shared_data[COORDINATOR]
+    rest: RestData = shared_data[REST]
     if rest.data is None:
         await coordinator.async_request_refresh()
     return conf, coordinator, rest
 
 
-def _rest_coordinator(hass, rest, resource_template, update_interval):
+def _rest_coordinator(
+    hass: HomeAssistant,
+    rest: RestData,
+    resource_template: template.Template | None,
+    update_interval: timedelta,
+) -> DataUpdateCoordinator[None]:
     """Wrap a DataUpdateCoordinator around the rest object."""
     if resource_template:
 
@@ -154,33 +164,35 @@ def _rest_coordinator(hass, rest, resource_template, update_interval):
     )
 
 
-def create_rest_data_from_config(hass, config):
+def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> RestData:
     """Create RestData from config."""
-    resource = config.get(CONF_RESOURCE)
-    resource_template = config.get(CONF_RESOURCE_TEMPLATE)
-    method = config.get(CONF_METHOD)
-    payload = config.get(CONF_PAYLOAD)
-    verify_ssl = config.get(CONF_VERIFY_SSL)
-    username = config.get(CONF_USERNAME)
-    password = config.get(CONF_PASSWORD)
-    headers = config.get(CONF_HEADERS)
-    params = config.get(CONF_PARAMS)
-    timeout = config.get(CONF_TIMEOUT)
+    resource: str | None = config.get(CONF_RESOURCE)
+    resource_template: template.Template | None = config.get(CONF_RESOURCE_TEMPLATE)
+    method: str = config[CONF_METHOD]
+    payload: str | None = config.get(CONF_PAYLOAD)
+    verify_ssl: bool = config[CONF_VERIFY_SSL]
+    username: str | None = config.get(CONF_USERNAME)
+    password: str | None = config.get(CONF_PASSWORD)
+    headers: dict[str, str] | None = config.get(CONF_HEADERS)
+    params: dict[str, str] | None = config.get(CONF_PARAMS)
+    timeout: int = config[CONF_TIMEOUT]
 
     if resource_template is not None:
         resource_template.hass = hass
         resource = resource_template.async_render(parse_result=False)
 
+    if not resource:
+        raise HomeAssistantError("Resource not set for RestData")
+
     template.attach(hass, headers)
     template.attach(hass, params)
 
+    auth: httpx.DigestAuth | tuple[str, str] | None = None
     if username and password:
         if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
             auth = httpx.DigestAuth(username, password)
         else:
             auth = (username, password)
-    else:
-        auth = None
 
     return RestData(
         hass, method, resource, auth, headers, params, payload, verify_ssl, timeout
diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py
index 351c59a9f52..c1990b28336 100644
--- a/homeassistant/components/rest/data.py
+++ b/homeassistant/components/rest/data.py
@@ -1,8 +1,11 @@
 """Support for RESTful API."""
+from __future__ import annotations
+
 import logging
 
 import httpx
 
+from homeassistant.core import HomeAssistant
 from homeassistant.helpers import template
 from homeassistant.helpers.httpx_client import get_async_client
 
@@ -16,16 +19,16 @@ class RestData:
 
     def __init__(
         self,
-        hass,
-        method,
-        resource,
-        auth,
-        headers,
-        params,
-        data,
-        verify_ssl,
-        timeout=DEFAULT_TIMEOUT,
-    ):
+        hass: HomeAssistant,
+        method: str,
+        resource: str,
+        auth: httpx.DigestAuth | tuple[str, str] | None,
+        headers: dict[str, str] | None,
+        params: dict[str, str] | None,
+        data: str | None,
+        verify_ssl: bool,
+        timeout: int = DEFAULT_TIMEOUT,
+    ) -> None:
         """Initialize the data object."""
         self._hass = hass
         self._method = method
@@ -36,16 +39,16 @@ class RestData:
         self._request_data = data
         self._timeout = timeout
         self._verify_ssl = verify_ssl
-        self._async_client = None
-        self.data = None
-        self.last_exception = None
-        self.headers = None
+        self._async_client: httpx.AsyncClient | None = None
+        self.data: str | None = None
+        self.last_exception: Exception | None = None
+        self.headers: httpx.Headers | None = None
 
-    def set_url(self, url):
+    def set_url(self, url: str) -> None:
         """Set url."""
         self._resource = url
 
-    async def async_update(self, log_errors=True):
+    async def async_update(self, log_errors: bool = True) -> None:
         """Get the latest data from REST service with provided method."""
         if not self._async_client:
             self._async_client = get_async_client(
-- 
GitLab


From 9afb4c6c9e64acda62cb4b8c45e6db18500fd63c Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 14:35:49 +0200
Subject: [PATCH 601/985] Adjust precipitation units (#79780)

* Adjust precipitation units

* Use PRECIPITATION_INTENSITY

* Revert isy994

* Adjust SensorDeviceClass docstring

* Adjust comment
---
 homeassistant/components/sensor/__init__.py |  2 +-
 homeassistant/const.py                      | 34 ++++++++++++--
 homeassistant/util/unit_conversion.py       | 21 +++++----
 tests/util/test_unit_conversion.py          | 51 ++++++++++++++++-----
 4 files changed, 83 insertions(+), 25 deletions(-)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index f7fbecea224..3c158f1be9c 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -266,7 +266,7 @@ class SensorDeviceClass(StrEnum):
     SPEED = "speed"
     """Generic speed.
 
-    Unit of measurement: `SPEED_*` units
+    Unit of measurement: `SPEED_*` or `PRECIPITATION_INTENSITY_*` units
     - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h`
     - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph`
     - Nautical: `kn`
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 99af9974ea0..49749c1b5e5 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -525,11 +525,15 @@ TIME_YEARS: Final = "y"
 
 # Length units
 LENGTH_MILLIMETERS: Final = "mm"
+"""Note: for precipitation, please use PRECIPITATION_MILLIMETERS"""
+
 LENGTH_CENTIMETERS: Final = "cm"
 LENGTH_METERS: Final = "m"
 LENGTH_KILOMETERS: Final = "km"
 
 LENGTH_INCHES: Final = "in"
+"""Note: for precipitation, please use PRECIPITATION_INCHES"""
+
 LENGTH_FEET: Final = "ft"
 LENGTH_YARD: Final = "yd"
 LENGTH_MILES: Final = "mi"
@@ -602,10 +606,25 @@ REVOLUTIONS_PER_MINUTE: Final = "rpm"
 IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²"
 IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)"
 
+# Precipitation intensity units
+# The derivation of these units is a volume of rain amassing in a container
+# with constant cross section in a given time
+PRECIPITATION_INTENSITY_INCHES_PER_DAY: Final = "in/d"
+PRECIPITATION_INTENSITY_INCHES_PER_HOUR: Final = "in/h"
+PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY: Final = "mm/d"
+PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR: Final = "mm/h"
+
 # Precipitation units
-PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h"
+# The derivation of these units is a volume of rain amassing in a container
+# with constant cross section
 PRECIPITATION_INCHES: Final = "in"
+PRECIPITATION_MILLIMETERS: Final = "mm"
+
+PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h"
+"""Deprecated: please use PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR"""
+
 PRECIPITATION_INCHES_PER_HOUR: Final = "in/h"
+"""Deprecated: please use PRECIPITATION_INTENSITY_INCHES_PER_HOUR"""
 
 # Concentration units
 CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
@@ -616,15 +635,22 @@ CONCENTRATION_PARTS_PER_MILLION: Final = "ppm"
 CONCENTRATION_PARTS_PER_BILLION: Final = "ppb"
 
 # Speed units
-SPEED_MILLIMETERS_PER_DAY: Final = "mm/d"
 SPEED_FEET_PER_SECOND: Final = "ft/s"
-SPEED_INCHES_PER_DAY: Final = "in/d"
 SPEED_METERS_PER_SECOND: Final = "m/s"
-SPEED_INCHES_PER_HOUR: Final = "in/h"
 SPEED_KILOMETERS_PER_HOUR: Final = "km/h"
 SPEED_KNOTS: Final = "kn"
 SPEED_MILES_PER_HOUR: Final = "mph"
 
+SPEED_MILLIMETERS_PER_DAY: Final = "mm/d"
+"""Deprecated: please use PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY"""
+
+SPEED_INCHES_PER_DAY: Final = "in/d"
+"""Deprecated: please use PRECIPITATION_INTENSITY_INCHES_PER_DAY"""
+
+SPEED_INCHES_PER_HOUR: Final = "in/h"
+"""Deprecated: please use PRECIPITATION_INTENSITY_INCHES_PER_HOUR"""
+
+
 # Signal_strength units
 SIGNAL_STRENGTH_DECIBELS: Final = "dB"
 SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm"
diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py
index 9aa3084887e..0bf895f34a0 100644
--- a/homeassistant/util/unit_conversion.py
+++ b/homeassistant/util/unit_conversion.py
@@ -21,6 +21,10 @@ from homeassistant.const import (
     MASS_POUNDS,
     POWER_KILO_WATT,
     POWER_WATT,
+    PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_BAR,
     PRESSURE_CBAR,
     PRESSURE_HPA,
@@ -31,13 +35,10 @@ from homeassistant.const import (
     PRESSURE_PA,
     PRESSURE_PSI,
     SPEED_FEET_PER_SECOND,
-    SPEED_INCHES_PER_DAY,
-    SPEED_INCHES_PER_HOUR,
     SPEED_KILOMETERS_PER_HOUR,
     SPEED_KNOTS,
     SPEED_METERS_PER_SECOND,
     SPEED_MILES_PER_HOUR,
-    SPEED_MILLIMETERS_PER_DAY,
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
     TEMP_KELVIN,
@@ -238,24 +239,26 @@ class SpeedConverter(BaseUnitConverter):
     UNIT_CLASS = "speed"
     NORMALIZED_UNIT = SPEED_METERS_PER_SECOND
     _UNIT_CONVERSION: dict[str, float] = {
+        PRECIPITATION_INTENSITY_INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
+        PRECIPITATION_INTENSITY_INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
+        PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
+        PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR: _HRS_TO_SECS / _MM_TO_M,
         SPEED_FEET_PER_SECOND: 1 / _FOOT_TO_M,
-        SPEED_INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
-        SPEED_INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
         SPEED_KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M,
         SPEED_KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M,
         SPEED_METERS_PER_SECOND: 1,
         SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M,
-        SPEED_MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
     }
     VALID_UNITS = {
+        PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+        PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+        PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+        PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
         SPEED_FEET_PER_SECOND,
-        SPEED_INCHES_PER_DAY,
-        SPEED_INCHES_PER_HOUR,
         SPEED_KILOMETERS_PER_HOUR,
         SPEED_KNOTS,
         SPEED_METERS_PER_SECOND,
         SPEED_MILES_PER_HOUR,
-        SPEED_MILLIMETERS_PER_DAY,
     }
 
 
diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py
index d74bacc66f8..98be32584c6 100644
--- a/tests/util/test_unit_conversion.py
+++ b/tests/util/test_unit_conversion.py
@@ -21,6 +21,10 @@ from homeassistant.const import (
     MASS_POUNDS,
     POWER_KILO_WATT,
     POWER_WATT,
+    PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_CBAR,
     PRESSURE_HPA,
     PRESSURE_INHG,
@@ -30,13 +34,10 @@ from homeassistant.const import (
     PRESSURE_PA,
     PRESSURE_PSI,
     SPEED_FEET_PER_SECOND,
-    SPEED_INCHES_PER_DAY,
-    SPEED_INCHES_PER_HOUR,
     SPEED_KILOMETERS_PER_HOUR,
     SPEED_KNOTS,
     SPEED_METERS_PER_SECOND,
     SPEED_MILES_PER_HOUR,
-    SPEED_MILLIMETERS_PER_DAY,
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
     TEMP_KELVIN,
@@ -93,14 +94,15 @@ INVALID_SYMBOL = "bob"
         (PressureConverter, PRESSURE_CBAR),
         (PressureConverter, PRESSURE_MMHG),
         (PressureConverter, PRESSURE_PSI),
+        (SpeedConverter, PRECIPITATION_INTENSITY_INCHES_PER_DAY),
+        (SpeedConverter, PRECIPITATION_INTENSITY_INCHES_PER_HOUR),
+        (SpeedConverter, PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY),
+        (SpeedConverter, PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR),
         (SpeedConverter, SPEED_FEET_PER_SECOND),
-        (SpeedConverter, SPEED_INCHES_PER_DAY),
-        (SpeedConverter, SPEED_INCHES_PER_HOUR),
         (SpeedConverter, SPEED_KILOMETERS_PER_HOUR),
         (SpeedConverter, SPEED_KNOTS),
         (SpeedConverter, SPEED_METERS_PER_SECOND),
         (SpeedConverter, SPEED_MILES_PER_HOUR),
-        (SpeedConverter, SPEED_MILLIMETERS_PER_DAY),
         (TemperatureConverter, TEMP_CELSIUS),
         (TemperatureConverter, TEMP_FAHRENHEIT),
         (TemperatureConverter, TEMP_KELVIN),
@@ -389,17 +391,44 @@ def test_pressure_convert(
         # 5 mi/h * 1.609 km/mi = 8.04672 km/h
         (5, SPEED_MILES_PER_HOUR, 8.04672, SPEED_KILOMETERS_PER_HOUR),
         # 5 in/day * 25.4 mm/in = 127 mm/day
-        (5, SPEED_INCHES_PER_DAY, 127, SPEED_MILLIMETERS_PER_DAY),
+        (
+            5,
+            PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+            127,
+            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+        ),
         # 5 mm/day / 25.4 mm/in = 0.19685 in/day
-        (5, SPEED_MILLIMETERS_PER_DAY, pytest.approx(0.1968504), SPEED_INCHES_PER_DAY),
+        (
+            5,
+            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+            pytest.approx(0.1968504),
+            PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+        ),
+        # 48 mm/day = 2 mm/h
+        (
+            48,
+            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+            pytest.approx(2),
+            PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        ),
         # 5 in/hr * 24 hr/day = 3048 mm/day
-        (5, SPEED_INCHES_PER_HOUR, 3048, SPEED_MILLIMETERS_PER_DAY),
+        (
+            5,
+            PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+            3048,
+            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+        ),
         # 5 m/s * 39.3701 in/m * 3600 s/hr = 708661
-        (5, SPEED_METERS_PER_SECOND, pytest.approx(708661.42), SPEED_INCHES_PER_HOUR),
+        (
+            5,
+            SPEED_METERS_PER_SECOND,
+            pytest.approx(708661.42),
+            PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+        ),
         # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s
         (
             5000,
-            SPEED_INCHES_PER_HOUR,
+            PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
             pytest.approx(0.0352778),
             SPEED_METERS_PER_SECOND,
         ),
-- 
GitLab


From 62b07358225178ae915a0901aebc5644e3c63c3e Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 15:09:05 +0200
Subject: [PATCH 602/985] Adjust precipitation units in components (#79783)

* Adjust precipitation units in components

* Don't update darksky
---
 homeassistant/components/aemet/const.py          |  8 ++++----
 .../components/ambient_station/sensor.py         |  4 ++--
 homeassistant/components/buienradar/sensor.py    |  6 +++---
 homeassistant/components/ecowitt/sensor.py       |  8 ++++----
 homeassistant/components/isy994/const.py         | 16 ++++++++--------
 homeassistant/components/rfxtrx/sensor.py        |  4 ++--
 homeassistant/components/tellduslive/sensor.py   |  4 ++--
 .../zwave_js/discovery_data_template.py          |  8 ++++----
 8 files changed, 29 insertions(+), 29 deletions(-)

diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py
index 645c1ad0ea2..378168af4ec 100644
--- a/homeassistant/components/aemet/const.py
+++ b/homeassistant/components/aemet/const.py
@@ -21,7 +21,7 @@ from homeassistant.components.weather import (
 from homeassistant.const import (
     DEGREE,
     PERCENTAGE,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     SPEED_KILOMETERS_PER_HOUR,
     TEMP_CELSIUS,
@@ -208,7 +208,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key=ATTR_API_FORECAST_PRECIPITATION,
         name="Precipitation",
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     ),
     SensorEntityDescription(
         key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
@@ -265,7 +265,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key=ATTR_API_RAIN,
         name="Rain",
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     ),
     SensorEntityDescription(
         key=ATTR_API_RAIN_PROB,
@@ -276,7 +276,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key=ATTR_API_SNOW,
         name="Snow",
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     ),
     SensorEntityDescription(
         key=ATTR_API_SNOW_PROB,
diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py
index 65c726bfff3..da6eb143a52 100644
--- a/homeassistant/components/ambient_station/sensor.py
+++ b/homeassistant/components/ambient_station/sensor.py
@@ -19,7 +19,7 @@ from homeassistant.const import (
     LIGHT_LUX,
     PERCENTAGE,
     PRECIPITATION_INCHES,
-    PRECIPITATION_INCHES_PER_HOUR,
+    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
     PRESSURE_INHG,
     SPEED_MILES_PER_HOUR,
     TEMP_FAHRENHEIT,
@@ -195,7 +195,7 @@ SENSOR_DESCRIPTIONS = (
         key=TYPE_HOURLYRAININ,
         name="Hourly rain rate",
         icon="mdi:water",
-        native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index 279fdc145d5..ccd5b60132e 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -38,7 +38,7 @@ from homeassistant.const import (
     LENGTH_KILOMETERS,
     LENGTH_MILLIMETERS,
     PERCENTAGE,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     SPEED_KILOMETERS_PER_HOUR,
     TEMP_CELSIUS,
@@ -183,7 +183,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key="precipitation",
         name="Precipitation",
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
         icon="mdi:weather-pouring",
         state_class=SensorStateClass.MEASUREMENT,
     ),
@@ -197,7 +197,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key="precipitation_forecast_average",
         name="Precipitation forecast average",
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
         icon="mdi:weather-pouring",
     ),
     SensorEntityDescription(
diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py
index 3139a033289..172561d7125 100644
--- a/homeassistant/components/ecowitt/sensor.py
+++ b/homeassistant/components/ecowitt/sensor.py
@@ -27,8 +27,8 @@ from homeassistant.const import (
     LIGHT_LUX,
     PERCENTAGE,
     POWER_WATT,
-    PRECIPITATION_INCHES_PER_HOUR,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
+    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     PRESSURE_INHG,
     SPEED_KILOMETERS_PER_HOUR,
@@ -157,12 +157,12 @@ ECOWITT_SENSORS_MAPPING: Final = {
     ),
     EcoWittSensorTypes.RAIN_RATE_MM: SensorEntityDescription(
         key="RAIN_RATE_MM",
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     EcoWittSensorTypes.RAIN_RATE_INCHES: SensorEntityDescription(
         key="RAIN_RATE_INCHES",
-        native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     EcoWittSensorTypes.LIGHTNING_DISTANCE_KM: SensorEntityDescription(
diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py
index 0a0149c376e..36c1c9c5521 100644
--- a/homeassistant/components/isy994/const.py
+++ b/homeassistant/components/isy994/const.py
@@ -37,7 +37,10 @@ from homeassistant.const import (
     PERCENTAGE,
     POWER_KILO_WATT,
     POWER_WATT,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
+    PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     PRESSURE_INHG,
     PRESSURE_MBAR,
@@ -46,12 +49,9 @@ from homeassistant.const import (
     SERVICE_UNLOCK,
     SOUND_PRESSURE_DB,
     SOUND_PRESSURE_WEIGHTED_DBA,
-    SPEED_INCHES_PER_DAY,
-    SPEED_INCHES_PER_HOUR,
     SPEED_KILOMETERS_PER_HOUR,
     SPEED_METERS_PER_SECOND,
     SPEED_MILES_PER_HOUR,
-    SPEED_MILLIMETERS_PER_DAY,
     STATE_CLOSED,
     STATE_CLOSING,
     STATE_LOCKED,
@@ -342,7 +342,7 @@ UOM_FRIENDLY_NAME = {
     "21": "%AH",
     "22": "%RH",
     "23": PRESSURE_INHG,
-    "24": SPEED_INCHES_PER_HOUR,
+    "24": PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
     UOM_INDEX: UOM_INDEX,  # Index type. Use "node.formatted" for value
     "26": TEMP_KELVIN,
     "27": "keyword",
@@ -364,7 +364,7 @@ UOM_FRIENDLY_NAME = {
     "43": ELECTRIC_POTENTIAL_MILLIVOLT,
     "44": TIME_MINUTES,
     "45": TIME_MINUTES,
-    "46": PRECIPITATION_MILLIMETERS_PER_HOUR,
+    "46": PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     "47": TIME_MONTHS,
     "48": SPEED_MILES_PER_HOUR,
     "49": SPEED_METERS_PER_SECOND,
@@ -407,7 +407,7 @@ UOM_FRIENDLY_NAME = {
     "103": CURRENCY_DOLLAR,
     "104": CURRENCY_CENT,
     "105": LENGTH_INCHES,
-    "106": SPEED_MILLIMETERS_PER_DAY,
+    "106": PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
     "107": "",  # raw 1-byte unsigned value
     "108": "",  # raw 2-byte unsigned value
     "109": "",  # raw 3-byte unsigned value
@@ -420,7 +420,7 @@ UOM_FRIENDLY_NAME = {
     "117": PRESSURE_MBAR,
     "118": PRESSURE_HPA,
     "119": ENERGY_WATT_HOUR,
-    "120": SPEED_INCHES_PER_DAY,
+    "120": PRECIPITATION_INTENSITY_INCHES_PER_DAY,
 }
 
 UOM_TO_STATES = {
diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py
index 563b166e0aa..9f854924cf4 100644
--- a/homeassistant/components/rfxtrx/sensor.py
+++ b/homeassistant/components/rfxtrx/sensor.py
@@ -25,7 +25,7 @@ from homeassistant.const import (
     LENGTH_MILLIMETERS,
     PERCENTAGE,
     POWER_WATT,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
     SPEED_METERS_PER_SECOND,
@@ -172,7 +172,7 @@ SENSOR_TYPES = (
         key="Rain rate",
         name="Rain rate",
         state_class=SensorStateClass.MEASUREMENT,
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     ),
     RfxtrxSensorEntityDescription(
         key="Sound",
diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py
index e2995620fb1..192a256b48f 100644
--- a/homeassistant/components/tellduslive/sensor.py
+++ b/homeassistant/components/tellduslive/sensor.py
@@ -14,7 +14,7 @@ from homeassistant.const import (
     LIGHT_LUX,
     PERCENTAGE,
     POWER_WATT,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     SPEED_METERS_PER_SECOND,
     TEMP_CELSIUS,
     UV_INDEX,
@@ -57,7 +57,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
     SENSOR_TYPE_RAINRATE: SensorEntityDescription(
         key=SENSOR_TYPE_RAINRATE,
         name="Rain rate",
-        native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
         icon="mdi:water",
         state_class=SensorStateClass.MEASUREMENT,
     ),
diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py
index 9ae1cd36d13..590e3965b0c 100644
--- a/homeassistant/components/zwave_js/discovery_data_template.py
+++ b/homeassistant/components/zwave_js/discovery_data_template.py
@@ -109,8 +109,8 @@ from homeassistant.const import (
     PERCENTAGE,
     POWER_BTU_PER_HOUR,
     POWER_WATT,
-    PRECIPITATION_INCHES_PER_HOUR,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
+    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_INHG,
     PRESSURE_MMHG,
     PRESSURE_PSI,
@@ -201,14 +201,14 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
     VOLUME_GALLONS: UNIT_GALLONS,
     FREQUENCY_HERTZ: UNIT_HERTZ,
     PRESSURE_INHG: UNIT_INCHES_OF_MERCURY,
-    PRECIPITATION_INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR,
+    PRECIPITATION_INTENSITY_INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR,
     MASS_KILOGRAMS: UNIT_KILOGRAM,
     FREQUENCY_KILOHERTZ: UNIT_KILOHERTZ,
     VOLUME_LITERS: UNIT_LITER,
     LIGHT_LUX: UNIT_LUX,
     LENGTH_METERS: UNIT_METER,
     ELECTRIC_CURRENT_MILLIAMPERE: UNIT_MILLIAMPERE,
-    PRECIPITATION_MILLIMETERS_PER_HOUR: UNIT_MILLIMETER_HOUR,
+    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR: UNIT_MILLIMETER_HOUR,
     ELECTRIC_POTENTIAL_MILLIVOLT: UNIT_MILLIVOLT,
     SPEED_MILES_PER_HOUR: UNIT_MPH,
     SPEED_METERS_PER_SECOND: UNIT_M_S,
-- 
GitLab


From 05ef02bff64c8cd32c3fdc926b209812af84b964 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 16:04:11 +0200
Subject: [PATCH 603/985] Add precipitation_intensity sensor device class
 (#79779)

---
 homeassistant/components/sensor/__init__.py         | 9 +++++++++
 homeassistant/components/sensor/device_condition.py | 4 ++++
 homeassistant/components/sensor/device_trigger.py   | 4 ++++
 3 files changed, 17 insertions(+)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index 3c158f1be9c..c6c5908b5d8 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -241,6 +241,14 @@ class SensorDeviceClass(StrEnum):
     Unit of measurement: `W`, `kW`
     """
 
+    PRECIPITATION_INTENSITY = "precipitation_intensity"
+    """Precipitation intensity.
+
+    Unit of measurement:
+    - `in/d`, `in/h`
+    - `mm/d`, `mm/h`
+    """
+
     PRESSURE = "pressure"
     """Pressure.
 
@@ -361,6 +369,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
 
 UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
     SensorDeviceClass.DISTANCE: DistanceConverter,
+    SensorDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter,
     SensorDeviceClass.PRESSURE: PressureConverter,
     SensorDeviceClass.SPEED: SpeedConverter,
     SensorDeviceClass.TEMPERATURE: TemperatureConverter,
diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py
index 93ba51d2668..6abd0bfbfaa 100644
--- a/homeassistant/components/sensor/device_condition.py
+++ b/homeassistant/components/sensor/device_condition.py
@@ -52,6 +52,7 @@ CONF_IS_PM10 = "is_pm10"
 CONF_IS_PM25 = "is_pm25"
 CONF_IS_POWER = "is_power"
 CONF_IS_POWER_FACTOR = "is_power_factor"
+CONF_IS_PRECIPITATION_INTENSITY = "is_precipitation_intensity"
 CONF_IS_PRESSURE = "is_pressure"
 CONF_IS_SPEED = "is_speed"
 CONF_IS_REACTIVE_POWER = "is_reactive_power"
@@ -86,6 +87,9 @@ ENTITY_CONDITIONS = {
     SensorDeviceClass.PM1: [{CONF_TYPE: CONF_IS_PM1}],
     SensorDeviceClass.PM10: [{CONF_TYPE: CONF_IS_PM10}],
     SensorDeviceClass.PM25: [{CONF_TYPE: CONF_IS_PM25}],
+    SensorDeviceClass.PRECIPITATION_INTENSITY: [
+        {CONF_TYPE: CONF_IS_PRECIPITATION_INTENSITY}
+    ],
     SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}],
     SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_IS_REACTIVE_POWER}],
     SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}],
diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py
index 9e433cea31b..675a59ef78d 100644
--- a/homeassistant/components/sensor/device_trigger.py
+++ b/homeassistant/components/sensor/device_trigger.py
@@ -51,6 +51,7 @@ CONF_PM10 = "pm10"
 CONF_PM25 = "pm25"
 CONF_POWER = "power"
 CONF_POWER_FACTOR = "power_factor"
+CONF_PRECIPITATION_INTENSITY = "precipitation_intensity"
 CONF_PRESSURE = "pressure"
 CONF_REACTIVE_POWER = "reactive_power"
 CONF_SIGNAL_STRENGTH = "signal_strength"
@@ -85,6 +86,9 @@ ENTITY_TRIGGERS = {
     SensorDeviceClass.PM25: [{CONF_TYPE: CONF_PM25}],
     SensorDeviceClass.POWER: [{CONF_TYPE: CONF_POWER}],
     SensorDeviceClass.POWER_FACTOR: [{CONF_TYPE: CONF_POWER_FACTOR}],
+    SensorDeviceClass.PRECIPITATION_INTENSITY: [
+        {CONF_TYPE: CONF_PRECIPITATION_INTENSITY}
+    ],
     SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_PRESSURE}],
     SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_REACTIVE_POWER}],
     SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}],
-- 
GitLab


From 6ea6782d236a1823aca5d498a3769b0864856df5 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Wed, 19 Oct 2022 17:46:54 +0200
Subject: [PATCH 604/985] Add buttons to dismiss notifications in LaMetric
 (#80605)

---
 homeassistant/components/lametric/button.py |  14 +++
 tests/components/lametric/test_button.py    | 104 ++++++++++++++++++++
 2 files changed, 118 insertions(+)

diff --git a/homeassistant/components/lametric/button.py b/homeassistant/components/lametric/button.py
index 8de7ddb16ff..f6c2b03b02c 100644
--- a/homeassistant/components/lametric/button.py
+++ b/homeassistant/components/lametric/button.py
@@ -48,6 +48,20 @@ BUTTONS = [
         entity_category=EntityCategory.CONFIG,
         press_fn=lambda api: api.app_previous(),
     ),
+    LaMetricButtonEntityDescription(
+        key="dismiss_current",
+        name="Dismiss current notification",
+        icon="mdi:bell-cancel",
+        entity_category=EntityCategory.CONFIG,
+        press_fn=lambda api: api.dismiss_current_notification(),
+    ),
+    LaMetricButtonEntityDescription(
+        key="dismiss_all",
+        name="Dismiss all notifications",
+        icon="mdi:bell-cancel",
+        entity_category=EntityCategory.CONFIG,
+        press_fn=lambda api: api.dismiss_all_notifications(),
+    ),
 ]
 
 
diff --git a/tests/components/lametric/test_button.py b/tests/components/lametric/test_button.py
index d37b6dd1c18..db1f0c57929 100644
--- a/tests/components/lametric/test_button.py
+++ b/tests/components/lametric/test_button.py
@@ -120,6 +120,110 @@ async def test_button_app_previous(
     assert state.state == "2022-09-19T12:07:30+00:00"
 
 
+@pytest.mark.freeze_time("2022-10-19 12:44:00")
+async def test_button_dismiss_current_notification(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_lametric: MagicMock,
+) -> None:
+    """Test the LaMetric dismiss current notification button."""
+    device_registry = dr.async_get(hass)
+    entity_registry = er.async_get(hass)
+
+    state = hass.states.get("button.frenck_s_lametric_dismiss_current_notification")
+    assert state
+    assert state.attributes.get(ATTR_ICON) == "mdi:bell-cancel"
+    assert state.state == STATE_UNKNOWN
+
+    entry = entity_registry.async_get(
+        "button.frenck_s_lametric_dismiss_current_notification"
+    )
+    assert entry
+    assert entry.unique_id == "SA110405124500W00BS9-dismiss_current"
+    assert entry.entity_category == EntityCategory.CONFIG
+
+    assert entry.device_id
+    device_entry = device_registry.async_get(entry.device_id)
+    assert device_entry
+    assert device_entry.configuration_url is None
+    assert device_entry.connections == {
+        (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
+    }
+    assert device_entry.entry_type is None
+    assert device_entry.identifiers == {(DOMAIN, "SA110405124500W00BS9")}
+    assert device_entry.manufacturer == "LaMetric Inc."
+    assert device_entry.model == "LM 37X8"
+    assert device_entry.name == "Frenck's LaMetric"
+    assert device_entry.sw_version == "2.2.2"
+    assert device_entry.hw_version is None
+
+    await hass.services.async_call(
+        BUTTON_DOMAIN,
+        SERVICE_PRESS,
+        {ATTR_ENTITY_ID: "button.frenck_s_lametric_dismiss_current_notification"},
+        blocking=True,
+    )
+
+    assert len(mock_lametric.dismiss_current_notification.mock_calls) == 1
+    mock_lametric.dismiss_current_notification.assert_called_with()
+
+    state = hass.states.get("button.frenck_s_lametric_dismiss_current_notification")
+    assert state
+    assert state.state == "2022-10-19T12:44:00+00:00"
+
+
+@pytest.mark.freeze_time("2022-10-19 12:44:00")
+async def test_button_dismiss_all_notifications(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_lametric: MagicMock,
+) -> None:
+    """Test the LaMetric dismiss all notifications button."""
+    device_registry = dr.async_get(hass)
+    entity_registry = er.async_get(hass)
+
+    state = hass.states.get("button.frenck_s_lametric_dismiss_all_notifications")
+    assert state
+    assert state.attributes.get(ATTR_ICON) == "mdi:bell-cancel"
+    assert state.state == STATE_UNKNOWN
+
+    entry = entity_registry.async_get(
+        "button.frenck_s_lametric_dismiss_all_notifications"
+    )
+    assert entry
+    assert entry.unique_id == "SA110405124500W00BS9-dismiss_all"
+    assert entry.entity_category == EntityCategory.CONFIG
+
+    assert entry.device_id
+    device_entry = device_registry.async_get(entry.device_id)
+    assert device_entry
+    assert device_entry.configuration_url is None
+    assert device_entry.connections == {
+        (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
+    }
+    assert device_entry.entry_type is None
+    assert device_entry.identifiers == {(DOMAIN, "SA110405124500W00BS9")}
+    assert device_entry.manufacturer == "LaMetric Inc."
+    assert device_entry.model == "LM 37X8"
+    assert device_entry.name == "Frenck's LaMetric"
+    assert device_entry.sw_version == "2.2.2"
+    assert device_entry.hw_version is None
+
+    await hass.services.async_call(
+        BUTTON_DOMAIN,
+        SERVICE_PRESS,
+        {ATTR_ENTITY_ID: "button.frenck_s_lametric_dismiss_all_notifications"},
+        blocking=True,
+    )
+
+    assert len(mock_lametric.dismiss_all_notifications.mock_calls) == 1
+    mock_lametric.dismiss_all_notifications.assert_called_with()
+
+    state = hass.states.get("button.frenck_s_lametric_dismiss_all_notifications")
+    assert state
+    assert state.state == "2022-10-19T12:44:00+00:00"
+
+
 @pytest.mark.freeze_time("2022-10-11 22:00:00")
 async def test_button_error(
     hass: HomeAssistant,
-- 
GitLab


From 374d46ec09e89132673617aad65ab4e76674b530 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 17:49:40 +0200
Subject: [PATCH 605/985] Improve type hints in blebox (#80511)

* Add generics to blebox

* Remove walrus

* Move logic into each platform

* Code style

* Apply suggestion
---
 homeassistant/components/blebox/__init__.py | 25 +++++----------------
 homeassistant/components/blebox/button.py   | 23 ++++++++++++-------
 homeassistant/components/blebox/climate.py  | 16 ++++++++-----
 homeassistant/components/blebox/cover.py    | 19 ++++++++++------
 homeassistant/components/blebox/light.py    | 17 +++++++-------
 homeassistant/components/blebox/sensor.py   | 19 ++++++++++------
 homeassistant/components/blebox/switch.py   | 18 ++++++++++-----
 tests/components/blebox/test_init.py        |  4 ++--
 8 files changed, 79 insertions(+), 62 deletions(-)

diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py
index 35a334f36f3..4314412a13d 100644
--- a/homeassistant/components/blebox/__init__.py
+++ b/homeassistant/components/blebox/__init__.py
@@ -1,5 +1,6 @@
 """The BleBox devices integration."""
 import logging
+from typing import Generic, TypeVar
 
 from blebox_uniapi.box import Box
 from blebox_uniapi.error import Error
@@ -8,7 +9,7 @@ from blebox_uniapi.session import ApiHost
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_HOST, CONF_PORT, Platform
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.entity import DeviceInfo, Entity
@@ -28,6 +29,8 @@ PLATFORMS = [
 
 PARALLEL_UPDATES = 0
 
+_FeatureT = TypeVar("_FeatureT", bound=Feature)
+
 
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Set up BleBox devices from a config entry."""
@@ -64,26 +67,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     return unload_ok
 
 
-@callback
-def create_blebox_entities(
-    hass, config_entry, async_add_entities, entity_klass, entity_type
-):
-    """Create entities from a BleBox product's features."""
-
-    product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
-    entities = []
-
-    if entity_type in product.features:
-        for feature in product.features[entity_type]:
-            entities.append(entity_klass(feature))
-
-    async_add_entities(entities, True)
-
-
-class BleBoxEntity(Entity):
+class BleBoxEntity(Entity, Generic[_FeatureT]):
     """Implements a common class for entities representing a BleBox feature."""
 
-    def __init__(self, feature: Feature) -> None:
+    def __init__(self, feature: _FeatureT) -> None:
         """Initialize a BleBox entity."""
         self._feature = feature
         self._attr_name = feature.full_name
diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py
index e9ceaac2dc7..93a5a4e0c5a 100644
--- a/homeassistant/components/blebox/button.py
+++ b/homeassistant/components/blebox/button.py
@@ -1,12 +1,16 @@
 """BleBox button entities implementation."""
 from __future__ import annotations
 
+from blebox_uniapi.box import Box
+import blebox_uniapi.button
+
 from homeassistant.components.button import ButtonEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from . import BleBoxEntity, create_blebox_entities
+from . import BleBoxEntity
+from .const import DOMAIN, PRODUCT
 
 
 async def async_setup_entry(
@@ -15,20 +19,23 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a BleBox button entry."""
-    create_blebox_entities(
-        hass, config_entry, async_add_entities, BleBoxButtonEntity, "buttons"
-    )
+    product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+
+    entities = [
+        BleBoxButtonEntity(feature) for feature in product.features.get("buttons", [])
+    ]
+    async_add_entities(entities, True)
 
 
-class BleBoxButtonEntity(BleBoxEntity, ButtonEntity):
+class BleBoxButtonEntity(BleBoxEntity[blebox_uniapi.button.Button], ButtonEntity):
     """Representation of BleBox buttons."""
 
-    def __init__(self, feature):
+    def __init__(self, feature: blebox_uniapi.button.Button) -> None:
         """Initialize a BleBox button feature."""
         super().__init__(feature)
         self._attr_icon = self.get_icon()
 
-    def get_icon(self):
+    def get_icon(self) -> str | None:
         """Return icon for endpoint."""
         if "up" in self._feature.query_string:
             return "mdi:arrow-up-circle"
@@ -40,7 +47,7 @@ class BleBoxButtonEntity(BleBoxEntity, ButtonEntity):
             return "mdi:arrow-up-circle"
         if "close" in self._feature.query_string:
             return "mdi:arrow-down-circle"
-        return ""
+        return None
 
     async def async_press(self) -> None:
         """Handle the button press."""
diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py
index 65920b170c5..9b632c9aceb 100644
--- a/homeassistant/components/blebox/climate.py
+++ b/homeassistant/components/blebox/climate.py
@@ -2,6 +2,9 @@
 from datetime import timedelta
 from typing import Any
 
+from blebox_uniapi.box import Box
+import blebox_uniapi.climate
+
 from homeassistant.components.climate import (
     ClimateEntity,
     ClimateEntityFeature,
@@ -13,7 +16,8 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from . import BleBoxEntity, create_blebox_entities
+from . import BleBoxEntity
+from .const import DOMAIN, PRODUCT
 
 SCAN_INTERVAL = timedelta(seconds=5)
 
@@ -24,13 +28,15 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a BleBox climate entity."""
+    product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
 
-    create_blebox_entities(
-        hass, config_entry, async_add_entities, BleBoxClimateEntity, "climates"
-    )
+    entities = [
+        BleBoxClimateEntity(feature) for feature in product.features.get("climates", [])
+    ]
+    async_add_entities(entities, True)
 
 
-class BleBoxClimateEntity(BleBoxEntity, ClimateEntity):
+class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEntity):
     """Representation of a BleBox climate feature (saunaBox)."""
 
     _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py
index 882356f1a77..80e2fbd30e7 100644
--- a/homeassistant/components/blebox/cover.py
+++ b/homeassistant/components/blebox/cover.py
@@ -3,6 +3,9 @@ from __future__ import annotations
 
 from typing import Any
 
+from blebox_uniapi.box import Box
+import blebox_uniapi.cover
+
 from homeassistant.components.cover import (
     ATTR_POSITION,
     CoverDeviceClass,
@@ -14,7 +17,8 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_O
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from . import BleBoxEntity, create_blebox_entities
+from . import BleBoxEntity
+from .const import DOMAIN, PRODUCT
 
 BLEBOX_TO_COVER_DEVICE_CLASSES = {
     "gate": CoverDeviceClass.GATE,
@@ -44,16 +48,17 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a BleBox entry."""
-
-    create_blebox_entities(
-        hass, config_entry, async_add_entities, BleBoxCoverEntity, "covers"
-    )
+    product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+    entities = [
+        BleBoxCoverEntity(feature) for feature in product.features.get("covers", [])
+    ]
+    async_add_entities(entities, True)
 
 
-class BleBoxCoverEntity(BleBoxEntity, CoverEntity):
+class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
     """Representation of a BleBox cover feature."""
 
-    def __init__(self, feature):
+    def __init__(self, feature: blebox_uniapi.cover.Cover) -> None:
         """Initialize a BleBox cover feature."""
         super().__init__(feature)
         self._attr_device_class = BLEBOX_TO_COVER_DEVICE_CLASSES[feature.device_class]
diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py
index c1245d52fec..b138aae15b7 100644
--- a/homeassistant/components/blebox/light.py
+++ b/homeassistant/components/blebox/light.py
@@ -5,6 +5,7 @@ from datetime import timedelta
 import logging
 from typing import Any
 
+from blebox_uniapi.box import Box
 import blebox_uniapi.light
 from blebox_uniapi.light import BleboxColorMode
 
@@ -23,7 +24,8 @@ from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from . import BleBoxEntity, create_blebox_entities
+from . import BleBoxEntity
+from .const import DOMAIN, PRODUCT
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -36,10 +38,11 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a BleBox entry."""
-
-    create_blebox_entities(
-        hass, config_entry, async_add_entities, BleBoxLightEntity, "lights"
-    )
+    product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+    entities = [
+        BleBoxLightEntity(feature) for feature in product.features.get("lights", [])
+    ]
+    async_add_entities(entities, True)
 
 
 COLOR_MODE_MAP = {
@@ -53,11 +56,9 @@ COLOR_MODE_MAP = {
 }
 
 
-class BleBoxLightEntity(BleBoxEntity, LightEntity):
+class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
     """Representation of BleBox lights."""
 
-    _feature: blebox_uniapi.light.Light
-
     def __init__(self, feature: blebox_uniapi.light.Light) -> None:
         """Initialize a BleBox light."""
         super().__init__(feature)
diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py
index f3c0c393fd9..60d94507f4b 100644
--- a/homeassistant/components/blebox/sensor.py
+++ b/homeassistant/components/blebox/sensor.py
@@ -1,6 +1,9 @@
 """BleBox sensor entities."""
 from dataclasses import dataclass
 
+from blebox_uniapi.box import Box
+import blebox_uniapi.sensor
+
 from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
@@ -11,7 +14,8 @@ from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, TEMP_C
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from . import BleBoxEntity, create_blebox_entities
+from . import BleBoxEntity
+from .const import DOMAIN, PRODUCT
 
 
 @dataclass
@@ -49,16 +53,17 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a BleBox entry."""
-
-    create_blebox_entities(
-        hass, config_entry, async_add_entities, BleBoxSensorEntity, "sensors"
-    )
+    product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+    entities = [
+        BleBoxSensorEntity(feature) for feature in product.features.get("sensors", [])
+    ]
+    async_add_entities(entities, True)
 
 
-class BleBoxSensorEntity(BleBoxEntity, SensorEntity):
+class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEntity):
     """Representation of a BleBox sensor feature."""
 
-    def __init__(self, feature):
+    def __init__(self, feature: blebox_uniapi.sensor.BaseSensor) -> None:
         """Initialize a BleBox sensor feature."""
         super().__init__(feature)
 
diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py
index 5ae37d6b34d..d7145ebb620 100644
--- a/homeassistant/components/blebox/switch.py
+++ b/homeassistant/components/blebox/switch.py
@@ -2,12 +2,16 @@
 from datetime import timedelta
 from typing import Any
 
+from blebox_uniapi.box import Box
+import blebox_uniapi.switch
+
 from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from . import BleBoxEntity, create_blebox_entities
+from . import BleBoxEntity
+from .const import DOMAIN, PRODUCT
 
 SCAN_INTERVAL = timedelta(seconds=5)
 
@@ -18,15 +22,17 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a BleBox switch entity."""
-    create_blebox_entities(
-        hass, config_entry, async_add_entities, BleBoxSwitchEntity, "switches"
-    )
+    product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+    entities = [
+        BleBoxSwitchEntity(feature) for feature in product.features.get("switches", [])
+    ]
+    async_add_entities(entities, True)
 
 
-class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity):
+class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity):
     """Representation of a BleBox switch feature."""
 
-    def __init__(self, feature):
+    def __init__(self, feature: blebox_uniapi.switch.Switch) -> None:
         """Initialize a BleBox switch feature."""
         super().__init__(feature)
         self._attr_device_class = SwitchDeviceClass.SWITCH
diff --git a/tests/components/blebox/test_init.py b/tests/components/blebox/test_init.py
index c0add2696b5..9320c3c271c 100644
--- a/tests/components/blebox/test_init.py
+++ b/tests/components/blebox/test_init.py
@@ -7,7 +7,7 @@ import blebox_uniapi
 from homeassistant.components.blebox.const import DOMAIN
 from homeassistant.config_entries import ConfigEntryState
 
-from .conftest import mock_config, patch_product_identify
+from .conftest import mock_config, patch_product_identify, setup_product_mock
 
 
 async def test_setup_failure(hass, caplog):
@@ -44,7 +44,7 @@ async def test_setup_failure_on_connection(hass, caplog):
 
 async def test_unload_config_entry(hass):
     """Test that unloading works properly."""
-    patch_product_identify(None)
+    setup_product_mock("switches", [])
 
     entry = mock_config()
     entry.add_to_hass(hass)
-- 
GitLab


From 2b2275dfb38ad438dc0e98dae62fd5665d52a231 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 18:54:50 +0200
Subject: [PATCH 606/985] Use US_CUSTOMARY_SYSTEM in components (#80623)

---
 homeassistant/components/citybikes/sensor.py       |  4 ++--
 homeassistant/components/ecowitt/sensor.py         |  4 ++--
 homeassistant/components/gdacs/__init__.py         |  4 ++--
 homeassistant/components/gdacs/geo_location.py     |  6 +++---
 .../components/geonetnz_quakes/__init__.py         |  4 ++--
 .../components/geonetnz_quakes/geo_location.py     |  6 +++---
 .../components/geonetnz_volcano/config_flow.py     |  4 ++--
 .../components/google_travel_time/sensor.py        |  4 ++--
 .../components/here_travel_time/config_flow.py     |  4 ++--
 homeassistant/components/mazda/sensor.py           |  4 ++--
 homeassistant/components/nissan_leaf/sensor.py     |  6 +++---
 homeassistant/components/nws/sensor.py             |  4 ++--
 homeassistant/components/rainmachine/select.py     |  4 ++--
 homeassistant/components/subaru/sensor.py          | 14 +++++++++-----
 homeassistant/components/tomorrowio/sensor.py      |  6 +++---
 .../components/waze_travel_time/sensor.py          |  4 ++--
 16 files changed, 43 insertions(+), 39 deletions(-)

diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py
index 25716b67464..5da74167a0b 100644
--- a/homeassistant/components/citybikes/sensor.py
+++ b/homeassistant/components/citybikes/sensor.py
@@ -37,7 +37,7 @@ from homeassistant.helpers.event import async_track_time_interval
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 from homeassistant.util import location
 from homeassistant.util.unit_conversion import DistanceConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -171,7 +171,7 @@ async def async_setup_platform(
     stations_list = set(config.get(CONF_STATIONS_LIST, []))
     radius = config.get(CONF_RADIUS, 0)
     name = config[CONF_NAME]
-    if hass.config.units is IMPERIAL_SYSTEM:
+    if hass.config.units is US_CUSTOMARY_SYSTEM:
         radius = DistanceConverter.convert(radius, LENGTH_FEET, LENGTH_METERS)
 
     # Create a single instance of CityBikesNetworks.
diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py
index 172561d7125..8c55c8bf801 100644
--- a/homeassistant/components/ecowitt/sensor.py
+++ b/homeassistant/components/ecowitt/sensor.py
@@ -40,7 +40,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import StateType
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from .const import DOMAIN
 from .entity import EcowittEntity
@@ -219,7 +219,7 @@ async def async_setup_entry(
         # Ignore metrics that are not supported by the user's locale
         if sensor.stype in _METRIC and hass.config.units is not METRIC_SYSTEM:
             return
-        if sensor.stype in _IMPERIAL and hass.config.units is not IMPERIAL_SYSTEM:
+        if sensor.stype in _IMPERIAL and hass.config.units is not US_CUSTOMARY_SYSTEM:
             return
         mapping = ECOWITT_SENSORS_MAPPING[sensor.stype]
 
diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py
index 71c06033469..269da061b58 100644
--- a/homeassistant/components/gdacs/__init__.py
+++ b/homeassistant/components/gdacs/__init__.py
@@ -20,7 +20,7 @@ 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_conversion import DistanceConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     CONF_CATEGORIES,
@@ -88,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
     feeds = hass.data[DOMAIN].setdefault(FEED, {})
 
     radius = config_entry.data[CONF_RADIUS]
-    if hass.config.units is IMPERIAL_SYSTEM:
+    if hass.config.units is US_CUSTOMARY_SYSTEM:
         radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS)
     # Create feed entity manager for all platforms.
     manager = GdacsFeedEntityManager(hass, config_entry, radius)
diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py
index b6083f2aa71..5d3b8f3375b 100644
--- a/homeassistant/components/gdacs/geo_location.py
+++ b/homeassistant/components/gdacs/geo_location.py
@@ -16,7 +16,7 @@ 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_conversion import DistanceConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import GdacsFeedEntityManager
 from .const import DEFAULT_ICON, DOMAIN, FEED
@@ -107,7 +107,7 @@ class GdacsEvent(GeolocationEvent):
 
     async def async_added_to_hass(self) -> None:
         """Call when entity is added to hass."""
-        if self.hass.config.units is IMPERIAL_SYSTEM:
+        if self.hass.config.units is US_CUSTOMARY_SYSTEM:
             self._attr_unit_of_measurement = LENGTH_MILES
         self._remove_signal_delete = async_dispatcher_connect(
             self.hass, f"gdacs_delete_{self._external_id}", self._delete_callback
@@ -149,7 +149,7 @@ class GdacsEvent(GeolocationEvent):
             event_name = f"{feed_entry.country} ({feed_entry.event_id})"
         self._attr_name = f"{feed_entry.event_type}: {event_name}"
         # Convert distance if not metric system.
-        if self.hass.config.units is IMPERIAL_SYSTEM:
+        if self.hass.config.units is US_CUSTOMARY_SYSTEM:
             self._attr_distance = DistanceConverter.convert(
                 feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES
             )
diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py
index bef0ee4c1fd..e09b4e720e3 100644
--- a/homeassistant/components/geonetnz_quakes/__init__.py
+++ b/homeassistant/components/geonetnz_quakes/__init__.py
@@ -20,7 +20,7 @@ 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_conversion import DistanceConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     CONF_MINIMUM_MAGNITUDE,
@@ -95,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
     feeds = hass.data[DOMAIN].setdefault(FEED, {})
 
     radius = config_entry.data[CONF_RADIUS]
-    if hass.config.units is IMPERIAL_SYSTEM:
+    if hass.config.units is US_CUSTOMARY_SYSTEM:
         radius = DistanceConverter.convert(radius, LENGTH_MILES, LENGTH_KILOMETERS)
     # Create feed entity manager for all platforms.
     manager = GeonetnzQuakesFeedEntityManager(hass, config_entry, radius)
diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py
index bd382f1767d..a530c2d8fdb 100644
--- a/homeassistant/components/geonetnz_quakes/geo_location.py
+++ b/homeassistant/components/geonetnz_quakes/geo_location.py
@@ -15,7 +15,7 @@ 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_conversion import DistanceConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import GeonetnzQuakesFeedEntityManager
 from .const import DOMAIN, FEED
@@ -93,7 +93,7 @@ class GeonetnzQuakesEvent(GeolocationEvent):
 
     async def async_added_to_hass(self) -> None:
         """Call when entity is added to hass."""
-        if self.hass.config.units is IMPERIAL_SYSTEM:
+        if self.hass.config.units is US_CUSTOMARY_SYSTEM:
             self._attr_unit_of_measurement = LENGTH_MILES
         self._remove_signal_delete = async_dispatcher_connect(
             self.hass,
@@ -136,7 +136,7 @@ class GeonetnzQuakesEvent(GeolocationEvent):
         """Update the internal state from the provided feed entry."""
         self._attr_name = feed_entry.title
         # Convert distance if not metric system.
-        if self.hass.config.units is IMPERIAL_SYSTEM:
+        if self.hass.config.units is US_CUSTOMARY_SYSTEM:
             self._attr_distance = DistanceConverter.convert(
                 feed_entry.distance_to_home, LENGTH_KILOMETERS, LENGTH_MILES
             )
diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py
index d095aefab01..34e16970d4f 100644
--- a/homeassistant/components/geonetnz_volcano/config_flow.py
+++ b/homeassistant/components/geonetnz_volcano/config_flow.py
@@ -11,7 +11,7 @@ from homeassistant.const import (
 )
 from homeassistant.core import callback
 from homeassistant.helpers import config_validation as cv
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     DEFAULT_RADIUS,
@@ -62,7 +62,7 @@ class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         if identifier in configured_instances(self.hass):
             return await self._show_form({"base": "already_configured"})
 
-        if self.hass.config.units is IMPERIAL_SYSTEM:
+        if self.hass.config.units is US_CUSTOMARY_SYSTEM:
             user_input[CONF_UNIT_SYSTEM] = IMPERIAL_UNITS
         else:
             user_input[CONF_UNIT_SYSTEM] = METRIC_UNITS
diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py
index 3ea38af1f27..412f1738294 100644
--- a/homeassistant/components/google_travel_time/sensor.py
+++ b/homeassistant/components/google_travel_time/sensor.py
@@ -22,7 +22,7 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.location import find_coordinates
 import homeassistant.util.dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     ATTRIBUTION,
@@ -66,7 +66,7 @@ async def async_setup_entry(
 
         if CONF_UNITS not in options:
             options[CONF_UNITS] = UNITS_METRIC
-            if hass.config.units is IMPERIAL_SYSTEM:
+            if hass.config.units is US_CUSTOMARY_SYSTEM:
                 options[CONF_UNITS] = UNITS_IMPERIAL
 
         if CONF_TRAVEL_MODE in new_data:
diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py
index a0f1fbbeb8c..38bd1742c91 100644
--- a/homeassistant/components/here_travel_time/config_flow.py
+++ b/homeassistant/components/here_travel_time/config_flow.py
@@ -24,7 +24,7 @@ from homeassistant.helpers.selector import (
     LocationSelector,
     TimeSelector,
 )
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     CONF_ARRIVAL_TIME,
@@ -98,7 +98,7 @@ def default_options(hass: HomeAssistant) -> dict[str, str | None]:
         CONF_DEPARTURE_TIME: None,
         CONF_UNIT_SYSTEM: METRIC_UNITS,
     }
-    if hass.config.units is IMPERIAL_SYSTEM:
+    if hass.config.units is US_CUSTOMARY_SYSTEM:
         default[CONF_UNIT_SYSTEM] = IMPERIAL_UNITS
     return default
 
diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py
index b322c80b6e6..212d646051c 100644
--- a/homeassistant/components/mazda/sensor.py
+++ b/homeassistant/components/mazda/sensor.py
@@ -21,7 +21,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import StateType
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, UnitSystem
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM, UnitSystem
 
 from . import MazdaEntity
 from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@@ -51,7 +51,7 @@ class MazdaSensorEntityDescription(
 
 def _get_distance_unit(unit_system: UnitSystem) -> str:
     """Return the distance unit for the given unit system."""
-    if unit_system is IMPERIAL_SYSTEM:
+    if unit_system is US_CUSTOMARY_SYSTEM:
         return LENGTH_MILES
     return LENGTH_KILOMETERS
 
diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py
index 4543467f932..c92ed2300a4 100644
--- a/homeassistant/components/nissan_leaf/sensor.py
+++ b/homeassistant/components/nissan_leaf/sensor.py
@@ -13,7 +13,7 @@ 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_conversion import DistanceConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import LeafEntity
 from .const import (
@@ -123,7 +123,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity):
         if ret is None:
             return None
 
-        if self.car.hass.config.units is IMPERIAL_SYSTEM or self.car.force_miles:
+        if self.car.hass.config.units is US_CUSTOMARY_SYSTEM or self.car.force_miles:
             ret = DistanceConverter.convert(ret, LENGTH_KILOMETERS, LENGTH_MILES)
 
         return round(ret)
@@ -131,7 +131,7 @@ class LeafRangeSensor(LeafEntity, SensorEntity):
     @property
     def native_unit_of_measurement(self) -> str:
         """Battery range unit."""
-        if self.car.hass.config.units is IMPERIAL_SYSTEM or self.car.force_miles:
+        if self.car.hass.config.units is US_CUSTOMARY_SYSTEM or self.car.force_miles:
             return LENGTH_MILES
         return LENGTH_KILOMETERS
 
diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py
index ab34781f209..2e7495701a9 100644
--- a/homeassistant/components/nws/sensor.py
+++ b/homeassistant/components/nws/sensor.py
@@ -23,7 +23,7 @@ from homeassistant.util.unit_conversion import (
     PressureConverter,
     SpeedConverter,
 )
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import base_unique_id, device_info
 from .const import (
@@ -81,7 +81,7 @@ class NWSSensor(CoordinatorEntity, SensorEntity):
         self.entity_description = description
 
         self._attr_name = f"{station} {description.name}"
-        if hass.config.units is IMPERIAL_SYSTEM:
+        if hass.config.units is US_CUSTOMARY_SYSTEM:
             self._attr_native_unit_of_measurement = description.unit_convert
 
     @property
diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py
index 12860e59e79..9010b9b4fed 100644
--- a/homeassistant/components/rainmachine/select.py
+++ b/homeassistant/components/rainmachine/select.py
@@ -11,7 +11,7 @@ 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 homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM, UnitSystem
 
 from . import RainMachineData, RainMachineEntity
 from .const import DATA_RESTRICTIONS_UNIVERSAL, DOMAIN
@@ -130,7 +130,7 @@ class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity):
         self._label_to_api_value_map = {}
 
         for option in description.extended_options:
-            if unit_system is IMPERIAL_SYSTEM:
+            if unit_system is US_CUSTOMARY_SYSTEM:
                 label = option.imperial_label
             else:
                 label = option.metric_label
diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py
index 672fe6b0dcc..bb467fc77de 100644
--- a/homeassistant/components/subaru/sensor.py
+++ b/homeassistant/components/subaru/sensor.py
@@ -30,7 +30,11 @@ from homeassistant.helpers.update_coordinator import (
     DataUpdateCoordinator,
 )
 from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, LENGTH_UNITS, PRESSURE_UNITS
+from homeassistant.util.unit_system import (
+    LENGTH_UNITS,
+    PRESSURE_UNITS,
+    US_CUSTOMARY_SYSTEM,
+)
 
 from . import get_device_info
 from .const import (
@@ -223,7 +227,7 @@ class SubaruSensor(
         if unit in LENGTH_UNITS:
             return round(unit_system.length(current_value, unit), 1)
 
-        if unit in PRESSURE_UNITS and unit_system == IMPERIAL_SYSTEM:
+        if unit in PRESSURE_UNITS and unit_system == US_CUSTOMARY_SYSTEM:
             return round(
                 unit_system.pressure(current_value, unit),
                 1,
@@ -235,7 +239,7 @@ class SubaruSensor(
                 FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
                 FUEL_CONSUMPTION_MILES_PER_GALLON,
             ]
-            and unit_system == IMPERIAL_SYSTEM
+            and unit_system == US_CUSTOMARY_SYSTEM
         ):
             return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1)
 
@@ -250,14 +254,14 @@ class SubaruSensor(
             return self.hass.config.units.length_unit
 
         if unit in PRESSURE_UNITS:
-            if self.hass.config.units == IMPERIAL_SYSTEM:
+            if self.hass.config.units == US_CUSTOMARY_SYSTEM:
                 return self.hass.config.units.pressure_unit
 
         if unit in [
             FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
             FUEL_CONSUMPTION_MILES_PER_GALLON,
         ]:
-            if self.hass.config.units == IMPERIAL_SYSTEM:
+            if self.hass.config.units == US_CUSTOMARY_SYSTEM:
                 return FUEL_CONSUMPTION_MILES_PER_GALLON
 
         return unit
diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py
index 78c973fd80b..07b922e72ed 100644
--- a/homeassistant/components/tomorrowio/sensor.py
+++ b/homeassistant/components/tomorrowio/sensor.py
@@ -38,7 +38,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 homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity
 from .const import (
@@ -327,7 +327,7 @@ class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity):
         )
         if self.entity_description.native_unit_of_measurement is None:
             self._attr_native_unit_of_measurement = description.unit_metric
-            if hass.config.units is IMPERIAL_SYSTEM:
+            if hass.config.units is US_CUSTOMARY_SYSTEM:
                 self._attr_native_unit_of_measurement = description.unit_imperial
 
     @property
@@ -356,7 +356,7 @@ class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity):
             desc.imperial_conversion
             and desc.unit_imperial is not None
             and desc.unit_imperial != desc.unit_metric
-            and self.hass.config.units is IMPERIAL_SYSTEM
+            and self.hass.config.units is US_CUSTOMARY_SYSTEM
         ):
             return handle_conversion(state, desc.imperial_conversion)
 
diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py
index 67081aeab5f..d9916ae7df1 100644
--- a/homeassistant/components/waze_travel_time/sensor.py
+++ b/homeassistant/components/waze_travel_time/sensor.py
@@ -26,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.location import find_coordinates
 from homeassistant.util.unit_conversion import DistanceConverter
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     CONF_AVOID_FERRIES,
@@ -69,7 +69,7 @@ async def async_setup_entry(
         CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS,
         CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS,
     }
-    if hass.config.units is IMPERIAL_SYSTEM:
+    if hass.config.units is US_CUSTOMARY_SYSTEM:
         defaults[CONF_UNITS] = IMPERIAL_UNITS
 
     if not config_entry.options:
-- 
GitLab


From 04cdcad7f8e3ea9eb838b7d9449011e5fec10999 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Wed, 19 Oct 2022 19:54:40 +0200
Subject: [PATCH 607/985] Expose UniFi PoE ports as individual switches
 (#80566)

* Add simple PoE control switches

* Add basic tests

* Complete testing

* Dont use port.up as part of available

* Bump aiounifi to v40
---
 homeassistant/components/unifi/manifest.json |   2 +-
 homeassistant/components/unifi/switch.py     |  87 ++++++++++++-
 requirements_all.txt                         |   2 +-
 requirements_test_all.txt                    |   2 +-
 tests/components/unifi/test_switch.py        | 126 ++++++++++++++++++-
 5 files changed, 213 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json
index 365ce086fb0..56b31b669e2 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==39"],
+  "requirements": ["aiounifi==40"],
   "codeowners": ["@Kane610"],
   "quality_scale": "platinum",
   "ssdp": [
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index fefcfcc56c6..df935822c5a 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -8,7 +8,7 @@ Support for controlling deep packet inspection (DPI) restriction groups.
 import asyncio
 from typing import Any
 
-from aiounifi.interfaces.api_handlers import SOURCE_EVENT
+from aiounifi.interfaces.api_handlers import SOURCE_EVENT, ItemEvent
 from aiounifi.models.client import ClientBlockRequest
 from aiounifi.models.device import (
     DeviceSetOutletRelayRequest,
@@ -111,6 +111,18 @@ async def async_setup_entry(
     items_added()
     known_poe_clients.clear()
 
+    @callback
+    def async_add_poe_switch(_: ItemEvent, obj_id: str) -> None:
+        """Add port PoE switch from UniFi controller."""
+        if not controller.api.ports[obj_id].port_poe:
+            return
+        async_add_entities([UnifiPoePortSwitch(obj_id, controller)])
+
+    controller.api.ports.subscribe(async_add_poe_switch, ItemEvent.ADDED)
+
+    for port_idx in controller.api.ports:
+        async_add_poe_switch(ItemEvent.ADDED, port_idx)
+
 
 @callback
 def add_block_entities(controller, async_add_entities, clients):
@@ -550,3 +562,76 @@ class UniFiOutletSwitch(UniFiBase, SwitchEntity):
 
     async def options_updated(self) -> None:
         """Config entry options are updated, no options to act on."""
+
+
+class UnifiPoePortSwitch(SwitchEntity):
+    """Representation of a Power-over-Ethernet source port on an UniFi device."""
+
+    _attr_device_class = SwitchDeviceClass.OUTLET
+    _attr_entity_category = EntityCategory.CONFIG
+    _attr_entity_registry_enabled_default = False
+    _attr_has_entity_name = True
+    _attr_icon = "mdi:ethernet"
+    _attr_should_poll = False
+
+    def __init__(self, obj_id: str, controller) -> None:
+        """Set up UniFi Network entity base."""
+        self._attr_unique_id = f"{obj_id}-PoE"
+        self._device_mac, port_idx = obj_id.split("_", 1)
+        self._port_idx = int(port_idx)
+        self._obj_id = obj_id
+        self.controller = controller
+
+        port = self.controller.api.ports[self._obj_id]
+        self._attr_name = f"{port.name} PoE"
+        self._attr_is_on = port.poe_mode != "off"
+
+        device = self.controller.api.devices[self._device_mac]
+        self._attr_device_info = DeviceInfo(
+            connections={(CONNECTION_NETWORK_MAC, device.mac)},
+            manufacturer=ATTR_MANUFACTURER,
+            model=device.model,
+            name=device.name or None,
+            sw_version=device.version,
+        )
+
+    async def async_added_to_hass(self) -> None:
+        """Entity created."""
+        self.async_on_remove(
+            self.controller.api.ports.subscribe(self.async_signalling_callback)
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                self.controller.signal_reachable,
+                self.async_signal_reachable_callback,
+            )
+        )
+
+    @callback
+    def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
+        """Object has new event."""
+        device = self.controller.api.devices[self._device_mac]
+        port = self.controller.api.ports[self._obj_id]
+        self._attr_available = self.controller.available and not device.disabled
+        self._attr_is_on = port.poe_mode != "off"
+        self.async_write_ha_state()
+
+    @callback
+    def async_signal_reachable_callback(self) -> None:
+        """Call when controller connection state change."""
+        self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
+
+    async def async_turn_on(self, **kwargs: Any) -> None:
+        """Enable POE for client."""
+        device = self.controller.api.devices[self._device_mac]
+        await self.controller.api.request(
+            DeviceSetPoePortModeRequest.create(device, self._port_idx, "auto")
+        )
+
+    async def async_turn_off(self, **kwargs: Any) -> None:
+        """Disable POE for client."""
+        device = self.controller.api.devices[self._device_mac]
+        await self.controller.api.request(
+            DeviceSetPoePortModeRequest.create(device, self._port_idx, "off")
+        )
diff --git a/requirements_all.txt b/requirements_all.txt
index 8a1320eaa17..f47a777466e 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==39
+aiounifi==40
 
 # homeassistant.components.vlc_telnet
 aiovlc==0.1.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 3b3b488ca15..ee41f529787 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==39
+aiounifi==40
 
 # homeassistant.components.vlc_telnet
 aiovlc==0.1.0
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index 9965c25a1b5..41494114c1e 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -1,13 +1,18 @@
 """UniFi Network switch platform tests."""
+
 from copy import deepcopy
+from datetime import timedelta
 
 from aiounifi.controller import MESSAGE_CLIENT_REMOVED, MESSAGE_DEVICE, MESSAGE_EVENT
+from aiounifi.models.message import MessageKey
+from aiounifi.websocket import WebsocketState
 
 from homeassistant import config_entries, core
 from homeassistant.components.switch import (
     DOMAIN as SWITCH_DOMAIN,
     SERVICE_TURN_OFF,
     SERVICE_TURN_ON,
+    SwitchDeviceClass,
 )
 from homeassistant.components.unifi.const import (
     CONF_BLOCK_CLIENT,
@@ -18,10 +23,19 @@ from homeassistant.components.unifi.const import (
     DOMAIN as UNIFI_DOMAIN,
 )
 from homeassistant.components.unifi.switch import POE_SWITCH
-from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
+from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
+from homeassistant.const import (
+    ATTR_DEVICE_CLASS,
+    ATTR_ENTITY_ID,
+    STATE_OFF,
+    STATE_ON,
+    STATE_UNAVAILABLE,
+)
 from homeassistant.helpers import entity_registry as er
 from homeassistant.helpers.dispatcher import async_dispatcher_send
 from homeassistant.helpers.entity import EntityCategory
+from homeassistant.helpers.entity_registry import RegistryEntryDisabler
+from homeassistant.util import dt
 
 from .test_controller import (
     CONTROLLER_HOST,
@@ -31,7 +45,7 @@ from .test_controller import (
     setup_unifi_integration,
 )
 
-from tests.common import mock_restore_cache
+from tests.common import async_fire_time_changed, mock_restore_cache
 
 CLIENT_1 = {
     "hostname": "client_1",
@@ -1373,3 +1387,111 @@ async def test_restore_client_no_old_state(hass, aioclient_mock):
 
     poe_client = hass.states.get("switch.poe_client")
     assert poe_client.state == "unavailable"  # self.poe_mode is None
+
+
+async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket):
+    """Test the update_items function with some clients."""
+    config_entry = await setup_unifi_integration(
+        hass, aioclient_mock, devices_response=[DEVICE_1]
+    )
+    controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
+
+    assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
+
+    ent_reg = er.async_get(hass)
+    ent_reg_entry = ent_reg.async_get("switch.mock_name_port_1_poe")
+    assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION
+    assert ent_reg_entry.entity_category is EntityCategory.CONFIG
+
+    # Enable entity
+    ent_reg.async_update_entity(
+        entity_id="switch.mock_name_port_1_poe", disabled_by=None
+    )
+    await hass.async_block_till_done()
+
+    async_fire_time_changed(
+        hass,
+        dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
+    )
+    await hass.async_block_till_done()
+
+    # Validate state object
+    switch_1 = hass.states.get("switch.mock_name_port_1_poe")
+    assert switch_1 is not None
+    assert switch_1.state == STATE_ON
+    assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET
+
+    # Update state object
+    device_1 = deepcopy(DEVICE_1)
+    device_1["port_table"][0]["poe_mode"] = "off"
+    mock_unifi_websocket(
+        data={
+            "meta": {"message": MessageKey.DEVICE.value},
+            "data": [device_1],
+        }
+    )
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
+
+    # Turn off PoE
+    aioclient_mock.clear_requests()
+    aioclient_mock.put(
+        f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/mock-id",
+    )
+
+    await hass.services.async_call(
+        SWITCH_DOMAIN,
+        "turn_off",
+        {"entity_id": "switch.mock_name_port_1_poe"},
+        blocking=True,
+    )
+    assert aioclient_mock.call_count == 1
+    assert aioclient_mock.mock_calls[0][2] == {
+        "port_overrides": [{"poe_mode": "off", "port_idx": 1, "portconf_id": "1a1"}]
+    }
+
+    # Turn on PoE
+    await hass.services.async_call(
+        SWITCH_DOMAIN,
+        "turn_on",
+        {"entity_id": "switch.mock_name_port_1_poe"},
+        blocking=True,
+    )
+    assert aioclient_mock.call_count == 2
+    assert aioclient_mock.mock_calls[1][2] == {
+        "port_overrides": [{"poe_mode": "auto", "port_idx": 1, "portconf_id": "1a1"}]
+    }
+
+    # Availability signalling
+
+    # Controller disconnects
+    mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE
+
+    # Controller reconnects
+    mock_unifi_websocket(state=WebsocketState.RUNNING)
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
+
+    # Device gets disabled
+    device_1["disabled"] = True
+    mock_unifi_websocket(
+        data={
+            "meta": {"message": MessageKey.DEVICE.value},
+            "data": [device_1],
+        }
+    )
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE
+
+    # Device gets re-enabled
+    device_1["disabled"] = False
+    mock_unifi_websocket(
+        data={
+            "meta": {"message": MessageKey.DEVICE.value},
+            "data": [device_1],
+        }
+    )
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
-- 
GitLab


From be3a0228102144e92807816bc0720ea0cedcea55 Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Wed, 19 Oct 2022 20:40:32 +0200
Subject: [PATCH 608/985] Limit recorder pytest job [ci] (#80625)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
---
 .github/workflows/ci.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 53221be7cb8..8425635abb2 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -874,7 +874,7 @@ jobs:
     if: |
       (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core')
       && github.event.inputs.lint-only != 'true'
-      && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob)
+      && needs.info.outputs.test_full_suite == 'true'
     needs:
       - info
       - base
-- 
GitLab


From bd0838cd491c70ecc88aefc95d153fb38d169397 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 19 Oct 2022 21:54:07 +0200
Subject: [PATCH 609/985] Tweak MariaDB CI job (#80631)

---
 .github/workflows/ci.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8425635abb2..36003cac2f3 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -970,6 +970,7 @@ jobs:
     needs:
       - info
       - pytest
+      - pytest-mariadb
     steps:
       - name: Check out code from GitHub
         uses: actions/checkout@v3.1.0
-- 
GitLab


From 60a208a8603a3db86d881bf3f180cf3a22adb039 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Wed, 19 Oct 2022 21:56:19 +0200
Subject: [PATCH 610/985] Add integration_type to Axis, deCONZ and UniFi
 manifest (#80630)

---
 homeassistant/components/axis/manifest.json   | 17 +++++++++++++----
 homeassistant/components/deconz/manifest.json |  1 +
 homeassistant/components/unifi/manifest.json  |  1 +
 homeassistant/generated/integrations.json     |  2 +-
 4 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json
index c69d7346b9e..aad057cd4b3 100644
--- a/homeassistant/components/axis/manifest.json
+++ b/homeassistant/components/axis/manifest.json
@@ -5,7 +5,9 @@
   "documentation": "https://www.home-assistant.io/integrations/axis",
   "requirements": ["axis==44"],
   "dhcp": [
-    { "registered_devices": true },
+    {
+      "registered_devices": true
+    },
     {
       "hostname": "axis-00408c*",
       "macaddress": "00408C*"
@@ -27,20 +29,27 @@
   "zeroconf": [
     {
       "type": "_axis-video._tcp.local.",
-      "properties": { "macaddress": "00408c*" }
+      "properties": {
+        "macaddress": "00408c*"
+      }
     },
     {
       "type": "_axis-video._tcp.local.",
-      "properties": { "macaddress": "accc8e*" }
+      "properties": {
+        "macaddress": "accc8e*"
+      }
     },
     {
       "type": "_axis-video._tcp.local.",
-      "properties": { "macaddress": "b8a44f*" }
+      "properties": {
+        "macaddress": "b8a44f*"
+      }
     }
   ],
   "after_dependencies": ["mqtt"],
   "codeowners": ["@Kane610"],
   "quality_scale": "platinum",
   "iot_class": "local_push",
+  "integration_type": "device",
   "loggers": ["axis"]
 }
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 81bdc974f25..5de15b16177 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -13,5 +13,6 @@
   "codeowners": ["@Kane610"],
   "quality_scale": "platinum",
   "iot_class": "local_push",
+  "integration_type": "hub",
   "loggers": ["pydeconz"]
 }
diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json
index 56b31b669e2..ad5178c2d29 100644
--- a/homeassistant/components/unifi/manifest.json
+++ b/homeassistant/components/unifi/manifest.json
@@ -21,5 +21,6 @@
     }
   ],
   "iot_class": "local_push",
+  "integration_type": "hub",
   "loggers": ["aiounifi"]
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index f4c3c9cc76b..019529a94f0 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -445,7 +445,7 @@
     "axis": {
       "config_flow": true,
       "iot_class": "local_push",
-      "integration_type": "hub",
+      "integration_type": "device",
       "name": "Axis"
     },
     "baf": {
-- 
GitLab


From bad840e464981ac0a0787446dd9e591904605055 Mon Sep 17 00:00:00 2001
From: rozie <rozie@poczta.onet.pl>
Date: Wed, 19 Oct 2022 22:48:39 +0200
Subject: [PATCH 611/985] Fix solaredge missing data value (#80321)

* Fix issue #80263: use get to fetch dict value

* use None instead -1 for unknown value

* Update homeassistant/components/solaredge/coordinator.py

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Add guards for not multipling None

* Missing if added

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
---
 homeassistant/components/solaredge/coordinator.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py
index 7d3e949fc10..5def236598a 100644
--- a/homeassistant/components/solaredge/coordinator.py
+++ b/homeassistant/components/solaredge/coordinator.py
@@ -276,17 +276,19 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService):
 
         for key, value in power_flow.items():
             if key in ["LOAD", "PV", "GRID", "STORAGE"]:
-                self.data[key] = value["currentPower"]
+                self.data[key] = value.get("currentPower")
                 self.attributes[key] = {"status": value["status"]}
 
             if key in ["GRID"]:
                 export = key.lower() in power_to
-                self.data[key] *= -1 if export else 1
+                if self.data[key]:
+                    self.data[key] *= -1 if export else 1
                 self.attributes[key]["flow"] = "export" if export else "import"
 
             if key in ["STORAGE"]:
                 charge = key.lower() in power_to
-                self.data[key] *= -1 if charge else 1
+                if self.data[key]:
+                    self.data[key] *= -1 if charge else 1
                 self.attributes[key]["flow"] = "charge" if charge else "discharge"
                 self.attributes[key]["soc"] = value["chargeLevel"]
 
-- 
GitLab


From bdfead90958bb90e8e41dc69d0670325644d98b8 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 19 Oct 2022 23:02:11 +0200
Subject: [PATCH 612/985] Fix invalid warning in scrape (#80599)

---
 homeassistant/components/scrape/sensor.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index d34080c1712..1f696d73007 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -170,7 +170,7 @@ class ScrapeSensor(SensorEntity):
                 else:
                     value = tag.text
         except IndexError:
-            _LOGGER.warning("Index '%s' not found in %s", self._attr, self.entity_id)
+            _LOGGER.warning("Index '%s' not found in %s", self._index, self.entity_id)
             value = None
         except KeyError:
             _LOGGER.warning(
-- 
GitLab


From eb141a532c0d3a52c7a43c44886c9273192a8a58 Mon Sep 17 00:00:00 2001
From: uvjustin <46082645+uvjustin@users.noreply.github.com>
Date: Thu, 20 Oct 2022 05:06:49 +0800
Subject: [PATCH 613/985] Bump ha-av to v10.0.0 (#80514)

---
 homeassistant/components/generic/manifest.json | 2 +-
 homeassistant/components/stream/manifest.json  | 2 +-
 requirements_all.txt                           | 2 +-
 requirements_test_all.txt                      | 2 +-
 tests/components/stream/test_worker.py         | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json
index 78c3625abc7..83b34f73dc8 100644
--- a/homeassistant/components/generic/manifest.json
+++ b/homeassistant/components/generic/manifest.json
@@ -2,7 +2,7 @@
   "domain": "generic",
   "name": "Generic Camera",
   "config_flow": true,
-  "requirements": ["ha-av==10.0.0b5", "pillow==9.2.0"],
+  "requirements": ["ha-av==10.0.0", "pillow==9.2.0"],
   "dependencies": ["http"],
   "documentation": "https://www.home-assistant.io/integrations/generic",
   "codeowners": ["@davet2001"],
diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json
index 262c20e420a..fad6b80c3fb 100644
--- a/homeassistant/components/stream/manifest.json
+++ b/homeassistant/components/stream/manifest.json
@@ -2,7 +2,7 @@
   "domain": "stream",
   "name": "Stream",
   "documentation": "https://www.home-assistant.io/integrations/stream",
-  "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0b5"],
+  "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0"],
   "dependencies": ["http"],
   "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"],
   "quality_scale": "internal",
diff --git a/requirements_all.txt b/requirements_all.txt
index f47a777466e..792750f96e6 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -820,7 +820,7 @@ ha-HAP-python==4.5.2
 
 # homeassistant.components.generic
 # homeassistant.components.stream
-ha-av==10.0.0b5
+ha-av==10.0.0
 
 # homeassistant.components.ffmpeg
 ha-ffmpeg==3.0.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index ee41f529787..75bd9a50505 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -612,7 +612,7 @@ ha-HAP-python==4.5.2
 
 # homeassistant.components.generic
 # homeassistant.components.stream
-ha-av==10.0.0b5
+ha-av==10.0.0
 
 # homeassistant.components.ffmpeg
 ha-ffmpeg==3.0.2
diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py
index 54400af65ab..e77b062fa9c 100644
--- a/tests/components/stream/test_worker.py
+++ b/tests/components/stream/test_worker.py
@@ -794,7 +794,7 @@ async def test_durations(hass, worker_finished_stream):
             assert math.isclose(
                 (av_part.duration - av_part.start_time) / av.time_base,
                 part.duration,
-                abs_tol=2 / av_part.streams.video[0].rate + 1e-6,
+                abs_tol=2 / av_part.streams.video[0].average_rate + 1e-6,
             )
             # Also check that the sum of the durations so far matches the last dts
             # in the media.
-- 
GitLab


From d0ed4b1ff26774a187ffb9b26fe0f58a8c6fcf51 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Wed, 19 Oct 2022 23:10:01 +0200
Subject: [PATCH 614/985] Replace constants with enums in UniFi (#80637)

Replace constants with enums
Fix bad imports
---
 homeassistant/components/unifi/controller.py  |  2 +-
 .../components/unifi/device_tracker.py        |  2 +-
 homeassistant/components/unifi/switch.py      |  3 +-
 tests/components/unifi/conftest.py            |  6 +-
 tests/components/unifi/test_controller.py     | 13 ++--
 tests/components/unifi/test_device_tracker.py | 59 +++++++++----------
 tests/components/unifi/test_sensor.py         | 10 ++--
 tests/components/unifi/test_switch.py         | 13 ++--
 tests/components/unifi/test_update.py         | 16 +++--
 9 files changed, 59 insertions(+), 65 deletions(-)

diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 6ad8e416445..1f3fd48140e 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -9,7 +9,7 @@ from typing import Any
 
 from aiohttp import CookieJar
 import aiounifi
-from aiounifi.controller import (
+from aiounifi.interfaces.messages import (
     DATA_CLIENT_REMOVED,
     DATA_DPI_GROUP,
     DATA_DPI_GROUP_REMOVED,
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index f184663291d..1c91ea4724b 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -3,7 +3,7 @@
 from datetime import timedelta
 import logging
 
-from aiounifi.interfaces.api_handlers import SOURCE_DATA, SOURCE_EVENT
+from aiounifi.models.api import SOURCE_DATA, SOURCE_EVENT
 from aiounifi.models.event import EventKey
 
 from homeassistant.components.device_tracker import DOMAIN, SourceType
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index df935822c5a..68fe84b6b60 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -8,7 +8,8 @@ Support for controlling deep packet inspection (DPI) restriction groups.
 import asyncio
 from typing import Any
 
-from aiounifi.interfaces.api_handlers import SOURCE_EVENT, ItemEvent
+from aiounifi.interfaces.api_handlers import ItemEvent
+from aiounifi.models.api import SOURCE_EVENT
 from aiounifi.models.client import ClientBlockRequest
 from aiounifi.models.device import (
     DeviceSetOutletRelayRequest,
diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py
index e2b77ac1ed1..40635823836 100644
--- a/tests/components/unifi/conftest.py
+++ b/tests/components/unifi/conftest.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 
 from unittest.mock import patch
 
-from aiounifi.websocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA
+from aiounifi.websocket import WebsocketSignal
 import pytest
 
 from homeassistant.helpers import device_registry as dr
@@ -20,10 +20,10 @@ def mock_unifi_websocket():
             """Generate a websocket call."""
             if data:
                 mock.return_value.data = data
-                mock.call_args[1]["callback"](SIGNAL_DATA)
+                mock.call_args[1]["callback"](WebsocketSignal.DATA)
             elif state:
                 mock.return_value.state = state
-                mock.call_args[1]["callback"](SIGNAL_CONNECTION_STATE)
+                mock.call_args[1]["callback"](WebsocketSignal.CONNECTION_STATE)
             else:
                 raise NotImplementedError
 
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
index 5de99a3f434..0079e8e984c 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -7,7 +7,8 @@ from http import HTTPStatus
 from unittest.mock import Mock, patch
 
 import aiounifi
-from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
+from aiounifi.models.event import EventKey
+from aiounifi.websocket import WebsocketState
 import pytest
 
 from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
@@ -359,13 +360,13 @@ async def test_connection_state_signalling(
     # Controller is connected
     assert hass.states.get("device_tracker.client").state == "home"
 
-    mock_unifi_websocket(state=STATE_DISCONNECTED)
+    mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
     await hass.async_block_till_done()
 
     # Controller is disconnected
     assert hass.states.get("device_tracker.client").state == "unavailable"
 
-    mock_unifi_websocket(state=STATE_RUNNING)
+    mock_unifi_websocket(state=WebsocketState.RUNNING)
     await hass.async_block_till_done()
 
     # Controller is once again connected
@@ -403,7 +404,7 @@ async def test_wireless_client_event_calls_update_wireless_devices(
                     {
                         "datetime": "2020-01-20T19:37:04Z",
                         "user": "00:00:00:00:00:01",
-                        "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED,
+                        "key": EventKey.WIRELESS_CLIENT_CONNECTED.value,
                         "msg": "User[11:22:33:44:55:66] has connected to WLAN",
                         "time": 1579549024893,
                     }
@@ -423,7 +424,7 @@ async def test_reconnect_mechanism(hass, aioclient_mock, mock_unifi_websocket):
         f"https://{DEFAULT_HOST}:1234/api/login", status=HTTPStatus.BAD_GATEWAY
     )
 
-    mock_unifi_websocket(state=STATE_DISCONNECTED)
+    mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
     await hass.async_block_till_done()
 
     assert aioclient_mock.call_count == 0
@@ -459,7 +460,7 @@ async def test_reconnect_mechanism_exceptions(
     with patch("aiounifi.Controller.login", side_effect=exception), patch(
         "homeassistant.components.unifi.controller.UniFiController.reconnect"
     ) as mock_reconnect:
-        mock_unifi_websocket(state=STATE_DISCONNECTED)
+        mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
         await hass.async_block_till_done()
 
         new_time = dt_util.utcnow() + timedelta(seconds=RETRY_TIMER)
diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py
index e5d53a6d882..d004d96a73c 100644
--- a/tests/components/unifi/test_device_tracker.py
+++ b/tests/components/unifi/test_device_tracker.py
@@ -3,13 +3,8 @@
 from datetime import timedelta
 from unittest.mock import patch
 
-from aiounifi.controller import (
-    MESSAGE_CLIENT,
-    MESSAGE_CLIENT_REMOVED,
-    MESSAGE_DEVICE,
-    MESSAGE_EVENT,
-)
-from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
+from aiounifi.models.message import MessageKey
+from aiounifi.websocket import WebsocketState
 
 from homeassistant import config_entries
 from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
@@ -64,7 +59,7 @@ async def test_tracked_wireless_clients(
     client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -85,7 +80,7 @@ async def test_tracked_wireless_clients(
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -165,7 +160,7 @@ async def test_tracked_clients(
     client_1["last_seen"] += 1
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client_1],
         }
     )
@@ -215,7 +210,7 @@ async def test_tracked_wireless_clients_event_source(
     }
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_EVENT},
+            "meta": {"message": MessageKey.EVENT.value},
             "data": [event],
         }
     )
@@ -242,7 +237,7 @@ async def test_tracked_wireless_clients_event_source(
     }
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_EVENT},
+            "meta": {"message": MessageKey.EVENT.value},
             "data": [event],
         }
     )
@@ -265,7 +260,7 @@ async def test_tracked_wireless_clients_event_source(
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -292,7 +287,7 @@ async def test_tracked_wireless_clients_event_source(
     }
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_EVENT},
+            "meta": {"message": MessageKey.EVENT.value},
             "data": [event],
         }
     )
@@ -357,14 +352,14 @@ async def test_tracked_devices(
     device_1["next_interval"] = 20
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_DEVICE},
+            "meta": {"message": MessageKey.DEVICE.value},
             "data": [device_1],
         }
     )
     device_2["next_interval"] = 50
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_DEVICE},
+            "meta": {"message": MessageKey.DEVICE.value},
             "data": [device_2],
         }
     )
@@ -388,7 +383,7 @@ async def test_tracked_devices(
     device_1["disabled"] = True
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_DEVICE},
+            "meta": {"message": MessageKey.DEVICE.value},
             "data": [device_1],
         }
     )
@@ -427,7 +422,7 @@ async def test_remove_clients(
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT_REMOVED},
+            "meta": {"message": MessageKey.CLIENT_REMOVED.value},
             "data": [client_1],
         }
     )
@@ -480,14 +475,14 @@ async def test_controller_state_change(
     assert hass.states.get("device_tracker.device").state == STATE_HOME
 
     # Controller unavailable
-    mock_unifi_websocket(state=STATE_DISCONNECTED)
+    mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.client").state == STATE_UNAVAILABLE
     assert hass.states.get("device_tracker.device").state == STATE_UNAVAILABLE
 
     # Controller available
-    mock_unifi_websocket(state=STATE_RUNNING)
+    mock_unifi_websocket(state=WebsocketState.RUNNING)
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
@@ -730,7 +725,7 @@ async def test_option_ssid_filter(
     client["essid"] = "other_ssid"
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -738,7 +733,7 @@ async def test_option_ssid_filter(
     client_on_ssid2["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client_on_ssid2],
         }
     )
@@ -761,13 +756,13 @@ async def test_option_ssid_filter(
     client_on_ssid2["last_seen"] += 1
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client_on_ssid2],
         }
     )
@@ -788,7 +783,7 @@ async def test_option_ssid_filter(
     client_on_ssid2["last_seen"] += 1
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client_on_ssid2],
         }
     )
@@ -801,7 +796,7 @@ async def test_option_ssid_filter(
     client_on_ssid2["last_seen"] += 1
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client_on_ssid2],
         }
     )
@@ -850,7 +845,7 @@ async def test_wireless_client_go_wired_issue(
     client["is_wired"] = True
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -876,7 +871,7 @@ async def test_wireless_client_go_wired_issue(
     client["last_seen"] += 1
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -892,7 +887,7 @@ async def test_wireless_client_go_wired_issue(
     client["is_wired"] = False
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -936,7 +931,7 @@ async def test_option_ignore_wired_bug(
     client["is_wired"] = True
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -962,7 +957,7 @@ async def test_option_ignore_wired_bug(
     client["last_seen"] += 1
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
@@ -978,7 +973,7 @@ async def test_option_ignore_wired_bug(
     client["is_wired"] = False
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [client],
         }
     )
diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py
index 398c6c6c3f5..0bf03b10029 100644
--- a/tests/components/unifi/test_sensor.py
+++ b/tests/components/unifi/test_sensor.py
@@ -3,7 +3,7 @@
 from datetime import datetime
 from unittest.mock import patch
 
-from aiounifi.controller import MESSAGE_CLIENT, MESSAGE_CLIENT_REMOVED
+from aiounifi.models.message import MessageKey
 import pytest
 
 from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
@@ -89,7 +89,7 @@ async def test_bandwidth_sensors(hass, aioclient_mock, mock_unifi_websocket):
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT},
+            "meta": {"message": MessageKey.CLIENT.value},
             "data": [wireless_client],
         }
     )
@@ -201,7 +201,7 @@ async def test_uptime_sensors(
     with patch("homeassistant.util.dt.now", return_value=now):
         mock_unifi_websocket(
             data={
-                "meta": {"message": MESSAGE_CLIENT},
+                "meta": {"message": MessageKey.CLIENT.value},
                 "data": [uptime_client],
             }
         )
@@ -217,7 +217,7 @@ async def test_uptime_sensors(
     with patch("homeassistant.util.dt.now", return_value=now):
         mock_unifi_websocket(
             data={
-                "meta": {"message": MESSAGE_CLIENT},
+                "meta": {"message": MessageKey.CLIENT.value},
                 "data": [uptime_client],
             }
         )
@@ -310,7 +310,7 @@ async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket):
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT_REMOVED},
+            "meta": {"message": MessageKey.CLIENT_REMOVED.value},
             "data": [wired_client],
         }
     )
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index 41494114c1e..e3ce7859e03 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -3,7 +3,6 @@
 from copy import deepcopy
 from datetime import timedelta
 
-from aiounifi.controller import MESSAGE_CLIENT_REMOVED, MESSAGE_DEVICE, MESSAGE_EVENT
 from aiounifi.models.message import MessageKey
 from aiounifi.websocket import WebsocketState
 
@@ -745,7 +744,7 @@ async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_CLIENT_REMOVED},
+            "meta": {"message": MessageKey.CLIENT_REMOVED.value},
             "data": [CLIENT_1, UNBLOCKED],
         }
     )
@@ -792,7 +791,7 @@ async def test_block_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_EVENT},
+            "meta": {"message": MessageKey.EVENT.value},
             "data": [EVENT_BLOCKED_CLIENT_UNBLOCKED],
         }
     )
@@ -805,7 +804,7 @@ async def test_block_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_EVENT},
+            "meta": {"message": MessageKey.EVENT.value},
             "data": [EVENT_BLOCKED_CLIENT_BLOCKED],
         }
     )
@@ -960,7 +959,7 @@ async def test_outlet_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_DEVICE},
+            "meta": {"message": MessageKey.DEVICE.value},
             "data": [outlet_up1],
         }
     )
@@ -1048,7 +1047,7 @@ async def test_new_client_discovered_on_block_control(
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_EVENT},
+            "meta": {"message": MessageKey.EVENT.value},
             "data": [EVENT_BLOCKED_CLIENT_CONNECTED],
         }
     )
@@ -1154,7 +1153,7 @@ async def test_new_client_discovered_on_poe_control(
 
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_EVENT},
+            "meta": {"message": MessageKey.EVENT.value},
             "data": [EVENT_CLIENT_2_CONNECTED],
         }
     )
diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py
index 677491319a1..9f212b5e065 100644
--- a/tests/components/unifi/test_update.py
+++ b/tests/components/unifi/test_update.py
@@ -1,8 +1,8 @@
 """The tests for the UniFi Network update platform."""
 from copy import deepcopy
 
-from aiounifi.controller import MESSAGE_DEVICE
-from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
+from aiounifi.models.message import MessageKey
+from aiounifi.websocket import WebsocketState
 from yarl import URL
 
 from homeassistant.components.unifi.const import CONF_SITE_ID
@@ -104,7 +104,7 @@ async def test_device_updates(
     device_1["state"] = 4
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_DEVICE},
+            "meta": {"message": MessageKey.DEVICE.value},
             "data": [device_1],
         }
     )
@@ -124,7 +124,7 @@ async def test_device_updates(
     del device_1["upgrade_to_firmware"]
     mock_unifi_websocket(
         data={
-            "meta": {"message": MESSAGE_DEVICE},
+            "meta": {"message": MessageKey.DEVICE.value},
             "data": [device_1],
         }
     )
@@ -188,9 +188,7 @@ async def test_install(hass, aioclient_mock):
     )
 
 
-async def test_controller_state_change(
-    hass, aioclient_mock, mock_unifi_websocket, mock_device_registry
-):
+async def test_controller_state_change(hass, aioclient_mock, mock_unifi_websocket):
     """Verify entities state reflect on controller becoming unavailable."""
     await setup_unifi_integration(
         hass,
@@ -202,13 +200,13 @@ async def test_controller_state_change(
     assert hass.states.get("update.device_1").state == STATE_ON
 
     # Controller unavailable
-    mock_unifi_websocket(state=STATE_DISCONNECTED)
+    mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
     await hass.async_block_till_done()
 
     assert hass.states.get("update.device_1").state == STATE_UNAVAILABLE
 
     # Controller available
-    mock_unifi_websocket(state=STATE_RUNNING)
+    mock_unifi_websocket(state=WebsocketState.RUNNING)
     await hass.async_block_till_done()
 
     assert hass.states.get("update.device_1").state == STATE_ON
-- 
GitLab


From 2c36ee412a9118afb6db557508a3621316d99386 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Wed, 19 Oct 2022 16:52:19 -0500
Subject: [PATCH 615/985] Bump dbus-fast to 1.47.0 (#80633)

---
 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 c11256a3657..e3c54f5631b 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -10,7 +10,7 @@
     "bleak-retry-connector==2.3.1",
     "bluetooth-adapters==0.6.0",
     "bluetooth-auto-recovery==0.3.4",
-    "dbus-fast==1.45.0"
+    "dbus-fast==1.47.0"
   ],
   "codeowners": ["@bdraco"],
   "config_flow": true,
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index a88d6918303..14c2c13c9a8 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.45.0
+dbus-fast==1.47.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 792750f96e6..d6e647f4ba5 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.45.0
+dbus-fast==1.47.0
 
 # homeassistant.components.debugpy
 debugpy==1.6.3
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 75bd9a50505..d443caf9e19 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.45.0
+dbus-fast==1.47.0
 
 # homeassistant.components.debugpy
 debugpy==1.6.3
-- 
GitLab


From 265e2741e721596163fad9f56079b1db29b942d4 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Thu, 20 Oct 2022 00:32:39 +0000
Subject: [PATCH 616/985] [ci skip] Translation update

---
 .../components/coinbase/translations/hu.json  |  6 ++++
 .../components/coinbase/translations/it.json  |  6 ++++
 .../devolo_home_network/translations/it.json  |  8 +++++-
 .../devolo_home_network/translations/nl.json  |  8 +++++-
 .../google_travel_time/translations/hu.json   |  3 +-
 .../google_travel_time/translations/it.json   |  3 +-
 .../google_travel_time/translations/nl.json   |  3 +-
 .../huawei_lte/translations/nl.json           | 10 ++++++-
 .../components/lametric/translations/hu.json  |  2 ++
 .../components/lametric/translations/it.json  |  2 ++
 .../components/lametric/translations/nl.json  |  1 +
 .../lametric/translations/select.it.json      |  8 ++++++
 .../lametric/translations/select.nl.json      |  8 ++++++
 .../lutron_caseta/translations/hu.json        |  3 ++
 .../lutron_caseta/translations/it.json        |  3 ++
 .../lutron_caseta/translations/nl.json        |  3 ++
 .../openexchangerates/translations/it.json    |  4 +--
 .../components/overkiz/translations/it.json   |  3 +-
 .../plugwise/translations/select.en.json      | 28 +++++++++----------
 .../plugwise/translations/select.nl.json      |  7 +++++
 .../components/snooz/translations/it.json     | 27 ++++++++++++++++++
 .../components/snooz/translations/nl.json     | 21 ++++++++++++++
 .../components/zwave_js/translations/hu.json  |  3 +-
 .../components/zwave_js/translations/it.json  |  3 +-
 24 files changed, 148 insertions(+), 25 deletions(-)
 create mode 100644 homeassistant/components/lametric/translations/select.it.json
 create mode 100644 homeassistant/components/lametric/translations/select.nl.json
 create mode 100644 homeassistant/components/plugwise/translations/select.nl.json
 create mode 100644 homeassistant/components/snooz/translations/it.json
 create mode 100644 homeassistant/components/snooz/translations/nl.json

diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json
index 54122d29966..3eee97475f3 100644
--- a/homeassistant/components/coinbase/translations/hu.json
+++ b/homeassistant/components/coinbase/translations/hu.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "A Coinbase 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\u00f3j\u00e1t 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 Coinbase YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "A k\u00e9rt valutaegyenlegek k\u00f6z\u00fcl egyet vagy t\u00f6bbet nem biztos\u00edt a Coinbase API.",
diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json
index f26e08a727c..06507e2e71c 100644
--- a/homeassistant/components/coinbase/translations/it.json
+++ b/homeassistant/components/coinbase/translations/it.json
@@ -21,6 +21,12 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "description": "La configurazione di Coinbase tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.",
+            "title": "La configurazione YAML di Coinbase \u00e8 stata rimossa"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Uno o pi\u00f9 dei saldi in valuta richiesti non sono forniti dalla tua API Coinbase.",
diff --git a/homeassistant/components/devolo_home_network/translations/it.json b/homeassistant/components/devolo_home_network/translations/it.json
index 118ad0e79c6..9494d34f01a 100644
--- a/homeassistant/components/devolo_home_network/translations/it.json
+++ b/homeassistant/components/devolo_home_network/translations/it.json
@@ -2,7 +2,8 @@
     "config": {
         "abort": {
             "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
-            "home_control": "L'unit\u00e0 centrale devolo Home Control non funziona con questa integrazione."
+            "home_control": "L'unit\u00e0 centrale devolo Home Control non funziona con questa integrazione.",
+            "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
         },
         "error": {
             "cannot_connect": "Impossibile connettersi",
@@ -10,6 +11,11 @@
         },
         "flow_title": "{product} ({name})",
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "Password"
+                }
+            },
             "user": {
                 "data": {
                     "ip_address": "Indirizzo IP"
diff --git a/homeassistant/components/devolo_home_network/translations/nl.json b/homeassistant/components/devolo_home_network/translations/nl.json
index af3d6e55365..e531c85c891 100644
--- a/homeassistant/components/devolo_home_network/translations/nl.json
+++ b/homeassistant/components/devolo_home_network/translations/nl.json
@@ -2,7 +2,8 @@
     "config": {
         "abort": {
             "already_configured": "Apparaat is al geconfigureerd",
-            "home_control": "De devolo Home Control Centrale Unit werkt niet met deze integratie."
+            "home_control": "De devolo Home Control Centrale Unit werkt niet met deze integratie.",
+            "reauth_successful": "Herauthenticatie geslaagd"
         },
         "error": {
             "cannot_connect": "Kan geen verbinding maken",
@@ -10,6 +11,11 @@
         },
         "flow_title": "{product} ({name})",
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "Wachtwoord"
+                }
+            },
             "user": {
                 "data": {
                     "ip_address": "IP-adres"
diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json
index 03cfdc18b59..3ad294f4fab 100644
--- a/homeassistant/components/google_travel_time/translations/hu.json
+++ b/homeassistant/components/google_travel_time/translations/hu.json
@@ -4,7 +4,8 @@
             "already_configured": "A hely m\u00e1r konfigur\u00e1lva van"
         },
         "error": {
-            "cannot_connect": "Csatlakoz\u00e1si hiba"
+            "cannot_connect": "Csatlakoz\u00e1si hiba",
+            "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json
index 484bed6099e..16dc9f85f66 100644
--- a/homeassistant/components/google_travel_time/translations/it.json
+++ b/homeassistant/components/google_travel_time/translations/it.json
@@ -4,7 +4,8 @@
             "already_configured": "La posizione \u00e8 gi\u00e0 configurata"
         },
         "error": {
-            "cannot_connect": "Impossibile connettersi"
+            "cannot_connect": "Impossibile connettersi",
+            "invalid_auth": "Autenticazione non valida"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/google_travel_time/translations/nl.json b/homeassistant/components/google_travel_time/translations/nl.json
index bb088c97ca5..464042702dc 100644
--- a/homeassistant/components/google_travel_time/translations/nl.json
+++ b/homeassistant/components/google_travel_time/translations/nl.json
@@ -4,7 +4,8 @@
             "already_configured": "Locatie is al geconfigureerd"
         },
         "error": {
-            "cannot_connect": "Kan geen verbinding maken"
+            "cannot_connect": "Kan geen verbinding maken",
+            "invalid_auth": "Ongeldige authenticatie"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json
index 3e5148170a7..6fa91431fd0 100644
--- a/homeassistant/components/huawei_lte/translations/nl.json
+++ b/homeassistant/components/huawei_lte/translations/nl.json
@@ -1,7 +1,8 @@
 {
     "config": {
         "abort": {
-            "not_huawei_lte": "Geen Huawei LTE-apparaat"
+            "not_huawei_lte": "Geen Huawei LTE-apparaat",
+            "reauth_successful": "Herauthenticatie geslaagd"
         },
         "error": {
             "connection_timeout": "Time-out van de verbinding",
@@ -15,6 +16,13 @@
         },
         "flow_title": "{name}",
         "step": {
+            "reauth_confirm": {
+                "data": {
+                    "password": "Wachtwoord",
+                    "username": "Gebruikersnaam"
+                },
+                "title": "Integratie herauthenticeren"
+            },
             "user": {
                 "data": {
                     "password": "Wachtwoord",
diff --git a/homeassistant/components/lametric/translations/hu.json b/homeassistant/components/lametric/translations/hu.json
index 0e326d4b4e8..748f4bb59be 100644
--- a/homeassistant/components/lametric/translations/hu.json
+++ b/homeassistant/components/lametric/translations/hu.json
@@ -8,6 +8,8 @@
             "missing_configuration": "A LaMetric integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.",
             "no_devices": "A jogosult felhaszn\u00e1l\u00f3 nem rendelkezik LaMetric-eszk\u00f6z\u00f6kkel",
             "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.",
+            "reauth_device_not_found": "Az \u00fajb\u00f3l hiteles\u00edteni k\u00edv\u00e1nt eszk\u00f6z nem tal\u00e1lhat\u00f3 ebben a LaMetric-fi\u00f3kban",
+            "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.",
             "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/it.json b/homeassistant/components/lametric/translations/it.json
index c3496022701..c6b8f6c84df 100644
--- a/homeassistant/components/lametric/translations/it.json
+++ b/homeassistant/components/lametric/translations/it.json
@@ -8,6 +8,8 @@
             "missing_configuration": "L'integrazione LaMetric non \u00e8 configurata. Segui la documentazione.",
             "no_devices": "L'utente autorizzato non dispone di dispositivi LaMetric",
             "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
+            "reauth_device_not_found": "Il dispositivo che stai tentando di riautenticare non \u00e8 stato trovato in questo account LaMetric",
+            "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
             "unknown": "Errore imprevisto"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json
index 6907ef1fb33..addce23b5c9 100644
--- a/homeassistant/components/lametric/translations/nl.json
+++ b/homeassistant/components/lametric/translations/nl.json
@@ -4,6 +4,7 @@
             "already_configured": "Apparaat is al geconfigureerd",
             "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.",
             "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})",
+            "reauth_successful": "Herauthenticatie geslaagd",
             "unknown": "Onverwachte fout"
         },
         "error": {
diff --git a/homeassistant/components/lametric/translations/select.it.json b/homeassistant/components/lametric/translations/select.it.json
new file mode 100644
index 00000000000..cee7cb7796d
--- /dev/null
+++ b/homeassistant/components/lametric/translations/select.it.json
@@ -0,0 +1,8 @@
+{
+    "state": {
+        "lametric__brightness_mode": {
+            "auto": "Automatico",
+            "manual": "Manuale"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lametric/translations/select.nl.json b/homeassistant/components/lametric/translations/select.nl.json
new file mode 100644
index 00000000000..7cbf1a89a90
--- /dev/null
+++ b/homeassistant/components/lametric/translations/select.nl.json
@@ -0,0 +1,8 @@
+{
+    "state": {
+        "lametric__brightness_mode": {
+            "auto": "Automatisch",
+            "manual": "Handmatig"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json
index 1cbca645b6c..5796d1b2816 100644
--- a/homeassistant/components/lutron_caseta/translations/hu.json
+++ b/homeassistant/components/lutron_caseta/translations/hu.json
@@ -33,6 +33,9 @@
             "button_2": "M\u00e1sodik gomb",
             "button_3": "Harmadik gomb",
             "button_4": "Negyedik gomb",
+            "button_5": "\u00d6t\u00f6dik gomb",
+            "button_6": "Hatodik gomb",
+            "button_7": "Hetedik gomb",
             "close_1": "Bez\u00e1r\u00e1s 1.",
             "close_2": "Bez\u00e1r\u00e1s 2.",
             "close_3": "Bez\u00e1r\u00e1s 3.",
diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json
index 8b412cfa40f..c013d08b575 100644
--- a/homeassistant/components/lutron_caseta/translations/it.json
+++ b/homeassistant/components/lutron_caseta/translations/it.json
@@ -33,6 +33,9 @@
             "button_2": "Secondo pulsante",
             "button_3": "Terzo pulsante",
             "button_4": "Quarto pulsante",
+            "button_5": "Quinto pulsante",
+            "button_6": "Sesto pulsante",
+            "button_7": "Settimo pulsante",
             "close_1": "Chiudi 1",
             "close_2": "Chiudi 2",
             "close_3": "Chiudi 3",
diff --git a/homeassistant/components/lutron_caseta/translations/nl.json b/homeassistant/components/lutron_caseta/translations/nl.json
index 0d1063eee8c..8f50e2ddd69 100644
--- a/homeassistant/components/lutron_caseta/translations/nl.json
+++ b/homeassistant/components/lutron_caseta/translations/nl.json
@@ -33,6 +33,9 @@
             "button_2": "Tweede knop",
             "button_3": "Derde knop",
             "button_4": "Vierde knop",
+            "button_5": "Vijfde knop",
+            "button_6": "Zesde knop",
+            "button_7": "Zevende knop",
             "close_1": "Sluit 1",
             "close_2": "Sluit 2",
             "close_3": "Sluit 3",
diff --git a/homeassistant/components/openexchangerates/translations/it.json b/homeassistant/components/openexchangerates/translations/it.json
index 38fca960f50..491a44e7508 100644
--- a/homeassistant/components/openexchangerates/translations/it.json
+++ b/homeassistant/components/openexchangerates/translations/it.json
@@ -26,8 +26,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "La configurazione di Open Exchange Rates tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.",
-            "title": "La configurazione YAML di Open Exchange Rates sar\u00e0 rimossa"
+            "description": "La configurazione di Open Exchange Rates tramite YAML \u00e8 stata rimossa. \n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.",
+            "title": "La configurazione YAML di Open Exchange Rates \u00e8 stata rimossa"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/overkiz/translations/it.json b/homeassistant/components/overkiz/translations/it.json
index 21602a4cb89..49cc4b9a567 100644
--- a/homeassistant/components/overkiz/translations/it.json
+++ b/homeassistant/components/overkiz/translations/it.json
@@ -12,7 +12,8 @@
             "too_many_attempts": "Troppi tentativi con un token non valido, temporaneamente bandito",
             "too_many_requests": "Troppe richieste, riprova pi\u00f9 tardi.",
             "unknown": "Errore imprevisto",
-            "unknown_user": "Utente sconosciuto. Gli account Somfy Protect non sono supportati da questa integrazione."
+            "unknown_user": "Utente sconosciuto. Gli account Somfy Protect non sono supportati da questa integrazione.",
+            "unsupported_hardware": "L'hardware {unsupported_device} non \u00e8 supportato da questa integrazione."
         },
         "flow_title": "Gateway: {gateway_id}",
         "step": {
diff --git a/homeassistant/components/plugwise/translations/select.en.json b/homeassistant/components/plugwise/translations/select.en.json
index c54a933921e..150222c2878 100644
--- a/homeassistant/components/plugwise/translations/select.en.json
+++ b/homeassistant/components/plugwise/translations/select.en.json
@@ -1,17 +1,17 @@
 {
     "state": {
-      "plugwise__regulation_mode": {
-        "bleeding_cold": "Bleeding cold",
-        "bleeding_hot": "Bleeding hot",
-        "cooling": "Cooling",
-        "heating": "Heating",
-        "off": "Off"
-      },
-      "plugwise__dhw_mode": {
-        "off": "Off",
-        "auto": "Auto",
-        "boost": "Boost",
-        "comfort": "Comfort"
-      }
+        "plugwise__dhw_mode": {
+            "auto": "Auto",
+            "boost": "Boost",
+            "comfort": "Comfort",
+            "off": "Off"
+        },
+        "plugwise__regulation_mode": {
+            "bleeding_cold": "Bleeding cold",
+            "bleeding_hot": "Bleeding hot",
+            "cooling": "Cooling",
+            "heating": "Heating",
+            "off": "Off"
+        }
     }
-  }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/select.nl.json b/homeassistant/components/plugwise/translations/select.nl.json
new file mode 100644
index 00000000000..6e4ce4119fd
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/select.nl.json
@@ -0,0 +1,7 @@
+{
+    "state": {
+        "plugwise__regulation_mode": {
+            "off": "Uit"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/snooz/translations/it.json b/homeassistant/components/snooz/translations/it.json
new file mode 100644
index 00000000000..7f3dc5b8191
--- /dev/null
+++ b/homeassistant/components/snooz/translations/it.json
@@ -0,0 +1,27 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+            "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+            "no_devices_found": "Nessun dispositivo trovato sulla rete"
+        },
+        "flow_title": "{name}",
+        "progress": {
+            "wait_for_pairing_mode": "Per completare la configurazione, metti questo dispositivo in modalit\u00e0 di associazione. \n\n ### Come accedere alla modalit\u00e0 di associazione\n 1. Uscita forzata dalle app mobili di SNOOZ.\n 2. Tenere premuto il pulsante di accensione sul dispositivo. Rilasciare quando le luci iniziano a lampeggiare (circa 5 secondi)."
+        },
+        "step": {
+            "bluetooth_confirm": {
+                "description": "Vuoi configurare {name}?"
+            },
+            "pairing_timeout": {
+                "description": "Il dispositivo non \u00e8 entrato in modalit\u00e0 di associazione. Fai clic su Invia per riprovare.\n\n### Risoluzione dei problemi\n1. Verifica che il dispositivo non sia connesso all'app mobile.\n2. Scollegare il dispositivo per 5 secondi, quindi ricollegarlo."
+            },
+            "user": {
+                "data": {
+                    "address": "Dispositivo"
+                },
+                "description": "Seleziona un dispositivo da configurare"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/snooz/translations/nl.json b/homeassistant/components/snooz/translations/nl.json
new file mode 100644
index 00000000000..a46f954fe5f
--- /dev/null
+++ b/homeassistant/components/snooz/translations/nl.json
@@ -0,0 +1,21 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Apparaat is al geconfigureerd",
+            "already_in_progress": "De configuratie is momenteel al bezig",
+            "no_devices_found": "Geen apparaten gevonden op het netwerk"
+        },
+        "flow_title": "{name}",
+        "step": {
+            "bluetooth_confirm": {
+                "description": "Wilt u {name} instellen?"
+            },
+            "user": {
+                "data": {
+                    "address": "Apparaat"
+                },
+                "description": "Kies een apparaat om in te stellen"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json
index 5bf50719b49..b1e65cc05e2 100644
--- a/homeassistant/components/zwave_js/translations/hu.json
+++ b/homeassistant/components/zwave_js/translations/hu.json
@@ -10,7 +10,8 @@
             "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve",
             "cannot_connect": "Sikertelen csatlakoz\u00e1s",
             "discovery_requires_supervisor": "A felfedez\u00e9shez a fel\u00fcgyel\u0151re van sz\u00fcks\u00e9g.",
-            "not_zwave_device": "A felfedezett eszk\u00f6z nem Z-Wave eszk\u00f6z."
+            "not_zwave_device": "A felfedezett eszk\u00f6z nem Z-Wave eszk\u00f6z.",
+            "not_zwave_js_addon": "A felfedezett b\u0151v\u00edtm\u00e9ny nem a hivatalos Z-Wave JS b\u0151v\u00edtm\u00e9ny."
         },
         "error": {
             "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t.",
diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json
index 34977cdd48c..d104f510eaf 100644
--- a/homeassistant/components/zwave_js/translations/it.json
+++ b/homeassistant/components/zwave_js/translations/it.json
@@ -10,7 +10,8 @@
             "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
             "cannot_connect": "Impossibile connettersi",
             "discovery_requires_supervisor": "Il rilevamento richiede il Supervisor.",
-            "not_zwave_device": "Il dispositivo rilevato non \u00e8 un dispositivo Z-Wave."
+            "not_zwave_device": "Il dispositivo rilevato non \u00e8 un dispositivo Z-Wave.",
+            "not_zwave_js_addon": "Il componente aggiuntivo rilevato non \u00e8 il componente aggiuntivo Z-Wave JS ufficiale."
         },
         "error": {
             "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS. Controlla la configurazione.",
-- 
GitLab


From ce358129371d1abedc1eda7874c324c162c41aa3 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
Date: Wed, 19 Oct 2022 20:37:18 -0500
Subject: [PATCH 617/985] Address jellyfin sensor feedback (#80222)

---
 homeassistant/components/jellyfin/coordinator.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py
index 6bf913747ab..9bcf3cf733d 100644
--- a/homeassistant/components/jellyfin/coordinator.py
+++ b/homeassistant/components/jellyfin/coordinator.py
@@ -52,7 +52,6 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT]):
     @abstractmethod
     async def _fetch_data(self) -> JellyfinDataT:
         """Fetch the actual data."""
-        raise NotImplementedError
 
 
 class SessionsDataUpdateCoordinator(
@@ -60,10 +59,14 @@ class SessionsDataUpdateCoordinator(
 ):
     """Sessions update coordinator for Jellyfin."""
 
-    async def _fetch_data(self) -> dict:
+    async def _fetch_data(self) -> dict[str, dict[str, Any]]:
         """Fetch the data."""
         sessions = await self.hass.async_add_executor_job(
             self.api_client.jellyfin.sessions
         )
 
-        return {session["Id"]: session for session in sessions}
+        sessions_by_id: dict[str, Any] = {
+            session["Id"]: session for session in sessions
+        }
+
+        return sessions_by_id
-- 
GitLab


From 540d959dd286a2531c2dd8e666dbc93afa3be8c0 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 20 Oct 2022 00:13:41 -0500
Subject: [PATCH 618/985] Bump bluetooth-auto-recovery to 0.3.6 (#80643)

* Bump bluetooth-auto-recovery to 0.3.5

changelog: https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/compare/v0.3.4...v0.3.5

* bump again for more rfkill fixes
---
 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 e3c54f5631b..6c4b6f7dc6c 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -9,7 +9,7 @@
     "bleak==0.19.0",
     "bleak-retry-connector==2.3.1",
     "bluetooth-adapters==0.6.0",
-    "bluetooth-auto-recovery==0.3.4",
+    "bluetooth-auto-recovery==0.3.6",
     "dbus-fast==1.47.0"
   ],
   "codeowners": ["@bdraco"],
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 14c2c13c9a8..f1e674bbd10 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -13,7 +13,7 @@ bcrypt==3.1.7
 bleak-retry-connector==2.3.1
 bleak==0.19.0
 bluetooth-adapters==0.6.0
-bluetooth-auto-recovery==0.3.4
+bluetooth-auto-recovery==0.3.6
 certifi>=2021.5.30
 ciso8601==2.2.0
 cryptography==38.0.1
diff --git a/requirements_all.txt b/requirements_all.txt
index d6e647f4ba5..0309e8ca2ad 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.4
+bluetooth-auto-recovery==0.3.6
 
 # homeassistant.components.bond
 bond-async==0.1.22
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index d443caf9e19..49ff423b361 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.4
+bluetooth-auto-recovery==0.3.6
 
 # homeassistant.components.bond
 bond-async==0.1.22
-- 
GitLab


From d78c2a31a1eb4e945a5fa64a38a11ba7569e41ff Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Thu, 20 Oct 2022 08:59:06 +0200
Subject: [PATCH 619/985] Update pylint to 2.15.4 (#80612)

---
 homeassistant/components/forked_daapd/browse_media.py         | 2 +-
 homeassistant/components/recorder/pool.py                     | 1 +
 homeassistant/components/repairs/issue_handler.py             | 2 --
 .../components/zha/core/channels/manufacturerspecific.py      | 2 +-
 homeassistant/helpers/recorder.py                             | 1 -
 requirements_test.txt                                         | 4 ++--
 6 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py
index 099a042f58a..a4c97d3a035 100644
--- a/homeassistant/components/forked_daapd/browse_media.py
+++ b/homeassistant/components/forked_daapd/browse_media.py
@@ -172,7 +172,7 @@ async def get_owntone_content(
             result = await master.api.get_artists()  # list of artists with name, uri
         elif media_content.type == MediaType.GENRE:
             if result := await master.api.get_genres():  # returns list of genre names
-                for item in result:  # pylint: disable=not-an-iterable
+                for item in result:
                     # add generated genre uris to list of genre names
                     item["uri"] = create_owntone_uri(
                         MediaType.GENRE, cast(str, item["name"])
diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py
index a8579df834c..35a8623a1c1 100644
--- a/homeassistant/components/recorder/pool.py
+++ b/homeassistant/components/recorder/pool.py
@@ -128,6 +128,7 @@ class MutexPool(StaticPool):  # type: ignore[misc]
 
         if DEBUG_MUTEX_POOL:
             _LOGGER.debug("%s wait conn%s", threading.current_thread().name, trace_msg)
+        # pylint: disable-next=consider-using-with
         got_lock = MutexPool.pool_lock.acquire(timeout=1)
         if not got_lock:
             raise SQLAlchemyError
diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py
index 35a8d9cc49d..9c5e3854773 100644
--- a/homeassistant/components/repairs/issue_handler.py
+++ b/homeassistant/components/repairs/issue_handler.py
@@ -11,8 +11,6 @@ from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.integration_platform import (
     async_process_integration_platforms,
 )
-
-# pylint: disable-next=unused-import
 from homeassistant.helpers.issue_registry import (
     async_delete_issue,
     async_get as async_get_issue_registry,
diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py
index 724a794007d..febe589dffa 100644
--- a/homeassistant/components/zha/core/channels/manufacturerspecific.py
+++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py
@@ -158,7 +158,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
         Clear = 0xFF
 
     REPORT_CONFIG = ()
-    ZCL_INIT_ATTRS = {  # pylint: disable=invalid-name
+    ZCL_INIT_ATTRS = {
         "dimming_speed_up_remote": False,
         "dimming_speed_up_local": False,
         "ramp_rate_off_to_on_local": False,
diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py
index 32e08874a63..5545aa09f01 100644
--- a/homeassistant/helpers/recorder.py
+++ b/homeassistant/helpers/recorder.py
@@ -38,7 +38,6 @@ async def async_wait_recorder(hass: HomeAssistant) -> bool:
 
     Returns False immediately if the recorder is not enabled.
     """
-    # pylint: disable-next=import-outside-toplevel
     if DOMAIN not in hass.data:
         return False
     db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected
diff --git a/requirements_test.txt b/requirements_test.txt
index 82432d85072..2cbe45ace49 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -7,14 +7,14 @@
 
 -c homeassistant/package_constraints.txt
 -r requirements_test_pre_commit.txt
-astroid==2.12.5
+astroid==2.12.12
 codecov==2.1.12
 coverage==6.4.4
 freezegun==1.2.2
 mock-open==1.4.0
 mypy==0.982
 pre-commit==2.20.0
-pylint==2.15.0
+pylint==2.15.4
 pipdeptree==2.3.1
 pytest-aiohttp==0.3.0
 pytest-cov==3.0.0
-- 
GitLab


From 8a1cc05e0ccb9b6e86f1f43cff1faea3a4e9c20d Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 20 Oct 2022 10:43:32 +0200
Subject: [PATCH 620/985] Enforce kwargs in unit system initialisation (#80620)

* Enforce kwargs in unit system initialisation

* Fix tests

* Sort kwargs in unit_system
---
 homeassistant/util/unit_system.py | 41 ++++++-------
 tests/helpers/test_template.py    | 14 ++---
 tests/util/test_unit_system.py    | 98 +++++++++++++++----------------
 3 files changed, 77 insertions(+), 76 deletions(-)

diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 31fec50c8f7..bb2cd0862e1 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -9,15 +9,15 @@ import voluptuous as vol
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
     LENGTH,
-    LENGTH_INCHES,
     LENGTH_KILOMETERS,
     LENGTH_MILES,
-    LENGTH_MILLIMETERS,
     MASS,
     MASS_GRAMS,
     MASS_KILOGRAMS,
     MASS_OUNCES,
     MASS_POUNDS,
+    PRECIPITATION_INCHES,
+    PRECIPITATION_MILLIMETERS,
     PRESSURE,
     PRESSURE_PA,
     PRESSURE_PSI,
@@ -87,13 +87,14 @@ class UnitSystem:
     def __init__(
         self,
         name: str,
-        temperature: str,
+        *,
+        accumulated_precipitation: str,
         length: str,
-        wind_speed: str,
-        volume: str,
         mass: str,
         pressure: str,
-        accumulated_precipitation: str,
+        temperature: str,
+        volume: str,
+        wind_speed: str,
     ) -> None:
         """Initialize the unit system object."""
         errors: str = ", ".join(
@@ -241,24 +242,24 @@ validate_unit_system = vol.All(
 
 METRIC_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_METRIC,
-    TEMP_CELSIUS,
-    LENGTH_KILOMETERS,
-    SPEED_METERS_PER_SECOND,
-    VOLUME_LITERS,
-    MASS_GRAMS,
-    PRESSURE_PA,
-    LENGTH_MILLIMETERS,
+    accumulated_precipitation=PRECIPITATION_MILLIMETERS,
+    length=LENGTH_KILOMETERS,
+    mass=MASS_GRAMS,
+    pressure=PRESSURE_PA,
+    temperature=TEMP_CELSIUS,
+    volume=VOLUME_LITERS,
+    wind_speed=SPEED_METERS_PER_SECOND,
 )
 
 US_CUSTOMARY_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_US_CUSTOMARY,
-    TEMP_FAHRENHEIT,
-    LENGTH_MILES,
-    SPEED_MILES_PER_HOUR,
-    VOLUME_GALLONS,
-    MASS_POUNDS,
-    PRESSURE_PSI,
-    LENGTH_INCHES,
+    accumulated_precipitation=PRECIPITATION_INCHES,
+    length=LENGTH_MILES,
+    mass=MASS_POUNDS,
+    pressure=PRESSURE_PSI,
+    temperature=TEMP_FAHRENHEIT,
+    volume=VOLUME_GALLONS,
+    wind_speed=SPEED_MILES_PER_HOUR,
 )
 
 IMPERIAL_SYSTEM = US_CUSTOMARY_SYSTEM
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index 265706c9713..7679ddb44fc 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -43,13 +43,13 @@ def _set_up_units(hass):
     """Set up the tests."""
     hass.config.units = UnitSystem(
         "custom",
-        TEMP_CELSIUS,
-        LENGTH_METERS,
-        SPEED_KILOMETERS_PER_HOUR,
-        VOLUME_LITERS,
-        MASS_GRAMS,
-        PRESSURE_PA,
-        LENGTH_MILLIMETERS,
+        temperature=TEMP_CELSIUS,
+        length=LENGTH_METERS,
+        wind_speed=SPEED_KILOMETERS_PER_HOUR,
+        volume=VOLUME_LITERS,
+        mass=MASS_GRAMS,
+        pressure=PRESSURE_PA,
+        accumulated_precipitation=LENGTH_MILLIMETERS,
     )
 
 
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 2e558a7bd5d..4aa62417705 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -39,85 +39,85 @@ def test_invalid_units():
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            INVALID_UNIT,
-            LENGTH_METERS,
-            SPEED_METERS_PER_SECOND,
-            VOLUME_LITERS,
-            MASS_GRAMS,
-            PRESSURE_PA,
-            LENGTH_MILLIMETERS,
+            temperature=INVALID_UNIT,
+            length=LENGTH_METERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
+            volume=VOLUME_LITERS,
+            mass=MASS_GRAMS,
+            pressure=PRESSURE_PA,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            TEMP_CELSIUS,
-            INVALID_UNIT,
-            SPEED_METERS_PER_SECOND,
-            VOLUME_LITERS,
-            MASS_GRAMS,
-            PRESSURE_PA,
-            LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            length=INVALID_UNIT,
+            wind_speed=SPEED_METERS_PER_SECOND,
+            volume=VOLUME_LITERS,
+            mass=MASS_GRAMS,
+            pressure=PRESSURE_PA,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            TEMP_CELSIUS,
-            LENGTH_METERS,
-            INVALID_UNIT,
-            VOLUME_LITERS,
-            MASS_GRAMS,
-            PRESSURE_PA,
-            LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            length=LENGTH_METERS,
+            wind_speed=INVALID_UNIT,
+            volume=VOLUME_LITERS,
+            mass=MASS_GRAMS,
+            pressure=PRESSURE_PA,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            TEMP_CELSIUS,
-            LENGTH_METERS,
-            SPEED_METERS_PER_SECOND,
-            INVALID_UNIT,
-            MASS_GRAMS,
-            PRESSURE_PA,
-            LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            length=LENGTH_METERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
+            volume=INVALID_UNIT,
+            mass=MASS_GRAMS,
+            pressure=PRESSURE_PA,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            TEMP_CELSIUS,
-            LENGTH_METERS,
-            SPEED_METERS_PER_SECOND,
-            VOLUME_LITERS,
-            INVALID_UNIT,
-            PRESSURE_PA,
-            LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            length=LENGTH_METERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
+            volume=VOLUME_LITERS,
+            mass=INVALID_UNIT,
+            pressure=PRESSURE_PA,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            TEMP_CELSIUS,
-            LENGTH_METERS,
-            SPEED_METERS_PER_SECOND,
-            VOLUME_LITERS,
-            MASS_GRAMS,
-            INVALID_UNIT,
-            LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            length=LENGTH_METERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
+            volume=VOLUME_LITERS,
+            mass=MASS_GRAMS,
+            pressure=INVALID_UNIT,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            TEMP_CELSIUS,
-            LENGTH_METERS,
-            SPEED_METERS_PER_SECOND,
-            VOLUME_LITERS,
-            MASS_GRAMS,
-            PRESSURE_PA,
-            INVALID_UNIT,
+            temperature=TEMP_CELSIUS,
+            length=LENGTH_METERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
+            volume=VOLUME_LITERS,
+            mass=MASS_GRAMS,
+            pressure=PRESSURE_PA,
+            accumulated_precipitation=INVALID_UNIT,
         )
 
 
-- 
GitLab


From 41faa017cd6889eec6e48bf290ef7cd956605f26 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
Date: Thu, 20 Oct 2022 04:44:17 -0400
Subject: [PATCH 621/985] Use custom attributes description in Sonarr (#79845)

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
---
 homeassistant/components/sonarr/sensor.py | 101 ++++++++++++----------
 1 file changed, 54 insertions(+), 47 deletions(-)

diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py
index da0e4c5af8c..fe440b01be0 100644
--- a/homeassistant/components/sonarr/sensor.py
+++ b/homeassistant/components/sonarr/sensor.py
@@ -6,6 +6,7 @@ from dataclasses import dataclass
 from typing import Any, Generic
 
 from aiopyarr import (
+    Command,
     Diskspace,
     SonarrCalendar,
     SonarrQueue,
@@ -30,6 +31,7 @@ from .entity import SonarrEntity
 class SonarrSensorEntityDescriptionMixIn(Generic[SonarrDataT]):
     """Mixin for Sonarr sensor."""
 
+    attributes_fn: Callable[[SonarrDataT], dict[str, str]]
     value_fn: Callable[[SonarrDataT], StateType]
 
 
@@ -40,14 +42,52 @@ class SonarrSensorEntityDescription(
     """Class to describe a Sonarr sensor."""
 
 
+def get_disk_space_attr(disks: list[Diskspace]) -> dict[str, str]:
+    """Create the attributes for disk space."""
+    attrs: dict[str, str] = {}
+    for disk in disks:
+        free = disk.freeSpace / 1024**3
+        total = disk.totalSpace / 1024**3
+        usage = free / total * 100
+        attrs[disk.path] = f"{free:.2f}/{total:.2f}{DATA_GIGABYTES} ({usage:.2f}%)"
+    return attrs
+
+
+def get_queue_attr(queue: SonarrQueue) -> dict[str, str]:
+    """Create the attributes for series queue."""
+    attrs: dict[str, str] = {}
+    for item in queue.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}"
+        )
+        attrs[f"{item.series.title} {identifier}"] = f"{remaining_pct:.2f}%"
+    return attrs
+
+
+def get_wanted_attr(wanted: SonarrWantedMissing) -> dict[str, str]:
+    """Create the attributes for missing series."""
+    attrs: dict[str, str] = {}
+    for item in wanted.records:
+        identifier = f"S{item.seasonNumber:02d}E{item.episodeNumber:02d}"
+
+        name = f"{item.series.title} {identifier}"
+        attrs[name] = dt_util.as_local(
+            item.airDateUtc.replace(tzinfo=dt_util.UTC)
+        ).isoformat()
+    return attrs
+
+
 SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = {
-    "commands": SonarrSensorEntityDescription(
+    "commands": SonarrSensorEntityDescription[list[Command]](
         key="commands",
         name="Commands",
         icon="mdi:code-braces",
         native_unit_of_measurement="Commands",
         entity_registry_enabled_default=False,
         value_fn=len,
+        attributes_fn=lambda data: {c.name: c.status for c in data},
     ),
     "diskspace": SonarrSensorEntityDescription[list[Diskspace]](
         key="diskspace",
@@ -56,6 +96,7 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = {
         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}",
+        attributes_fn=get_disk_space_attr,
     ),
     "queue": SonarrSensorEntityDescription[SonarrQueue](
         key="queue",
@@ -64,6 +105,7 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = {
         native_unit_of_measurement="Episodes",
         entity_registry_enabled_default=False,
         value_fn=lambda data: data.totalRecords,
+        attributes_fn=get_queue_attr,
     ),
     "series": SonarrSensorEntityDescription[list[SonarrSeries]](
         key="series",
@@ -72,6 +114,10 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = {
         native_unit_of_measurement="Series",
         entity_registry_enabled_default=False,
         value_fn=len,
+        attributes_fn=lambda data: {
+            i.title: f"{getattr(i.statistics,'episodeFileCount', 0)}/{getattr(i.statistics, 'episodeCount', 0)} Episodes"
+            for i in data
+        },
     ),
     "upcoming": SonarrSensorEntityDescription[list[SonarrCalendar]](
         key="upcoming",
@@ -79,6 +125,9 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = {
         icon="mdi:television",
         native_unit_of_measurement="Episodes",
         value_fn=len,
+        attributes_fn=lambda data: {
+            e.series.title: f"S{e.seasonNumber:02d}E{e.episodeNumber:02d}" for e in data
+        },
     ),
     "wanted": SonarrSensorEntityDescription[SonarrWantedMissing](
         key="wanted",
@@ -87,6 +136,7 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = {
         native_unit_of_measurement="Episodes",
         entity_registry_enabled_default=False,
         value_fn=lambda data: data.totalRecords,
+        attributes_fn=get_wanted_attr,
     ),
 }
 
@@ -109,56 +159,13 @@ async def async_setup_entry(
 class SonarrSensor(SonarrEntity[SonarrDataT], SensorEntity):
     """Implementation of the Sonarr sensor."""
 
-    coordinator: SonarrDataUpdateCoordinator
+    coordinator: SonarrDataUpdateCoordinator[SonarrDataT]
     entity_description: SonarrSensorEntityDescription[SonarrDataT]
 
     @property
-    def extra_state_attributes(self) -> dict[str, str] | None:
+    def extra_state_attributes(self) -> dict[str, str]:
         """Return the state attributes of the entity."""
-        attrs = {}
-        key = self.entity_description.key
-        data = self.coordinator.data
-
-        if key == "diskspace":
-            for disk in data:
-                free = disk.freeSpace / 1024**3
-                total = disk.totalSpace / 1024**3
-                usage = free / total * 100
-
-                attrs[
-                    disk.path
-                ] = f"{free:.2f}/{total:.2f}{self.unit_of_measurement} ({usage:.2f}%)"
-        elif key == "commands":
-            for command in data:
-                attrs[command.name] = command.status
-        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":
-            for item in data:
-                stats = item.statistics
-                attrs[
-                    item.title
-                ] = f"{getattr(stats,'episodeFileCount', 0)}/{getattr(stats, 'episodeCount', 0)} Episodes"
-        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":
-            for item in data.records:
-                identifier = f"S{item.seasonNumber:02d}E{item.episodeNumber:02d}"
-
-                name = f"{item.series.title} {identifier}"
-                attrs[name] = dt_util.as_local(
-                    item.airDateUtc.replace(tzinfo=dt_util.UTC)
-                ).isoformat()
-
-        return attrs
+        return self.entity_description.attributes_fn(self.coordinator.data)
 
     @property
     def native_value(self) -> StateType:
-- 
GitLab


From cce4485fb70be11f52cf64734f6df6c5fb9598f6 Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Thu, 20 Oct 2022 11:22:30 +0200
Subject: [PATCH 622/985] Add missed write state request for MQTT cover
 (#80540)

Missed write state request for MQTT cover
---
 homeassistant/components/mqtt/cover.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py
index 6ed12b8adef..11901f15054 100644
--- a/homeassistant/components/mqtt/cover.py
+++ b/homeassistant/components/mqtt/cover.py
@@ -767,6 +767,7 @@ class MqttCover(MqttEntity, CoverEntity):
 
         return position
 
+    @callback
     def tilt_payload_received(self, _payload):
         """Set the tilt value."""
 
@@ -784,7 +785,7 @@ class MqttCover(MqttEntity, CoverEntity):
         ):
             level = self.find_percentage_in_range(payload)
             self._tilt_value = level
-            self.async_write_ha_state()
+            get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
         else:
             _LOGGER.warning(
                 "Payload '%s' is out of range, must be between '%s' and '%s' inclusive",
-- 
GitLab


From b23a66d776c41cbe484a38ab94dd7db588c3bde6 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 20 Oct 2022 12:20:39 +0200
Subject: [PATCH 623/985] Add websocket type hints in entity_registry (#80657)

* Add websocket type hints in entity_registry

* Adjust websocket_list_entities

* Fix update

* Fix websocket_update_entity

* Apply suggestion

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Apply suggestion

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 .../components/config/entity_registry.py      | 37 ++++++++++++++-----
 .../components/config/test_entity_registry.py |  2 +-
 2 files changed, 29 insertions(+), 10 deletions(-)

diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py
index b024c3f0128..b4bd7403c43 100644
--- a/homeassistant/components/config/entity_registry.py
+++ b/homeassistant/components/config/entity_registry.py
@@ -36,14 +36,18 @@ async def async_setup(hass: HomeAssistant) -> bool:
         {vol.Required("type"): "config/entity_registry/list"}
     )
     @callback
-    def websocket_list_entities(hass, connection, msg):
+    def websocket_list_entities(
+        hass: HomeAssistant,
+        connection: websocket_api.ActiveConnection,
+        msg: dict[str, Any],
+    ) -> None:
         """Handle list registry entries command."""
         nonlocal cached_list_entities
         if not cached_list_entities:
             registry = er.async_get(hass)
             cached_list_entities = message_to_json(
                 websocket_api.result_message(
-                    IDEN_TEMPLATE,
+                    IDEN_TEMPLATE,  # type: ignore[arg-type]
                     [_entry_dict(entry) for entry in registry.entities.values()],
                 )
             )
@@ -70,7 +74,11 @@ async def async_setup(hass: HomeAssistant) -> bool:
     }
 )
 @callback
-def websocket_get_entity(hass, connection, msg):
+def websocket_get_entity(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle get entity registry entry command.
 
     Async friendly.
@@ -120,7 +128,11 @@ def websocket_get_entity(hass, connection, msg):
     }
 )
 @callback
-def websocket_update_entity(hass, connection, msg):
+def websocket_update_entity(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle update entity websocket command.
 
     Async friendly.
@@ -153,7 +165,7 @@ def websocket_update_entity(hass, connection, msg):
         if entity_entry.device_id:
             device_registry = dr.async_get(hass)
             device = device_registry.async_get(entity_entry.device_id)
-            if device.disabled:
+            if device and device.disabled:
                 connection.send_message(
                     websocket_api.error_message(
                         msg["id"], "invalid_info", "Device is disabled"
@@ -184,11 +196,14 @@ def websocket_update_entity(hass, connection, msg):
         )
         return
 
-    result = {"entity_entry": _entry_ext_dict(entity_entry)}
+    result: dict[str, Any] = {"entity_entry": _entry_ext_dict(entity_entry)}
     if "disabled_by" in changes and changes["disabled_by"] is None:
         # Enabling an entity requires a config entry reload, or HA restart
-        config_entry = hass.config_entries.async_get_entry(entity_entry.config_entry_id)
-        if config_entry and not config_entry.supports_unload:
+        if (
+            not (config_entry_id := entity_entry.config_entry_id)
+            or (config_entry := hass.config_entries.async_get_entry(config_entry_id))
+            and not config_entry.supports_unload
+        ):
             result["require_restart"] = True
         else:
             result["reload_delay"] = config_entries.RELOAD_AFTER_UPDATE_DELAY
@@ -203,7 +218,11 @@ def websocket_update_entity(hass, connection, msg):
     }
 )
 @callback
-def websocket_remove_entity(hass, connection, msg):
+def websocket_remove_entity(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle remove entity websocket command.
 
     Async friendly.
diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py
index 2f4cd980d8e..ee46838b01c 100644
--- a/tests/components/config/test_entity_registry.py
+++ b/tests/components/config/test_entity_registry.py
@@ -345,7 +345,7 @@ async def test_update_entity(hass, client):
             "platform": "test_platform",
             "unique_id": "1234",
         },
-        "reload_delay": 30,
+        "require_restart": True,
     }
 
     # UPDATE ENTITY OPTION
-- 
GitLab


From 454394a24209367df0b066d1daa7a937628688ed Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
Date: Thu, 20 Oct 2022 12:29:51 +0200
Subject: [PATCH 624/985] Update frontend to 20221020.0 (#80661)

---
 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 98b978964a6..1c8a6465681 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==20221010.0"],
+  "requirements": ["home-assistant-frontend==20221020.0"],
   "dependencies": [
     "api",
     "auth",
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index f1e674bbd10..dddaec8712f 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -21,7 +21,7 @@ dbus-fast==1.47.0
 fnvhash==0.1.0
 hass-nabucasa==0.56.0
 home-assistant-bluetooth==1.6.0
-home-assistant-frontend==20221010.0
+home-assistant-frontend==20221020.0
 httpx==0.23.0
 ifaddr==0.1.7
 jinja2==3.1.2
diff --git a/requirements_all.txt b/requirements_all.txt
index 0309e8ca2ad..4826d0003b0 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==20221010.0
+home-assistant-frontend==20221020.0
 
 # homeassistant.components.home_connect
 homeconnect==0.7.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 49ff423b361..43f57742039 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==20221010.0
+home-assistant-frontend==20221020.0
 
 # homeassistant.components.home_connect
 homeconnect==0.7.2
-- 
GitLab


From eb93372cd67d92dbcd09dfcef63acb301de2dd12 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 20 Oct 2022 12:38:22 +0200
Subject: [PATCH 625/985] Improve type hint in entity_component (#80596)

Imrpove type hint in entity_component
---
 homeassistant/helpers/entity_component.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index 86d5436e287..eea0a9943a3 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -37,6 +37,7 @@ _EntityT = TypeVar("_EntityT", bound=entity.Entity)
 async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None:
     """Trigger an update for an entity."""
     domain = entity_id.split(".", 1)[0]
+    entity_comp: EntityComponent[entity.Entity] | None
     entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain)
 
     if entity_comp is None:
-- 
GitLab


From ae7eb9cef9c718e448d95b94fd4e1f62e3335292 Mon Sep 17 00:00:00 2001
From: Avishay <avishorp@gmail.com>
Date: Thu, 20 Oct 2022 14:15:30 +0300
Subject: [PATCH 626/985] Add mode control for Modbus climate entities (#73906)

* Add support for Modbus HVAC control registers
---
 homeassistant/components/modbus/__init__.py |  19 ++
 homeassistant/components/modbus/climate.py  |  94 ++++++-
 homeassistant/components/modbus/const.py    |   3 +
 tests/components/modbus/test_climate.py     | 257 ++++++++++++++++++--
 4 files changed, 351 insertions(+), 22 deletions(-)

diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py
index 08834177e4e..8aa2903506f 100644
--- a/homeassistant/components/modbus/__init__.py
+++ b/homeassistant/components/modbus/__init__.py
@@ -9,6 +9,7 @@ import voluptuous as vol
 from homeassistant.components.binary_sensor import (
     DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
 )
+from homeassistant.components.climate import HVACMode
 from homeassistant.components.cover import (
     DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA,
 )
@@ -65,6 +66,9 @@ from .const import (  # noqa: F401
     CONF_DATA_TYPE,
     CONF_FANS,
     CONF_HUB,
+    CONF_HVAC_MODE_REGISTER,
+    CONF_HVAC_MODE_VALUES,
+    CONF_HVAC_ONOFF_REGISTER,
     CONF_INPUT_TYPE,
     CONF_LAZY_ERROR,
     CONF_MAX_TEMP,
@@ -218,6 +222,21 @@ CLIMATE_SCHEMA = vol.All(
             vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int,
             vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
             vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
+            vol.Optional(CONF_HVAC_ONOFF_REGISTER): cv.positive_int,
+            vol.Optional(CONF_HVAC_MODE_REGISTER): vol.Maybe(
+                {
+                    CONF_ADDRESS: cv.positive_int,
+                    CONF_HVAC_MODE_VALUES: {
+                        vol.Optional(HVACMode.OFF.value): cv.positive_int,
+                        vol.Optional(HVACMode.HEAT.value): cv.positive_int,
+                        vol.Optional(HVACMode.COOL.value): cv.positive_int,
+                        vol.Optional(HVACMode.HEAT_COOL.value): cv.positive_int,
+                        vol.Optional(HVACMode.AUTO.value): cv.positive_int,
+                        vol.Optional(HVACMode.DRY.value): cv.positive_int,
+                        vol.Optional(HVACMode.FAN_ONLY.value): cv.positive_int,
+                    },
+                }
+            ),
         }
     ),
 )
diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py
index 7d6376a5a42..92efcfb17d5 100644
--- a/homeassistant/components/modbus/climate.py
+++ b/homeassistant/components/modbus/climate.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 
 from datetime import datetime
 import struct
-from typing import Any
+from typing import Any, cast
 
 from homeassistant.components.climate import (
     ClimateEntity,
@@ -12,6 +12,7 @@ from homeassistant.components.climate import (
 )
 from homeassistant.const import (
     ATTR_TEMPERATURE,
+    CONF_ADDRESS,
     CONF_NAME,
     CONF_TEMPERATURE_UNIT,
     PRECISION_TENTHS,
@@ -31,6 +32,9 @@ from .const import (
     CALL_TYPE_WRITE_REGISTER,
     CALL_TYPE_WRITE_REGISTERS,
     CONF_CLIMATES,
+    CONF_HVAC_MODE_REGISTER,
+    CONF_HVAC_MODE_VALUES,
+    CONF_HVAC_ONOFF_REGISTER,
     CONF_MAX_TEMP,
     CONF_MIN_TEMP,
     CONF_STEP,
@@ -63,8 +67,6 @@ async def async_setup_platform(
 class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
     """Representation of a Modbus Thermostat."""
 
-    _attr_hvac_mode = HVACMode.AUTO
-    _attr_hvac_modes = [HVACMode.AUTO]
     _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
 
     def __init__(
@@ -90,6 +92,33 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
         self._attr_target_temperature_step = config[CONF_TARGET_TEMP]
         self._attr_target_temperature_step = config[CONF_STEP]
 
+        if CONF_HVAC_MODE_REGISTER in config:
+            mode_config = config[CONF_HVAC_MODE_REGISTER]
+            self._hvac_mode_register = mode_config[CONF_ADDRESS]
+            self._attr_hvac_modes = cast(list[HVACMode], [])
+            self._attr_hvac_mode = None
+            self._hvac_mode_mapping: list[tuple[int, HVACMode]] = []
+            mode_value_config = mode_config[CONF_HVAC_MODE_VALUES]
+            for hvac_mode in HVACMode:
+                if hvac_mode.value in mode_value_config:
+                    self._hvac_mode_mapping.append(
+                        (mode_value_config[hvac_mode.value], hvac_mode)
+                    )
+                    self._attr_hvac_modes.append(hvac_mode)
+
+        else:
+            # No HVAC modes defined
+            self._hvac_mode_register = None
+            self._attr_hvac_mode = HVACMode.AUTO
+            self._attr_hvac_modes = [HVACMode.AUTO]
+
+        if CONF_HVAC_ONOFF_REGISTER in config:
+            self._hvac_onoff_register = config[CONF_HVAC_ONOFF_REGISTER]
+            if HVACMode.OFF not in self._attr_hvac_modes:
+                self._attr_hvac_modes.append(HVACMode.OFF)
+        else:
+            self._hvac_onoff_register = None
+
     async def async_added_to_hass(self) -> None:
         """Handle entity which will be added."""
         await self.async_base_added_to_hass()
@@ -99,8 +128,28 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
 
     async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
         """Set new target hvac mode."""
-        # Home Assistant expects this method.
-        # We'll keep it here to avoid getting exceptions.
+        if self._hvac_onoff_register is not None:
+            # Turn HVAC Off by writing 0 to the On/Off register, or 1 otherwise.
+            await self._hub.async_pymodbus_call(
+                self._slave,
+                self._hvac_onoff_register,
+                0 if hvac_mode == HVACMode.OFF else 1,
+                CALL_TYPE_WRITE_REGISTER,
+            )
+
+        if self._hvac_mode_register is not None:
+            # Write a value to the mode register for the desired mode.
+            for value, mode in self._hvac_mode_mapping:
+                if mode == hvac_mode:
+                    await self._hub.async_pymodbus_call(
+                        self._slave,
+                        self._hvac_mode_register,
+                        value,
+                        CALL_TYPE_WRITE_REGISTER,
+                    )
+                    break
+
+        await self.async_update()
 
     async def async_set_temperature(self, **kwargs: Any) -> None:
         """Set new target temperature."""
@@ -158,11 +207,36 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
         self._attr_current_temperature = await self._async_read_register(
             self._input_type, self._address
         )
+
+        # Read the mode register if defined
+        if self._hvac_mode_register is not None:
+            hvac_mode = await self._async_read_register(
+                CALL_TYPE_REGISTER_HOLDING, self._hvac_mode_register, raw=True
+            )
+
+            # Translate the value received
+            if hvac_mode is not None:
+                self._attr_hvac_mode = None
+                for value, mode in self._hvac_mode_mapping:
+                    if hvac_mode == value:
+                        self._attr_hvac_mode = mode
+                        break
+
+        # Read th on/off register if defined. If the value in this
+        # register is "OFF", it will take precedence over the value
+        # in the mode register.
+        if self._hvac_onoff_register is not None:
+            onoff = await self._async_read_register(
+                CALL_TYPE_REGISTER_HOLDING, self._hvac_onoff_register, raw=True
+            )
+            if onoff == 0:
+                self._attr_hvac_mode = HVACMode.OFF
+
         self._call_active = False
         self.async_write_ha_state()
 
     async def _async_read_register(
-        self, register_type: str, register: int
+        self, register_type: str, register: int, raw: bool | None = False
     ) -> float | None:
         """Read register using the Modbus hub slave."""
         result = await self._hub.async_pymodbus_call(
@@ -177,6 +251,14 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
             return -1
 
         self._lazy_errors = self._lazy_error_count
+
+        if raw:
+            # Return the raw value read from the register, do not change
+            # the object's state
+            self._attr_available = True
+            return int(result.registers[0])
+
+        # The regular handling of the value
         self._value = self.unpack_structure_result(result.registers)
         if not self._value:
             self._attr_available = False
diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py
index b09d75f27e0..2ad36f908ce 100644
--- a/homeassistant/components/modbus/const.py
+++ b/homeassistant/components/modbus/const.py
@@ -53,6 +53,9 @@ CONF_SWAP_NONE = "none"
 CONF_SWAP_WORD = "word"
 CONF_SWAP_WORD_BYTE = "word_byte"
 CONF_TARGET_TEMP = "target_temp_register"
+CONF_HVAC_MODE_REGISTER = "hvac_mode_register"
+CONF_HVAC_MODE_VALUES = "values"
+CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register"
 CONF_VERIFY = "verify"
 CONF_VERIFY_REGISTER = "verify_register"
 CONF_VERIFY_STATE = "verify_state"
diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py
index 942f6997c21..e554160d5bb 100644
--- a/tests/components/modbus/test_climate.py
+++ b/tests/components/modbus/test_climate.py
@@ -1,10 +1,18 @@
 """The tests for the Modbus climate component."""
 import pytest
 
-from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, HVACMode
+from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
+from homeassistant.components.climate.const import (
+    ATTR_HVAC_MODE,
+    ATTR_HVAC_MODES,
+    HVACMode,
+)
 from homeassistant.components.modbus.const import (
     CONF_CLIMATES,
     CONF_DATA_TYPE,
+    CONF_HVAC_MODE_REGISTER,
+    CONF_HVAC_MODE_VALUES,
+    CONF_HVAC_ONOFF_REGISTER,
     CONF_LAZY_ERROR,
     CONF_TARGET_TEMP,
     MODBUS_DOMAIN,
@@ -52,6 +60,40 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_")
                 }
             ],
         },
+        {
+            CONF_CLIMATES: [
+                {
+                    CONF_NAME: TEST_ENTITY_NAME,
+                    CONF_TARGET_TEMP: 117,
+                    CONF_ADDRESS: 117,
+                    CONF_SLAVE: 10,
+                    CONF_HVAC_ONOFF_REGISTER: 12,
+                }
+            ],
+        },
+        {
+            CONF_CLIMATES: [
+                {
+                    CONF_NAME: TEST_ENTITY_NAME,
+                    CONF_TARGET_TEMP: 117,
+                    CONF_ADDRESS: 117,
+                    CONF_SLAVE: 10,
+                    CONF_HVAC_ONOFF_REGISTER: 12,
+                    CONF_HVAC_MODE_REGISTER: {
+                        CONF_ADDRESS: 11,
+                        CONF_HVAC_MODE_VALUES: {
+                            HVACMode.OFF.value: 0,
+                            HVACMode.HEAT.value: 1,
+                            HVACMode.COOL.value: 2,
+                            HVACMode.HEAT_COOL.value: 3,
+                            HVACMode.DRY.value: 4,
+                            HVACMode.FAN_ONLY.value: 5,
+                            HVACMode.AUTO.value: 6,
+                        },
+                    },
+                }
+            ],
+        },
     ],
 )
 async def test_config_climate(hass, mock_modbus):
@@ -59,6 +101,62 @@ async def test_config_climate(hass, mock_modbus):
     assert CLIMATE_DOMAIN in hass.config.components
 
 
+@pytest.mark.parametrize(
+    "do_config",
+    [
+        {
+            CONF_CLIMATES: [
+                {
+                    CONF_NAME: TEST_ENTITY_NAME,
+                    CONF_TARGET_TEMP: 117,
+                    CONF_ADDRESS: 117,
+                    CONF_SLAVE: 10,
+                    CONF_HVAC_MODE_REGISTER: {
+                        CONF_ADDRESS: 11,
+                        CONF_HVAC_MODE_VALUES: {
+                            HVACMode.OFF.value: 0,
+                            HVACMode.HEAT.value: 1,
+                            HVACMode.COOL.value: 2,
+                            HVACMode.HEAT_COOL.value: 3,
+                        },
+                    },
+                }
+            ],
+        },
+    ],
+)
+async def test_config_hvac_mode_register(hass, mock_modbus):
+    """Run configuration test for mode register."""
+    state = hass.states.get(ENTITY_ID)
+    assert HVACMode.OFF in state.attributes[ATTR_HVAC_MODES]
+    assert HVACMode.HEAT in state.attributes[ATTR_HVAC_MODES]
+    assert HVACMode.COOL in state.attributes[ATTR_HVAC_MODES]
+    assert HVACMode.HEAT_COOL in state.attributes[ATTR_HVAC_MODES]
+
+
+@pytest.mark.parametrize(
+    "do_config",
+    [
+        {
+            CONF_CLIMATES: [
+                {
+                    CONF_NAME: TEST_ENTITY_NAME,
+                    CONF_TARGET_TEMP: 117,
+                    CONF_ADDRESS: 117,
+                    CONF_SLAVE: 10,
+                    CONF_HVAC_ONOFF_REGISTER: 11,
+                }
+            ],
+        },
+    ],
+)
+async def test_config_hvac_onoff_register(hass, mock_modbus):
+    """Run configuration test for On/Off register."""
+    state = hass.states.get(ENTITY_ID)
+    assert HVACMode.OFF in state.attributes[ATTR_HVAC_MODES]
+    assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES]
+
+
 @pytest.mark.parametrize(
     "do_config",
     [
@@ -90,28 +188,93 @@ async def test_temperature_climate(hass, expected, mock_do_cycle):
 
 
 @pytest.mark.parametrize(
-    "do_config",
+    "do_config,result,register_words",
     [
-        {
-            CONF_CLIMATES: [
-                {
-                    CONF_NAME: TEST_ENTITY_NAME,
-                    CONF_TARGET_TEMP: 117,
-                    CONF_ADDRESS: 117,
-                    CONF_SLAVE: 10,
-                    CONF_SCAN_INTERVAL: 0,
-                    CONF_DATA_TYPE: DataType.INT32,
-                }
-            ]
-        },
+        (
+            {
+                CONF_CLIMATES: [
+                    {
+                        CONF_NAME: TEST_ENTITY_NAME,
+                        CONF_TARGET_TEMP: 117,
+                        CONF_ADDRESS: 117,
+                        CONF_SLAVE: 10,
+                        CONF_SCAN_INTERVAL: 0,
+                        CONF_DATA_TYPE: DataType.INT32,
+                        CONF_HVAC_MODE_REGISTER: {
+                            CONF_ADDRESS: 118,
+                            CONF_HVAC_MODE_VALUES: {
+                                HVACMode.COOL.value: 0,
+                                HVACMode.HEAT.value: 1,
+                                HVACMode.DRY.value: 2,
+                            },
+                        },
+                    },
+                ]
+            },
+            HVACMode.COOL,
+            [0x00],
+        ),
+        (
+            {
+                CONF_CLIMATES: [
+                    {
+                        CONF_NAME: TEST_ENTITY_NAME,
+                        CONF_TARGET_TEMP: 117,
+                        CONF_ADDRESS: 117,
+                        CONF_SLAVE: 10,
+                        CONF_SCAN_INTERVAL: 0,
+                        CONF_DATA_TYPE: DataType.INT32,
+                        CONF_HVAC_MODE_REGISTER: {
+                            CONF_ADDRESS: 118,
+                            CONF_HVAC_MODE_VALUES: {
+                                HVACMode.COOL.value: 0,
+                                HVACMode.HEAT.value: 1,
+                                HVACMode.DRY.value: 2,
+                            },
+                        },
+                    },
+                ]
+            },
+            HVACMode.HEAT,
+            [0x01],
+        ),
+        (
+            {
+                CONF_CLIMATES: [
+                    {
+                        CONF_NAME: TEST_ENTITY_NAME,
+                        CONF_TARGET_TEMP: 117,
+                        CONF_ADDRESS: 117,
+                        CONF_SLAVE: 10,
+                        CONF_SCAN_INTERVAL: 0,
+                        CONF_DATA_TYPE: DataType.INT32,
+                        CONF_HVAC_MODE_REGISTER: {
+                            CONF_ADDRESS: 118,
+                            CONF_HVAC_MODE_VALUES: {
+                                HVACMode.COOL.value: 0,
+                                HVACMode.HEAT.value: 2,
+                                HVACMode.DRY.value: 3,
+                            },
+                        },
+                        CONF_HVAC_ONOFF_REGISTER: 119,
+                    },
+                ]
+            },
+            HVACMode.OFF,
+            [0x00],
+        ),
     ],
 )
-async def test_service_climate_update(hass, mock_modbus, mock_ha):
+async def test_service_climate_update(
+    hass, mock_modbus, mock_ha, result, register_words
+):
     """Run test for service homeassistant.update_entity."""
+    mock_modbus.read_holding_registers.return_value = ReadResult(register_words)
     await hass.services.async_call(
         "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
     )
-    assert hass.states.get(ENTITY_ID).state == "auto"
+    await hass.async_block_till_done()
+    assert hass.states.get(ENTITY_ID).state == result
 
 
 @pytest.mark.parametrize(
@@ -195,6 +358,68 @@ async def test_service_climate_set_temperature(
     )
 
 
+@pytest.mark.parametrize(
+    "hvac_mode, result, do_config",
+    [
+        (
+            HVACMode.COOL,
+            [0x00],
+            {
+                CONF_CLIMATES: [
+                    {
+                        CONF_NAME: TEST_ENTITY_NAME,
+                        CONF_TARGET_TEMP: 117,
+                        CONF_ADDRESS: 117,
+                        CONF_SLAVE: 10,
+                        CONF_HVAC_MODE_REGISTER: {
+                            CONF_ADDRESS: 118,
+                            CONF_HVAC_MODE_VALUES: {
+                                HVACMode.COOL.value: 1,
+                                HVACMode.HEAT.value: 2,
+                            },
+                        },
+                    }
+                ]
+            },
+        ),
+        (
+            HVACMode.HEAT,
+            [0x00],
+            {
+                CONF_CLIMATES: [
+                    {
+                        CONF_NAME: TEST_ENTITY_NAME,
+                        CONF_TARGET_TEMP: 117,
+                        CONF_ADDRESS: 117,
+                        CONF_SLAVE: 10,
+                        CONF_HVAC_MODE_REGISTER: {
+                            CONF_ADDRESS: 118,
+                            CONF_HVAC_MODE_VALUES: {
+                                HVACMode.COOL.value: 1,
+                                HVACMode.HEAT.value: 2,
+                            },
+                        },
+                        CONF_HVAC_ONOFF_REGISTER: 119,
+                    }
+                ]
+            },
+        ),
+    ],
+)
+async def test_service_set_mode(hass, hvac_mode, result, mock_modbus, mock_ha):
+    """Test set mode."""
+    mock_modbus.read_holding_registers.return_value = ReadResult(result)
+    await hass.services.async_call(
+        CLIMATE_DOMAIN,
+        "set_hvac_mode",
+        {
+            "entity_id": ENTITY_ID,
+            ATTR_HVAC_MODE: hvac_mode,
+        },
+        blocking=True,
+    )
+
+
 test_value = State(ENTITY_ID, 35)
 test_value.attributes = {ATTR_TEMPERATURE: 37}
 
-- 
GitLab


From 2c43606922b30e303c65339bde83ca54495220f9 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 20 Oct 2022 13:41:14 +0200
Subject: [PATCH 627/985] Add websocket type hints in components (#80654)

* Add websocket type hints in components

* Adjust

* Apply suggestion

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
---
 homeassistant/components/cloud/http_api.py    |  6 ++-
 .../components/config/area_registry.py        | 28 ++++++++++---
 .../components/config/config_entries.py       |  6 ++-
 .../components/energy/websocket_api.py        |  2 +-
 homeassistant/components/frontend/__init__.py |  4 +-
 .../components/mobile_app/websocket_api.py    |  6 ++-
 homeassistant/components/mqtt/__init__.py     |  6 ++-
 homeassistant/components/person/__init__.py   |  7 +++-
 .../components/recorder/websocket_api.py      | 10 ++---
 .../components/repairs/websocket_api.py       |  4 +-
 .../components/rtsp_to_webrtc/__init__.py     |  3 +-
 homeassistant/components/search/__init__.py   |  7 +++-
 .../components/shopping_list/__init__.py      | 14 +++----
 .../components/system_log/__init__.py         |  5 ++-
 .../components/trace/websocket_api.py         | 42 +++++++++++++++----
 homeassistant/components/webhook/__init__.py  |  6 ++-
 16 files changed, 116 insertions(+), 40 deletions(-)

diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index 90b8bad108f..01b6cd17508 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -689,7 +689,11 @@ async def thingtalk_convert(
 
 
 @websocket_api.websocket_command({"type": "cloud/tts/info"})
-def tts_info(hass, connection, msg):
+def tts_info(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Fetch available tts info."""
     connection.send_result(
         msg["id"], {"languages": [(lang, gender.value) for lang, gender in MAP_VOICE]}
diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py
index 3a2c55b3f5d..bf63a516b58 100644
--- a/homeassistant/components/config/area_registry.py
+++ b/homeassistant/components/config/area_registry.py
@@ -1,8 +1,10 @@
 """HTTP views to interact with the area registry."""
+from typing import Any
+
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.area_registry import async_get
 
 
@@ -17,7 +19,11 @@ async def async_setup(hass):
 
 @websocket_api.websocket_command({vol.Required("type"): "config/area_registry/list"})
 @callback
-def websocket_list_areas(hass, connection, msg):
+def websocket_list_areas(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle list areas command."""
     registry = async_get(hass)
     connection.send_result(
@@ -35,7 +41,11 @@ def websocket_list_areas(hass, connection, msg):
 )
 @websocket_api.require_admin
 @callback
-def websocket_create_area(hass, connection, msg):
+def websocket_create_area(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Create area command."""
     registry = async_get(hass)
 
@@ -59,7 +69,11 @@ def websocket_create_area(hass, connection, msg):
 )
 @websocket_api.require_admin
 @callback
-def websocket_delete_area(hass, connection, msg):
+def websocket_delete_area(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Delete area command."""
     registry = async_get(hass)
 
@@ -81,7 +95,11 @@ def websocket_delete_area(hass, connection, msg):
 )
 @websocket_api.require_admin
 @callback
-def websocket_update_area(hass, connection, msg):
+def websocket_update_area(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle update area websocket command."""
     registry = async_get(hass)
 
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index 50bcad45db7..39c5bce25cb 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -243,7 +243,11 @@ class OptionManagerFlowResourceView(FlowManagerResourceView):
 
 @websocket_api.require_admin
 @websocket_api.websocket_command({"type": "config_entries/flow/progress"})
-def config_entries_progress(hass, connection, msg):
+def config_entries_progress(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """List flows that are in progress but not started by a user.
 
     Example of a non-user initiated flow is a discovered Hue hub that
diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py
index f1b75c8ea9e..8fa35e607b2 100644
--- a/homeassistant/components/energy/websocket_api.py
+++ b/homeassistant/components/energy/websocket_api.py
@@ -185,7 +185,7 @@ async def ws_validate(
 async def ws_solar_forecast(
     hass: HomeAssistant,
     connection: websocket_api.ActiveConnection,
-    msg: dict,
+    msg: dict[str, Any],
     manager: EnergyManager,
 ) -> None:
     """Handle solar forecast command."""
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index c49af972e38..f5fe37c1819 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -614,7 +614,7 @@ class ManifestJSONView(HomeAssistantView):
 @callback
 @websocket_api.websocket_command({"type": "get_panels"})
 def websocket_get_panels(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle get panels command."""
     user_is_admin = connection.user.is_admin
@@ -630,7 +630,7 @@ def websocket_get_panels(
 @callback
 @websocket_api.websocket_command({"type": "frontend/get_themes"})
 def websocket_get_themes(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle get themes command."""
     if hass.config.safe_mode:
diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py
index 5225100662c..c900aa8f93b 100644
--- a/homeassistant/components/mobile_app/websocket_api.py
+++ b/homeassistant/components/mobile_app/websocket_api.py
@@ -57,7 +57,11 @@ def _ensure_webhook_access(func):
         vol.Required("confirm_id"): str,
     }
 )
-def handle_push_notification_confirm(hass, connection, msg):
+def handle_push_notification_confirm(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Confirm receipt of a push notification."""
     channel: PushChannel | None = hass.data[DOMAIN][DATA_PUSH_CHANNEL].get(
         msg["webhook_id"]
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index a63b6137fea..ba1af354071 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -496,7 +496,11 @@ async def async_reload_manual_mqtt_items(hass: HomeAssistant) -> None:
     {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str}
 )
 @callback
-def websocket_mqtt_info(hass, connection, msg):
+def websocket_mqtt_info(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Get MQTT debug info for device."""
     device_id = msg["device_id"]
     mqtt_info = debug_info.info_for_device(hass, device_id)
diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py
index 86a132027d8..a469a459ace 100644
--- a/homeassistant/components/person/__init__.py
+++ b/homeassistant/components/person/__init__.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import logging
+from typing import Any
 
 import voluptuous as vol
 
@@ -546,8 +547,10 @@ class Person(collection.CollectionEntity, RestoreEntity):
 
 @websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"})
 def ws_list_person(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
-):
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """List persons."""
     yaml, storage, _ = hass.data[DOMAIN]
     connection.send_result(
diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py
index d7fbce60767..37d117c910e 100644
--- a/homeassistant/components/recorder/websocket_api.py
+++ b/homeassistant/components/recorder/websocket_api.py
@@ -207,7 +207,7 @@ async def ws_validate_statistics(
 )
 @callback
 def ws_clear_statistics(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Clear statistics for a list of statistic_ids.
 
@@ -246,7 +246,7 @@ async def ws_get_statistics_metadata(
 )
 @callback
 def ws_update_statistics_metadata(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Update statistics metadata for a statistic_id.
 
@@ -269,7 +269,7 @@ def ws_update_statistics_metadata(
 )
 @callback
 def ws_change_statistics_unit(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Change the unit_of_measurement for a statistic_id.
 
@@ -371,7 +371,7 @@ async def ws_adjust_sum_statistics(
 )
 @callback
 def ws_import_statistics(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Import statistics."""
     metadata = msg["metadata"]
@@ -391,7 +391,7 @@ def ws_import_statistics(
 )
 @callback
 def ws_info(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Return status of the recorder."""
     instance = get_instance(hass)
diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py
index b4ccca7c894..c5408054318 100644
--- a/homeassistant/components/repairs/websocket_api.py
+++ b/homeassistant/components/repairs/websocket_api.py
@@ -46,7 +46,7 @@ def async_setup(hass: HomeAssistant) -> None:
     }
 )
 def ws_ignore_issue(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Fix an issue."""
     async_ignore_issue(hass, msg["domain"], msg["issue_id"], msg["ignore"])
@@ -61,7 +61,7 @@ def ws_ignore_issue(
 )
 @callback
 def ws_list_issues(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Return a list of issues."""
 
diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py
index f0e013fc02f..f5f114bce9c 100644
--- a/homeassistant/components/rtsp_to_webrtc/__init__.py
+++ b/homeassistant/components/rtsp_to_webrtc/__init__.py
@@ -19,6 +19,7 @@ Other integrations may use this integration with these steps:
 from __future__ import annotations
 
 import logging
+from typing import Any
 
 import async_timeout
 from rtsp_to_webrtc.client import get_adaptive_client
@@ -109,7 +110,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
 )
 @callback
 def ws_get_settings(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Handle the websocket command."""
     connection.send_result(
diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py
index e7df1de860e..70702f351f6 100644
--- a/homeassistant/components/search/__init__.py
+++ b/homeassistant/components/search/__init__.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 
 from collections import defaultdict, deque
 import logging
+from typing import Any
 
 import voluptuous as vol
 
@@ -44,7 +45,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     }
 )
 @callback
-def websocket_search_related(hass, connection, msg):
+def websocket_search_related(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Handle search."""
     searcher = Searcher(
         hass,
diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py
index 577786fca99..0f7afe3240e 100644
--- a/homeassistant/components/shopping_list/__init__.py
+++ b/homeassistant/components/shopping_list/__init__.py
@@ -360,8 +360,8 @@ class ClearCompletedItemsView(http.HomeAssistantView):
 @callback
 def websocket_handle_items(
     hass: HomeAssistant,
-    connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
 ) -> None:
     """Handle get shopping_list items."""
     connection.send_message(
@@ -372,7 +372,7 @@ def websocket_handle_items(
 @websocket_api.async_response
 async def websocket_handle_add(
     hass: HomeAssistant,
-    connection: websocket_api.connection.ActiveConnection,
+    connection: websocket_api.ActiveConnection,
     msg: dict[str, Any],
 ) -> None:
     """Handle add item to shopping_list."""
@@ -383,7 +383,7 @@ async def websocket_handle_add(
 @websocket_api.async_response
 async def websocket_handle_update(
     hass: HomeAssistant,
-    connection: websocket_api.connection.ActiveConnection,
+    connection: websocket_api.ActiveConnection,
     msg: dict[str, Any],
 ) -> None:
     """Handle update shopping_list item."""
@@ -406,7 +406,7 @@ async def websocket_handle_update(
 @websocket_api.async_response
 async def websocket_handle_clear(
     hass: HomeAssistant,
-    connection: websocket_api.connection.ActiveConnection,
+    connection: websocket_api.ActiveConnection,
     msg: dict[str, Any],
 ) -> None:
     """Handle clearing shopping_list items."""
@@ -422,8 +422,8 @@ async def websocket_handle_clear(
 )
 def websocket_handle_reorder(
     hass: HomeAssistant,
-    connection: websocket_api.connection.ActiveConnection,
-    msg: dict,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
 ) -> None:
     """Handle reordering shopping_list items."""
     msg_id = msg.pop("id")
diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py
index b0d538a4ff8..fae8598407e 100644
--- a/homeassistant/components/system_log/__init__.py
+++ b/homeassistant/components/system_log/__init__.py
@@ -3,6 +3,7 @@ from collections import OrderedDict, deque
 import logging
 import re
 import traceback
+from typing import Any
 
 import voluptuous as vol
 
@@ -240,8 +241,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
 @websocket_api.websocket_command({vol.Required("type"): "system_log/list"})
 @callback
 def list_errors(
-    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
-):
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
+) -> None:
     """List all possible diagnostic handlers."""
     connection.send_result(
         msg["id"],
diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py
index ba79ae68968..bf5ebfc1031 100644
--- a/homeassistant/components/trace/websocket_api.py
+++ b/homeassistant/components/trace/websocket_api.py
@@ -135,7 +135,11 @@ async def websocket_trace_contexts(
         vol.Optional("run_id"): str,
     }
 )
-def websocket_breakpoint_set(hass, connection, msg):
+def websocket_breakpoint_set(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Set breakpoint."""
     key = f"{msg['domain']}.{msg['item_id']}"
     node = msg["node"]
@@ -162,7 +166,11 @@ def websocket_breakpoint_set(hass, connection, msg):
         vol.Optional("run_id"): str,
     }
 )
-def websocket_breakpoint_clear(hass, connection, msg):
+def websocket_breakpoint_clear(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Clear breakpoint."""
     key = f"{msg['domain']}.{msg['item_id']}"
     node = msg["node"]
@@ -176,7 +184,11 @@ def websocket_breakpoint_clear(hass, connection, msg):
 @callback
 @websocket_api.require_admin
 @websocket_api.websocket_command({vol.Required("type"): "trace/debug/breakpoint/list"})
-def websocket_breakpoint_list(hass, connection, msg):
+def websocket_breakpoint_list(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """List breakpoints."""
     breakpoints = breakpoint_list(hass)
     for _breakpoint in breakpoints:
@@ -191,7 +203,11 @@ def websocket_breakpoint_list(hass, connection, msg):
 @websocket_api.websocket_command(
     {vol.Required("type"): "trace/debug/breakpoint/subscribe"}
 )
-def websocket_subscribe_breakpoint_events(hass, connection, msg):
+def websocket_subscribe_breakpoint_events(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Subscribe to breakpoint events."""
 
     @callback
@@ -240,7 +256,11 @@ def websocket_subscribe_breakpoint_events(hass, connection, msg):
         vol.Required("run_id"): str,
     }
 )
-def websocket_debug_continue(hass, connection, msg):
+def websocket_debug_continue(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Resume execution of halted script or automation."""
     key = f"{msg['domain']}.{msg['item_id']}"
     run_id = msg["run_id"]
@@ -260,7 +280,11 @@ def websocket_debug_continue(hass, connection, msg):
         vol.Required("run_id"): str,
     }
 )
-def websocket_debug_step(hass, connection, msg):
+def websocket_debug_step(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Single step a halted script or automation."""
     key = f"{msg['domain']}.{msg['item_id']}"
     run_id = msg["run_id"]
@@ -280,7 +304,11 @@ def websocket_debug_step(hass, connection, msg):
         vol.Required("run_id"): str,
     }
 )
-def websocket_debug_stop(hass, connection, msg):
+def websocket_debug_stop(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Stop a halted script or automation."""
     key = f"{msg['domain']}.{msg['item_id']}"
     run_id = msg["run_id"]
diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py
index f78d9059ee9..bd80f38b832 100644
--- a/homeassistant/components/webhook/__init__.py
+++ b/homeassistant/components/webhook/__init__.py
@@ -168,7 +168,11 @@ class WebhookView(HomeAssistantView):
     }
 )
 @callback
-def websocket_list(hass, connection, msg):
+def websocket_list(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Return a list of webhooks."""
     handlers = hass.data.setdefault(DOMAIN, {})
     result = [
-- 
GitLab


From aea7a9af18340e3b5432d4b273200c122564615d Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Thu, 20 Oct 2022 15:08:48 +0300
Subject: [PATCH 628/985] Bump aioshelly to 4.0.0 (#80423)

* Bump aioshelly to 4.0.0

* Remove leftover

* Fix number platform

* Set last_update_success to false upon failure in number and climate

* Set last_update_success upon failurie in entity
---
 homeassistant/components/shelly/__init__.py   |  42 ++-----
 homeassistant/components/shelly/climate.py    |  28 ++---
 .../components/shelly/config_flow.py          | 109 ++++++++----------
 homeassistant/components/shelly/const.py      |   6 -
 .../components/shelly/coordinator.py          |  95 ++++++++-------
 homeassistant/components/shelly/entity.py     |  41 +++----
 homeassistant/components/shelly/manifest.json |   2 +-
 homeassistant/components/shelly/number.py     |  24 ++--
 requirements_all.txt                          |   2 +-
 requirements_test_all.txt                     |   2 +-
 tests/components/shelly/test_config_flow.py   |  88 ++++----------
 11 files changed, 174 insertions(+), 265 deletions(-)

diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index 9759fd148d0..49a67b1a6a0 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -1,16 +1,12 @@
 """The Shelly integration."""
 from __future__ import annotations
 
-import asyncio
-from http import HTTPStatus
 from typing import Any, Final
 
-from aiohttp import ClientResponseError
 import aioshelly
 from aioshelly.block_device import BlockDevice
-from aioshelly.exceptions import AuthRequired, InvalidAuthError
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
 from aioshelly.rpc_device import RpcDevice
-import async_timeout
 import voluptuous as vol
 
 from homeassistant.config_entries import ConfigEntry
@@ -23,7 +19,6 @@ from homeassistant.helpers.typing import ConfigType
 from homeassistant.util.unit_system import METRIC_SYSTEM
 
 from .const import (
-    AIOSHELLY_DEVICE_TIMEOUT_SEC,
     CONF_COAP_PORT,
     CONF_SLEEP_PERIOD,
     DATA_CONFIG_ENTRY,
@@ -185,20 +180,11 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
         # Not a sleeping device, finish setup
         LOGGER.debug("Setting up online block device %s", entry.title)
         try:
-            async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
-                await device.initialize()
-                await device.update_status()
-        except asyncio.TimeoutError as err:
-            raise ConfigEntryNotReady(
-                str(err) or "Timeout during device setup"
-            ) from err
-        except OSError as err:
-            raise ConfigEntryNotReady(str(err) or "Error during device setup") from err
-        except AuthRequired as err:
-            raise ConfigEntryAuthFailed from err
-        except ClientResponseError as err:
-            if err.status == HTTPStatus.UNAUTHORIZED:
-                raise ConfigEntryAuthFailed from err
+            await device.initialize()
+        except DeviceConnectionError as err:
+            raise ConfigEntryNotReady(repr(err)) from err
+        except InvalidAuthError as err:
+            raise ConfigEntryAuthFailed(repr(err)) from err
 
         _async_block_device_setup()
     elif sleep_period is None or device_entry is None:
@@ -283,16 +269,12 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo
         # Not a sleeping device, finish setup
         LOGGER.debug("Setting up online RPC device %s", entry.title)
         try:
-            async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
-                await device.initialize()
-        except asyncio.TimeoutError as err:
-            raise ConfigEntryNotReady(
-                str(err) or "Timeout during device setup"
-            ) from err
-        except OSError as err:
-            raise ConfigEntryNotReady(str(err) or "Error during device setup") from err
-        except (AuthRequired, InvalidAuthError) as err:
-            raise ConfigEntryAuthFailed from err
+            await device.initialize()
+        except DeviceConnectionError as err:
+            raise ConfigEntryNotReady(repr(err)) from err
+        except InvalidAuthError as err:
+            raise ConfigEntryAuthFailed(repr(err)) from err
+
         _async_rpc_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
diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py
index a71cdd0b46d..38ba4a51c9f 100644
--- a/homeassistant/components/shelly/climate.py
+++ b/homeassistant/components/shelly/climate.py
@@ -1,13 +1,11 @@
 """Climate support for Shelly."""
 from __future__ import annotations
 
-import asyncio
 from collections.abc import Mapping
 from typing import Any, cast
 
 from aioshelly.block_device import Block
-from aioshelly.exceptions import AuthRequired
-import async_timeout
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
 
 from homeassistant.components.climate import (
     DOMAIN as CLIMATE_DOMAIN,
@@ -20,13 +18,14 @@ 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.exceptions import HomeAssistantError
 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 .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, LOGGER, SHTRV_01_TEMPERATURE_SETTINGS
+from .const import LOGGER, SHTRV_01_TEMPERATURE_SETTINGS
 from .coordinator import ShellyBlockCoordinator, get_entry_data
 from .utils import get_device_entry_gen
 
@@ -238,19 +237,16 @@ class BlockSleepingClimate(
         """Set block state (HTTP request)."""
         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.coordinator.device.http_request(
-                    "get", f"thermostat/{self._channel}", kwargs
-                )
-        except (asyncio.TimeoutError, OSError) as err:
-            LOGGER.error(
-                "Setting state for entity %s failed, state: %s, error: %s",
-                self.name,
-                kwargs,
-                repr(err),
+            return await self.coordinator.device.http_request(
+                "get", f"thermostat/{self._channel}", kwargs
             )
+        except DeviceConnectionError as err:
             self.coordinator.last_update_success = False
-            return None
+            raise HomeAssistantError(
+                f"Setting state for entity {self.name} failed, state: {kwargs}, error: {repr(err)}"
+            ) from err
+        except InvalidAuthError:
+            self.coordinator.entry.async_start_reauth(self.hass)
 
     async def async_set_temperature(self, **kwargs: Any) -> None:
         """Set new target temperature."""
@@ -327,7 +323,7 @@ class BlockSleepingClimate(
                         int(self.block.channel)
                     ]["schedule_profile_names"],
                 ]
-            except AuthRequired:
+            except InvalidAuthError:
                 self.coordinator.entry.async_start_reauth(self.hass)
             else:
                 self.async_write_ha_state()
diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py
index baa9c218d5f..0f6ae9c9da6 100644
--- a/homeassistant/components/shelly/config_flow.py
+++ b/homeassistant/components/shelly/config_flow.py
@@ -1,16 +1,17 @@
 """Config flow for Shelly integration."""
 from __future__ import annotations
 
-import asyncio
 from collections.abc import Mapping
-from http import HTTPStatus
 from typing import Any, Final
 
-import aiohttp
 import aioshelly
 from aioshelly.block_device import BlockDevice
+from aioshelly.exceptions import (
+    DeviceConnectionError,
+    FirmwareUnsupported,
+    InvalidAuthError,
+)
 from aioshelly.rpc_device import RpcDevice
-import async_timeout
 import voluptuous as vol
 
 from homeassistant import config_entries
@@ -20,7 +21,7 @@ from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers import aiohttp_client
 
-from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN, LOGGER
+from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER
 from .utils import (
     get_block_device_name,
     get_block_device_sleep_period,
@@ -35,8 +36,6 @@ from .utils import (
 
 HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str})
 
-HTTP_CONNECT_ERRORS: Final = (asyncio.TimeoutError, aiohttp.ClientError)
-
 
 async def validate_input(
     hass: HomeAssistant,
@@ -54,39 +53,38 @@ async def validate_input(
         data.get(CONF_PASSWORD),
     )
 
-    async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
-        if get_info_gen(info) == 2:
-            ws_context = await get_ws_context(hass)
-            rpc_device = await RpcDevice.create(
-                aiohttp_client.async_get_clientsession(hass),
-                ws_context,
-                options,
-            )
-            await rpc_device.shutdown()
-            assert rpc_device.shelly
-
-            return {
-                "title": get_rpc_device_name(rpc_device),
-                CONF_SLEEP_PERIOD: get_rpc_device_sleep_period(rpc_device.config),
-                "model": rpc_device.shelly.get("model"),
-                "gen": 2,
-            }
-
-        # Gen1
-        coap_context = await get_coap_context(hass)
-        block_device = await BlockDevice.create(
+    if get_info_gen(info) == 2:
+        ws_context = await get_ws_context(hass)
+        rpc_device = await RpcDevice.create(
             aiohttp_client.async_get_clientsession(hass),
-            coap_context,
+            ws_context,
             options,
         )
-        block_device.shutdown()
+        await rpc_device.shutdown()
+        assert rpc_device.shelly
+
         return {
-            "title": get_block_device_name(block_device),
-            CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings),
-            "model": block_device.model,
-            "gen": 1,
+            "title": get_rpc_device_name(rpc_device),
+            CONF_SLEEP_PERIOD: get_rpc_device_sleep_period(rpc_device.config),
+            "model": rpc_device.shelly.get("model"),
+            "gen": 2,
         }
 
+    # Gen1
+    coap_context = await get_coap_context(hass)
+    block_device = await BlockDevice.create(
+        aiohttp_client.async_get_clientsession(hass),
+        coap_context,
+        options,
+    )
+    block_device.shutdown()
+    return {
+        "title": get_block_device_name(block_device),
+        CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings),
+        "model": block_device.model,
+        "gen": 1,
+    }
+
 
 class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
     """Handle a config flow for Shelly."""
@@ -107,9 +105,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
             host: str = user_input[CONF_HOST]
             try:
                 self.info = await self._async_get_info(host)
-            except HTTP_CONNECT_ERRORS:
+            except DeviceConnectionError:
                 errors["base"] = "cannot_connect"
-            except aioshelly.exceptions.FirmwareUnsupported:
+            except FirmwareUnsupported:
                 return self.async_abort(reason="unsupported_firmware")
             except Exception:  # pylint: disable=broad-except
                 LOGGER.exception("Unexpected exception")
@@ -125,7 +123,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
                     device_info = await validate_input(
                         self.hass, self.host, self.info, {}
                     )
-                except HTTP_CONNECT_ERRORS:
+                except DeviceConnectionError:
                     errors["base"] = "cannot_connect"
                 except Exception:  # pylint: disable=broad-except
                     LOGGER.exception("Unexpected exception")
@@ -159,16 +157,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
                 device_info = await validate_input(
                     self.hass, self.host, self.info, user_input
                 )
-            except aiohttp.ClientResponseError as error:
-                if error.status == HTTPStatus.UNAUTHORIZED:
-                    errors["base"] = "invalid_auth"
-                else:
-                    errors["base"] = "cannot_connect"
-            except aioshelly.exceptions.InvalidAuthError:
+            except InvalidAuthError:
                 errors["base"] = "invalid_auth"
-            except HTTP_CONNECT_ERRORS:
-                errors["base"] = "cannot_connect"
-            except aioshelly.exceptions.JSONRPCError:
+            except DeviceConnectionError:
                 errors["base"] = "cannot_connect"
             except Exception:  # pylint: disable=broad-except
                 LOGGER.exception("Unexpected exception")
@@ -210,9 +201,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
         host = discovery_info.host
         try:
             self.info = await self._async_get_info(host)
-        except HTTP_CONNECT_ERRORS:
+        except DeviceConnectionError:
             return self.async_abort(reason="cannot_connect")
-        except aioshelly.exceptions.FirmwareUnsupported:
+        except FirmwareUnsupported:
             return self.async_abort(reason="unsupported_firmware")
 
         await self.async_set_unique_id(self.info["mac"])
@@ -231,7 +222,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
 
         try:
             self.device_info = await validate_input(self.hass, self.host, self.info, {})
-        except HTTP_CONNECT_ERRORS:
+        except DeviceConnectionError:
             return self.async_abort(reason="cannot_connect")
 
         return await self.async_step_confirm_discovery()
@@ -284,23 +275,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
         if user_input is not None:
             try:
                 info = await self._async_get_info(host)
-            except (
-                asyncio.TimeoutError,
-                aiohttp.ClientError,
-                aioshelly.exceptions.FirmwareUnsupported,
-            ):
+            except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported):
                 return self.async_abort(reason="reauth_unsuccessful")
 
             if self.entry.data.get("gen", 1) != 1:
                 user_input[CONF_USERNAME] = "admin"
             try:
                 await validate_input(self.hass, host, info, user_input)
-            except (
-                aiohttp.ClientResponseError,
-                aioshelly.exceptions.InvalidAuthError,
-                asyncio.TimeoutError,
-                aiohttp.ClientError,
-            ):
+            except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported):
                 return self.async_abort(reason="reauth_unsuccessful")
             else:
                 self.hass.config_entries.async_update_entry(
@@ -325,7 +307,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
 
     async def _async_get_info(self, host: str) -> dict[str, Any]:
         """Get info from shelly device."""
-        async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
-            return await aioshelly.common.get_info(
-                aiohttp_client.async_get_clientsession(self.hass), host
-            )
+        return await aioshelly.common.get_info(
+            aiohttp_client.async_get_clientsession(self.hass), host
+        )
diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py
index 401b8131487..39ca515e5ed 100644
--- a/homeassistant/components/shelly/const.py
+++ b/homeassistant/components/shelly/const.py
@@ -46,18 +46,12 @@ DUAL_MODE_LIGHT_MODELS: Final = (
     "SHCB-1",
 )
 
-# Used in "_async_update_data" as timeout for polling data from devices.
-POLLING_TIMEOUT_SEC: Final = 18
-
 # Refresh interval for REST sensors
 REST_SENSORS_UPDATE_INTERVAL: Final = 60
 
 # Refresh interval for RPC polling sensors
 RPC_SENSORS_POLLING_INTERVAL: Final = 60
 
-# Timeout used for aioshelly calls
-AIOSHELLY_DEVICE_TIMEOUT_SEC: Final = 10
-
 # Multiplier used to calculate the "update_interval" for sleeping devices.
 SLEEP_PERIOD_MULTIPLIER: Final = 1.2
 CONF_SLEEP_PERIOD: Final = "sleep_period"
diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py
index ec115cfc69f..f309fc99358 100644
--- a/homeassistant/components/shelly/coordinator.py
+++ b/homeassistant/components/shelly/coordinator.py
@@ -1,7 +1,6 @@
 """Coordinators for the Shelly integration."""
 from __future__ import annotations
 
-import asyncio
 from collections.abc import Coroutine
 from dataclasses import dataclass
 from datetime import timedelta
@@ -9,18 +8,18 @@ from typing import Any, cast
 
 import aioshelly
 from aioshelly.block_device import BlockDevice
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
 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.exceptions import HomeAssistantError
 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,
     ATTR_BETA,
     ATTR_CHANNEL,
     ATTR_CLICK_TYPE,
@@ -36,7 +35,6 @@ from .const import (
     INPUTS_EVENTS_DICT,
     LOGGER,
     MODELS_SUPPORTING_LIGHT_EFFECTS,
-    POLLING_TIMEOUT_SEC,
     REST_SENSORS_UPDATE_INTERVAL,
     RPC_INPUTS_EVENTS_TYPES,
     RPC_RECONNECT_INTERVAL,
@@ -212,11 +210,13 @@ class ShellyBlockCoordinator(DataUpdateCoordinator):
 
         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 UpdateFailed("Error fetching data") from err
+            await self.device.update()
+        except DeviceConnectionError as err:
+            raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
+        except InvalidAuthError:
+            self.entry.async_start_reauth(self.hass)
+        else:
+            device_update_info(self.hass, self.device, self.entry)
 
     @property
     def model(self) -> str:
@@ -278,11 +278,13 @@ class ShellyBlockCoordinator(DataUpdateCoordinator):
             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)
+            result = await self.device.trigger_ota_update(beta=beta)
+        except DeviceConnectionError as err:
+            raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err
+        except InvalidAuthError:
+            self.entry.async_start_reauth(self.hass)
+        else:
+            LOGGER.debug("Result of OTA update call: %s", result)
 
     def shutdown(self) -> None:
         """Shutdown the coordinator."""
@@ -323,20 +325,22 @@ class ShellyRestCoordinator(DataUpdateCoordinator):
 
     async def _async_update_data(self) -> None:
         """Fetch data."""
+        LOGGER.debug("REST update for %s", self.name)
         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 UpdateFailed("Error fetching data") from err
+            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
+        except DeviceConnectionError as err:
+            raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
+        except InvalidAuthError:
+            self.entry.async_start_reauth(self.hass)
+        else:
+            device_update_info(self.hass, self.device, self.entry)
 
     @property
     def mac(self) -> str:
@@ -436,13 +440,14 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
         if self.device.connected:
             return
 
+        LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name)
         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 UpdateFailed("Device disconnected") from err
+            await self.device.initialize()
+            device_update_info(self.hass, self.device, self.entry)
+        except DeviceConnectionError as err:
+            raise UpdateFailed(f"Device disconnected: {repr(err)}") from err
+        except InvalidAuthError:
+            self.entry.async_start_reauth(self.hass)
 
     @property
     def model(self) -> str:
@@ -503,12 +508,13 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
             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")
+            await self.device.trigger_ota_update(beta=beta)
+        except DeviceConnectionError as err:
+            raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err
+        except InvalidAuthError:
+            self.entry.async_start_reauth(self.hass)
+        else:
+            LOGGER.debug("OTA update call successful")
 
     async def shutdown(self) -> None:
         """Shutdown the coordinator."""
@@ -544,12 +550,13 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator):
         if not self.device.connected:
             raise UpdateFailed("Device disconnected")
 
+        LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
         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 UpdateFailed("Device disconnected") from err
+            await self.device.update_status()
+        except DeviceConnectionError as err:
+            raise UpdateFailed(f"Device disconnected: {repr(err)}") from err
+        except InvalidAuthError:
+            self.entry.async_start_reauth(self.hass)
 
     @property
     def model(self) -> str:
diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py
index 27f7b5dc689..72794be60cc 100644
--- a/homeassistant/components/shelly/entity.py
+++ b/homeassistant/components/shelly/entity.py
@@ -1,16 +1,16 @@
 """Shelly entity helper."""
 from __future__ import annotations
 
-import asyncio
 from collections.abc import Callable, Mapping
 from dataclasses import dataclass
 from typing import Any, cast
 
 from aioshelly.block_device import Block
-import async_timeout
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers import device_registry, entity, entity_registry
 from homeassistant.helpers.entity import DeviceInfo, EntityDescription
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -19,7 +19,7 @@ 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, CONF_SLEEP_PERIOD, LOGGER
+from .const import CONF_SLEEP_PERIOD, LOGGER
 from .coordinator import (
     ShellyBlockCoordinator,
     ShellyRpcCoordinator,
@@ -362,17 +362,14 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
         """Set block state (HTTP request)."""
         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.block.set_state(**kwargs)
-        except (asyncio.TimeoutError, OSError) as err:
-            LOGGER.error(
-                "Setting state for entity %s failed, state: %s, error: %s",
-                self.name,
-                kwargs,
-                repr(err),
-            )
+            return await self.block.set_state(**kwargs)
+        except DeviceConnectionError as err:
             self.coordinator.last_update_success = False
-            return None
+            raise HomeAssistantError(
+                f"Setting state for entity {self.name} failed, state: {kwargs}, error: {repr(err)}"
+            ) from err
+        except InvalidAuthError:
+            self.coordinator.entry.async_start_reauth(self.hass)
 
 
 class ShellyRpcEntity(entity.Entity):
@@ -425,18 +422,14 @@ class ShellyRpcEntity(entity.Entity):
             params,
         )
         try:
-            async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC):
-                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",
-                self.name,
-                method,
-                params,
-                repr(err),
-            )
+            return await self.coordinator.device.call_rpc(method, params)
+        except DeviceConnectionError as err:
             self.coordinator.last_update_success = False
-            return None
+            raise HomeAssistantError(
+                f"Call RPC for entity {self.name} failed, method: {method}, params: {params}, error: {repr(err)}"
+            ) from err
+        except InvalidAuthError:
+            self.coordinator.entry.async_start_reauth(self.hass)
 
 
 class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json
index 6996a42e022..b3bd329b868 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==3.0.0"],
+  "requirements": ["aioshelly==4.0.0"],
   "dependencies": ["http"],
   "zeroconf": [
     {
diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py
index eb61fccb9ef..bb7f17ea18d 100644
--- a/homeassistant/components/shelly/number.py
+++ b/homeassistant/components/shelly/number.py
@@ -1,11 +1,10 @@
 """Number for Shelly."""
 from __future__ import annotations
 
-import asyncio
 from dataclasses import dataclass
 from typing import Any, Final, cast
 
-import async_timeout
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
 
 from homeassistant.components.number import (
     NumberEntity,
@@ -15,11 +14,12 @@ from homeassistant.components.number import (
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import PERCENTAGE
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.entity_registry import RegistryEntry
 
-from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, LOGGER
+from .const import CONF_SLEEP_PERIOD, LOGGER
 from .entity import (
     BlockEntityDescription,
     ShellySleepingBlockAttributeEntity,
@@ -115,15 +115,13 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity):
 
     async def _set_state_full_path(self, path: str, params: Any) -> Any:
         """Set block state (HTTP request)."""
-
         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.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",
-                self.name,
-                params,
-                repr(err),
-            )
+            return await self.coordinator.device.http_request("get", path, params)
+        except DeviceConnectionError as err:
+            self.coordinator.last_update_success = False
+            raise HomeAssistantError(
+                f"Setting state for entity {self.name} failed, state: {params}, error: {repr(err)}"
+            ) from err
+        except InvalidAuthError:
+            self.coordinator.entry.async_start_reauth(self.hass)
diff --git a/requirements_all.txt b/requirements_all.txt
index 4826d0003b0..90b961b0961 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==3.0.0
+aioshelly==4.0.0
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 43f57742039..f2808df8629 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==3.0.0
+aioshelly==4.0.0
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py
index a761fe7836e..ad28ffbd4f0 100644
--- a/tests/components/shelly/test_config_flow.py
+++ b/tests/components/shelly/test_config_flow.py
@@ -1,10 +1,11 @@
 """Test the Shelly config flow."""
-import asyncio
-from http import HTTPStatus
 from unittest.mock import AsyncMock, Mock, patch
 
-import aiohttp
-import aioshelly
+from aioshelly.exceptions import (
+    DeviceConnectionError,
+    FirmwareUnsupported,
+    InvalidAuthError,
+)
 import pytest
 
 from homeassistant import config_entries, data_entry_flow
@@ -207,7 +208,7 @@ async def test_form_auth(hass, test_data):
 
 
 @pytest.mark.parametrize(
-    "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")]
+    "error", [(DeviceConnectionError, "cannot_connect"), (ValueError, "unknown")]
 )
 async def test_form_errors_get_info(hass, error):
     """Test we handle errors."""
@@ -324,7 +325,7 @@ async def test_form_missing_model_key_zeroconf(hass, caplog):
 
 
 @pytest.mark.parametrize(
-    "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")]
+    "error", [(DeviceConnectionError, "cannot_connect"), (ValueError, "unknown")]
 )
 async def test_form_errors_test_connection(hass, error):
     """Test we handle errors."""
@@ -431,10 +432,7 @@ async def test_form_firmware_unsupported(hass):
         DOMAIN, context={"source": config_entries.SOURCE_USER}
     )
 
-    with patch(
-        "aioshelly.common.get_info",
-        side_effect=aioshelly.exceptions.FirmwareUnsupported,
-    ):
+    with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported):
         result2 = await hass.config_entries.flow.async_configure(
             result["flow_id"],
             {"host": "1.1.1.1"},
@@ -447,15 +445,8 @@ async def test_form_firmware_unsupported(hass):
 @pytest.mark.parametrize(
     "error",
     [
-        (
-            aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST),
-            "cannot_connect",
-        ),
-        (
-            aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED),
-            "invalid_auth",
-        ),
-        (asyncio.TimeoutError, "cannot_connect"),
+        (InvalidAuthError, "invalid_auth"),
+        (DeviceConnectionError, "cannot_connect"),
         (ValueError, "unknown"),
     ],
 )
@@ -490,15 +481,8 @@ async def test_form_auth_errors_test_connection_gen1(hass, error):
 @pytest.mark.parametrize(
     "error",
     [
-        (
-            aioshelly.exceptions.JSONRPCError(code=400),
-            "cannot_connect",
-        ),
-        (
-            aioshelly.exceptions.InvalidAuthError(code=401),
-            "invalid_auth",
-        ),
-        (asyncio.TimeoutError, "cannot_connect"),
+        (DeviceConnectionError, "cannot_connect"),
+        (InvalidAuthError, "invalid_auth"),
         (ValueError, "unknown"),
     ],
 )
@@ -647,20 +631,8 @@ async def test_zeroconf_sleeping_device(hass):
     assert len(mock_setup_entry.mock_calls) == 1
 
 
-@pytest.mark.parametrize(
-    "error",
-    [
-        (
-            aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST),
-            "cannot_connect",
-        ),
-        (asyncio.TimeoutError, "cannot_connect"),
-    ],
-)
-async def test_zeroconf_sleeping_device_error(hass, error):
+async def test_zeroconf_sleeping_device_error(hass):
     """Test sleeping device configuration via zeroconf with error."""
-    exc = error
-
     with patch(
         "aioshelly.common.get_info",
         return_value={
@@ -671,7 +643,7 @@ async def test_zeroconf_sleeping_device_error(hass, error):
         },
     ), patch(
         "aioshelly.block_device.BlockDevice.create",
-        new=AsyncMock(side_effect=exc),
+        new=AsyncMock(side_effect=DeviceConnectionError),
     ):
         result = await hass.config_entries.flow.async_init(
             DOMAIN,
@@ -708,10 +680,7 @@ async def test_zeroconf_already_configured(hass):
 
 async def test_zeroconf_firmware_unsupported(hass):
     """Test we abort if device firmware is unsupported."""
-    with patch(
-        "aioshelly.common.get_info",
-        side_effect=aioshelly.exceptions.FirmwareUnsupported,
-    ):
+    with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported):
         result = await hass.config_entries.flow.async_init(
             DOMAIN,
             data=DISCOVERY_INFO,
@@ -724,7 +693,7 @@ async def test_zeroconf_firmware_unsupported(hass):
 
 async def test_zeroconf_cannot_connect(hass):
     """Test we get the form."""
-    with patch("aioshelly.common.get_info", side_effect=asyncio.TimeoutError):
+    with patch("aioshelly.common.get_info", side_effect=DeviceConnectionError):
         result = await hass.config_entries.flow.async_init(
             DOMAIN,
             data=DISCOVERY_INFO,
@@ -840,21 +809,13 @@ async def test_reauth_successful(hass, test_data):
 @pytest.mark.parametrize(
     "test_data",
     [
-        (
-            1,
-            {"username": "test user", "password": "test1 password"},
-            aioshelly.exceptions.InvalidAuthError(code=HTTPStatus.UNAUTHORIZED.value),
-        ),
-        (
-            2,
-            {"password": "test2 password"},
-            aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED),
-        ),
+        (1, {"username": "test user", "password": "test1 password"}),
+        (2, {"password": "test2 password"}),
     ],
 )
 async def test_reauth_unsuccessful(hass, test_data):
     """Test reauthentication flow failed."""
-    gen, user_input, exc = test_data
+    gen, user_input = test_data
     entry = MockConfigEntry(
         domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0", "gen": gen}
     )
@@ -865,9 +826,10 @@ async def test_reauth_unsuccessful(hass, test_data):
         return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen},
     ), patch(
         "aioshelly.block_device.BlockDevice.create",
-        new=AsyncMock(side_effect=exc),
+        new=AsyncMock(side_effect=InvalidAuthError),
     ), patch(
-        "aioshelly.rpc_device.RpcDevice.create", new=AsyncMock(side_effect=exc)
+        "aioshelly.rpc_device.RpcDevice.create",
+        new=AsyncMock(side_effect=InvalidAuthError),
     ):
         result = await hass.config_entries.flow.async_init(
             DOMAIN,
@@ -889,11 +851,7 @@ async def test_reauth_unsuccessful(hass, test_data):
 
 @pytest.mark.parametrize(
     "error",
-    [
-        asyncio.TimeoutError,
-        aiohttp.ClientError,
-        aioshelly.exceptions.FirmwareUnsupported,
-    ],
+    [DeviceConnectionError, FirmwareUnsupported],
 )
 async def test_reauth_get_info_error(hass, error):
     """Test reauthentication flow failed with error in get_info()."""
-- 
GitLab


From 4e4682d2e03345ff72c35cda2edf87e8e822121c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Huryn?= <michalhuryn@gmail.com>
Date: Thu, 20 Oct 2022 14:35:28 +0200
Subject: [PATCH 629/985] Add blebox binary_sensor platform (#79535)

* Add binary_sensor platform, with test.

* Applied suggestions by @epenet

* refactor: as @epenet suggested, passing entity_description to init

* Update homeassistant/components/blebox/binary_sensor.py

@epenet suggestion, moved refactored logic of create_blebox_entities into BleBoxBinarySensorEntity init

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* refactor: as @epenet class selector and entity creation moved to binary_sensor

* refactor: list comprehension in entity list setup in binary sensor

* Update homeassistant/components/blebox/binary_sensor.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
---
 homeassistant/components/blebox/__init__.py   |  1 +
 .../components/blebox/binary_sensor.py        | 55 +++++++++++++++++++
 tests/components/blebox/test_binary_sensor.py | 45 +++++++++++++++
 3 files changed, 101 insertions(+)
 create mode 100644 homeassistant/components/blebox/binary_sensor.py
 create mode 100644 tests/components/blebox/test_binary_sensor.py

diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py
index 4314412a13d..1a7c8104652 100644
--- a/homeassistant/components/blebox/__init__.py
+++ b/homeassistant/components/blebox/__init__.py
@@ -19,6 +19,7 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT
 _LOGGER = logging.getLogger(__name__)
 
 PLATFORMS = [
+    Platform.BINARY_SENSOR,
     Platform.BUTTON,
     Platform.CLIMATE,
     Platform.COVER,
diff --git a/homeassistant/components/blebox/binary_sensor.py b/homeassistant/components/blebox/binary_sensor.py
new file mode 100644
index 00000000000..7eb6fd1e5a2
--- /dev/null
+++ b/homeassistant/components/blebox/binary_sensor.py
@@ -0,0 +1,55 @@
+"""BleBox binary sensor entities."""
+
+from blebox_uniapi.binary_sensor import BinarySensor as BinarySensorFeature
+from blebox_uniapi.box import Box
+
+from homeassistant.components.binary_sensor import (
+    BinarySensorDeviceClass,
+    BinarySensorEntity,
+    BinarySensorEntityDescription,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from . import DOMAIN, PRODUCT, BleBoxEntity
+
+BINARY_SENSOR_TYPES = (
+    BinarySensorEntityDescription(
+        key="moisture",
+        device_class=BinarySensorDeviceClass.MOISTURE,
+    ),
+)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up a BleBox entry."""
+
+    product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
+    entities = [
+        BleBoxBinarySensorEntity(feature, description)
+        for feature in product.features.get("binary_sensors", [])
+        for description in BINARY_SENSOR_TYPES
+        if description.key == feature.device_class
+    ]
+    async_add_entities(entities, True)
+
+
+class BleBoxBinarySensorEntity(BleBoxEntity[BinarySensorFeature], BinarySensorEntity):
+    """Representation of a BleBox binary sensor feature."""
+
+    def __init__(
+        self, feature: BinarySensorFeature, description: BinarySensorEntityDescription
+    ) -> None:
+        """Initialize a BleBox binary sensor feature."""
+        super().__init__(feature)
+        self.entity_description = description
+
+    @property
+    def is_on(self) -> bool:
+        """Return the state."""
+        return self._feature.state
diff --git a/tests/components/blebox/test_binary_sensor.py b/tests/components/blebox/test_binary_sensor.py
new file mode 100644
index 00000000000..36258198d91
--- /dev/null
+++ b/tests/components/blebox/test_binary_sensor.py
@@ -0,0 +1,45 @@
+"""Blebox binary_sensor entities test."""
+from unittest.mock import PropertyMock
+
+import blebox_uniapi
+import pytest
+
+from homeassistant.components.binary_sensor import BinarySensorDeviceClass
+from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON
+from homeassistant.helpers import device_registry as dr
+
+from .conftest import async_setup_entity, mock_feature
+
+
+@pytest.fixture(name="rainsensor")
+def airsensor_fixture():
+    """Return a default air quality fixture."""
+    feature = mock_feature(
+        "binary_sensors",
+        blebox_uniapi.binary_sensor.Rain,
+        unique_id="BleBox-windRainSensor-ea68e74f4f49-0.rain",
+        full_name="windRainSensor-0.rain",
+        device_class="moisture",
+    )
+    product = feature.product
+    type(product).name = PropertyMock(return_value="My rain sensor")
+    type(product).model = PropertyMock(return_value="rainSensor")
+    return (feature, "binary_sensor.windrainsensor_0_rain")
+
+
+async def test_init(rainsensor, hass, config):
+    """Test binary_sensor initialisation."""
+    _, entity_id = rainsensor
+    entry = await async_setup_entity(hass, config, entity_id)
+    assert entry.unique_id == "BleBox-windRainSensor-ea68e74f4f49-0.rain"
+
+    state = hass.states.get(entity_id)
+    assert state.name == "windRainSensor-0.rain"
+
+    assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOISTURE
+    assert state.state == STATE_ON
+
+    device_registry = dr.async_get(hass)
+    device = device_registry.async_get(entry.device_id)
+
+    assert device.name == "My rain sensor"
-- 
GitLab


From e84e5f134ee6ccd04ad098a16c41dd2ed141371c Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 20 Oct 2022 15:42:23 +0200
Subject: [PATCH 630/985] Use US_CUSTOMARY_SYSTEM in tests (#80658)

* Use US_CUSTOMARY_SYSTEM in tests

* Don't update test_unit_system
---
 tests/components/accuweather/test_sensor.py   |  4 +-
 tests/components/alexa/test_smart_home.py     |  4 +-
 .../bmw_connected_drive/test_sensor.py        |  2 +-
 tests/components/demo/test_init.py            |  4 +-
 tests/components/demo/test_water_heater.py    |  4 +-
 tests/components/gdacs/test_geo_location.py   |  4 +-
 .../generic_thermostat/test_climate.py        |  4 +-
 .../geonetnz_quakes/test_geo_location.py      |  4 +-
 .../geonetnz_volcano/test_sensor.py           |  4 +-
 .../google_travel_time/test_sensor.py         |  8 ++-
 .../here_travel_time/test_config_flow.py      |  8 ++-
 tests/components/mazda/test_sensor.py         |  4 +-
 tests/components/mobile_app/test_sensor.py    |  8 +--
 tests/components/mysensors/test_sensor.py     |  8 ++-
 tests/components/number/test_init.py          |  6 +-
 tests/components/nws/test_sensor.py           |  4 +-
 tests/components/nws/test_weather.py          |  4 +-
 .../components/recorder/test_websocket_api.py | 58 +++++++++++++-----
 tests/components/sensor/test_init.py          |  6 +-
 tests/components/sensor/test_recorder.py      | 60 +++++++++++--------
 tests/components/subaru/test_sensor.py        |  4 +-
 tests/components/tomorrowio/test_sensor.py    |  4 +-
 tests/components/weather/test_init.py         | 24 ++++----
 23 files changed, 147 insertions(+), 93 deletions(-)

diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py
index ababaea5443..aba5c4437ed 100644
--- a/tests/components/accuweather/test_sensor.py
+++ b/tests/components/accuweather/test_sensor.py
@@ -30,7 +30,7 @@ from homeassistant.const import (
 from homeassistant.helpers import entity_registry as er
 from homeassistant.setup import async_setup_component
 from homeassistant.util.dt import utcnow
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import init_integration
 
@@ -704,7 +704,7 @@ async def test_manual_update_entity(hass):
 
 async def test_sensor_imperial_units(hass):
     """Test states of the sensor without forecast."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     await init_integration(hass)
 
     state = hass.states.get("sensor.home_cloud_ceiling")
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 9d329e76c45..e2ae8741f20 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -30,7 +30,7 @@ from homeassistant.const import STATE_UNKNOWN, TEMP_FAHRENHEIT
 from homeassistant.core import Context
 from homeassistant.helpers import entityfilter
 from homeassistant.setup import async_setup_component
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .test_common import (
     MockConfig,
@@ -2020,7 +2020,7 @@ async def test_unknown_sensor(hass):
 
 async def test_thermostat(hass):
     """Test thermostat discovery."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     device = (
         "climate.test_thermostat",
         "cool",
diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py
index 90b3f725a6d..f1b0011656f 100644
--- a/tests/components/bmw_connected_drive/test_sensor.py
+++ b/tests/components/bmw_connected_drive/test_sensor.py
@@ -3,8 +3,8 @@ import pytest
 
 from homeassistant.core import HomeAssistant
 from homeassistant.util.unit_system import (
-    IMPERIAL_SYSTEM as IMPERIAL,
     METRIC_SYSTEM as METRIC,
+    US_CUSTOMARY_SYSTEM as IMPERIAL,
     UnitSystem,
 )
 
diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py
index 1bec552a55f..c1b9d4c436e 100644
--- a/tests/components/demo/test_init.py
+++ b/tests/components/demo/test_init.py
@@ -17,7 +17,7 @@ from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
 from homeassistant.helpers.json import JSONEncoder
 from homeassistant.setup import async_setup_component
 import homeassistant.util.dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from tests.components.recorder.common import async_wait_recording_done
 
@@ -84,7 +84,7 @@ async def test_demo_statistics(recorder_mock, mock_history, hass):
 
 async def test_demo_statistics_growth(recorder_mock, mock_history, hass):
     """Test that the demo sum statistics adds to the previous state."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
 
     now = dt_util.now()
     last_week = now - datetime.timedelta(days=7)
diff --git a/tests/components/demo/test_water_heater.py b/tests/components/demo/test_water_heater.py
index a6b4d999ddb..75103fca920 100644
--- a/tests/components/demo/test_water_heater.py
+++ b/tests/components/demo/test_water_heater.py
@@ -4,7 +4,7 @@ import voluptuous as vol
 
 from homeassistant.components import water_heater
 from homeassistant.setup import async_setup_component
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from tests.components.water_heater import common
 
@@ -15,7 +15,7 @@ ENTITY_WATER_HEATER_CELSIUS = "water_heater.demo_water_heater_celsius"
 @pytest.fixture(autouse=True)
 async def setup_comp(hass):
     """Set up demo component."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     assert await async_setup_component(
         hass, water_heater.DOMAIN, {"water_heater": {"platform": "demo"}}
     )
diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py
index d55141ebe56..83b9efde1e8 100644
--- a/tests/components/gdacs/test_geo_location.py
+++ b/tests/components/gdacs/test_geo_location.py
@@ -34,7 +34,7 @@ from homeassistant.const import (
 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 IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import _generate_mock_feed_entry
 
@@ -208,7 +208,7 @@ async def test_setup(hass):
 
 async def test_setup_imperial(hass):
     """Test the setup of the integration using imperial unit system."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     # Set up some mock feed entries for this test.
     mock_entry_1 = _generate_mock_feed_entry(
         "1234",
diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py
index f6a8795a29d..52714ab15a2 100644
--- a/tests/components/generic_thermostat/test_climate.py
+++ b/tests/components/generic_thermostat/test_climate.py
@@ -37,7 +37,7 @@ from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback
 from homeassistant.helpers import entity_registry as er
 from homeassistant.setup import async_setup_component
 from homeassistant.util import dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from tests.common import (
     assert_setup_component,
@@ -1201,7 +1201,7 @@ async def setup_comp_9(hass):
 
 async def test_precision(hass, setup_comp_9):
     """Test that setting precision to tenths works as intended."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     await common.async_set_temperature(hass, 23.27)
     state = hass.states.get(ENTITY)
     assert state.attributes.get("temperature") == 23.3
diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py
index 130964b4eeb..327829d3d4b 100644
--- a/tests/components/geonetnz_quakes/test_geo_location.py
+++ b/tests/components/geonetnz_quakes/test_geo_location.py
@@ -28,7 +28,7 @@ from homeassistant.const import (
 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 IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import _generate_mock_feed_entry
 
@@ -171,7 +171,7 @@ async def test_setup(hass):
 
 async def test_setup_imperial(hass):
     """Test the setup of the integration using imperial unit system."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     # Set up some mock feed entries for this test.
     mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 15.5, (38.0, -3.0))
 
diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py
index dbb6834c596..9b53cb9cc9b 100644
--- a/tests/components/geonetnz_volcano/test_sensor.py
+++ b/tests/components/geonetnz_volcano/test_sensor.py
@@ -23,7 +23,7 @@ from homeassistant.const import (
 )
 from homeassistant.setup import async_setup_component
 import homeassistant.util.dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import _generate_mock_feed_entry
 
@@ -150,7 +150,7 @@ async def test_setup(hass):
 
 async def test_setup_imperial(hass):
     """Test the setup of the integration using imperial unit system."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     # Set up some mock feed entries for this test.
     mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 1, 15.5, (38.0, -3.0))
 
diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py
index 73bea673880..8d560d895f2 100644
--- a/tests/components/google_travel_time/test_sensor.py
+++ b/tests/components/google_travel_time/test_sensor.py
@@ -12,7 +12,11 @@ from homeassistant.components.google_travel_time.const import (
 )
 from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
 from homeassistant.core import HomeAssistant
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
+from homeassistant.util.unit_system import (
+    METRIC_SYSTEM,
+    US_CUSTOMARY_SYSTEM,
+    UnitSystem,
+)
 
 from .const import MOCK_CONFIG
 
@@ -227,7 +231,7 @@ async def test_sensor_deprecation_warning(hass, caplog):
     "unit_system, expected_unit_option",
     [
         (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC),
-        (IMPERIAL_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
+        (US_CUSTOMARY_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
     ],
 )
 async def test_sensor_unit_system(
diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py
index 1777258c6e9..120ffd828bc 100644
--- a/tests/components/here_travel_time/test_config_flow.py
+++ b/tests/components/here_travel_time/test_config_flow.py
@@ -31,7 +31,11 @@ from homeassistant.const import (
     CONF_UNIT_SYSTEM_METRIC,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
+from homeassistant.util.unit_system import (
+    METRIC_SYSTEM,
+    US_CUSTOMARY_SYSTEM,
+    UnitSystem,
+)
 
 from .const import (
     API_KEY,
@@ -232,7 +236,7 @@ async def test_step_destination_coordinates(
     "unit_system, expected_unit_option",
     [
         (METRIC_SYSTEM, CONF_UNIT_SYSTEM_METRIC),
-        (IMPERIAL_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
+        (US_CUSTOMARY_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL),
     ],
 )
 async def test_step_destination_entity(
diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py
index 2284101fa84..4484e6b7783 100644
--- a/tests/components/mazda/test_sensor.py
+++ b/tests/components/mazda/test_sensor.py
@@ -16,7 +16,7 @@ from homeassistant.const import (
     PRESSURE_PSI,
 )
 from homeassistant.helpers import entity_registry as er
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from . import init_integration
 
@@ -132,7 +132,7 @@ async def test_sensors(hass):
 
 async def test_sensors_imperial_units(hass):
     """Test that the sensors work properly with imperial units."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
 
     await init_integration(hass)
 
diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py
index 930fb522c4c..13e11db3eff 100644
--- a/tests/components/mobile_app/test_sensor.py
+++ b/tests/components/mobile_app/test_sensor.py
@@ -13,14 +13,14 @@ from homeassistant.const import (
     TEMP_FAHRENHEIT,
 )
 from homeassistant.helpers import device_registry as dr, entity_registry as er
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 
 @pytest.mark.parametrize(
     "unit_system, state_unit, state1, state2",
     (
         (METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"),
-        (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
+        (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
     ),
 )
 async def test_sensor(
@@ -124,9 +124,9 @@ async def test_sensor(
     "unique_id, unit_system, state_unit, state1, state2",
     (
         ("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"),
-        ("battery_temperature", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
+        ("battery_temperature", US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "253"),
         # The unique_id doesn't match that of the mobile app's battery temperature sensor
-        ("battery_temp", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "123"),
+        ("battery_temp", US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "123"),
     ),
 )
 async def test_sensor_migration(
diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py
index 58258682d5b..3a1b7b56872 100644
--- a/tests/components/mysensors/test_sensor.py
+++ b/tests/components/mysensors/test_sensor.py
@@ -21,7 +21,11 @@ from homeassistant.const import (
     TEMP_FAHRENHEIT,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
+from homeassistant.util.unit_system import (
+    METRIC_SYSTEM,
+    US_CUSTOMARY_SYSTEM,
+    UnitSystem,
+)
 
 from tests.common import MockConfigEntry
 
@@ -124,7 +128,7 @@ async def test_distance_sensor(
 
 @pytest.mark.parametrize(
     "unit_system, unit",
-    [(METRIC_SYSTEM, TEMP_CELSIUS), (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT)],
+    [(METRIC_SYSTEM, TEMP_CELSIUS), (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT)],
 )
 async def test_temperature_sensor(
     hass: HomeAssistant,
diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py
index 8d7f8a91ae8..98b30616952 100644
--- a/tests/components/number/test_init.py
+++ b/tests/components/number/test_init.py
@@ -25,7 +25,7 @@ from homeassistant.core import HomeAssistant, State
 from homeassistant.helpers import entity_registry as er
 from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY
 from homeassistant.setup import async_setup_component
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from tests.common import mock_restore_cache_with_extra_data
 
@@ -435,7 +435,7 @@ async def test_deprecated_methods(
     "native_min_value, state_min_value, native_step, state_step",
     [
         (
-            IMPERIAL_SYSTEM,
+            US_CUSTOMARY_SYSTEM,
             TEMP_FAHRENHEIT,
             TEMP_FAHRENHEIT,
             100,
@@ -450,7 +450,7 @@ async def test_deprecated_methods(
             3,
         ),
         (
-            IMPERIAL_SYSTEM,
+            US_CUSTOMARY_SYSTEM,
             TEMP_CELSIUS,
             TEMP_FAHRENHEIT,
             38,
diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py
index 78d39575522..9597618ccc8 100644
--- a/tests/components/nws/test_sensor.py
+++ b/tests/components/nws/test_sensor.py
@@ -6,7 +6,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
 from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN
 from homeassistant.helpers import entity_registry as er
 from homeassistant.util import slugify
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from .const import (
     EXPECTED_FORECAST_IMPERIAL,
@@ -24,7 +24,7 @@ from tests.common import MockConfigEntry
     "units,result_observation,result_forecast",
     [
         (
-            IMPERIAL_SYSTEM,
+            US_CUSTOMARY_SYSTEM,
             SENSOR_EXPECTED_OBSERVATION_IMPERIAL,
             EXPECTED_FORECAST_IMPERIAL,
         ),
diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py
index 3f3e9a649f3..b3a9a4bc9f1 100644
--- a/tests/components/nws/test_weather.py
+++ b/tests/components/nws/test_weather.py
@@ -15,7 +15,7 @@ from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
 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 IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from .const import (
     EXPECTED_FORECAST_IMPERIAL,
@@ -34,7 +34,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed
     "units,result_observation,result_forecast",
     [
         (
-            IMPERIAL_SYSTEM,
+            US_CUSTOMARY_SYSTEM,
             WEATHER_EXPECTED_OBSERVATION_IMPERIAL,
             EXPECTED_FORECAST_IMPERIAL,
         ),
diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py
index eeffc7ea7dd..58cf0e5c663 100644
--- a/tests/components/recorder/test_websocket_api.py
+++ b/tests/components/recorder/test_websocket_api.py
@@ -19,7 +19,7 @@ from homeassistant.components.recorder.statistics import (
 from homeassistant.helpers import recorder as recorder_helper
 from homeassistant.setup import async_setup_component
 import homeassistant.util.dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from .common import (
     async_recorder_block_till_done,
@@ -126,7 +126,7 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client):
     """Test statistics_during_period."""
     now = dt_util.utcnow()
 
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     await async_setup_component(hass, "sensor", {})
     await async_recorder_block_till_done(hass)
     hass.states.async_set("sensor.test", 10, attributes=POWER_SENSOR_KW_ATTRIBUTES)
@@ -431,7 +431,7 @@ async def test_statistics_during_period_in_the_past(
     hass.config.set_time_zone("UTC")
     now = dt_util.utcnow().replace()
 
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     await async_setup_component(hass, "sensor", {})
     await async_recorder_block_till_done(hass)
 
@@ -589,27 +589,57 @@ async def test_statistics_during_period_bad_end_time(
 @pytest.mark.parametrize(
     "units, attributes, display_unit, statistics_unit, unit_class",
     [
-        (IMPERIAL_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"),
+        (US_CUSTOMARY_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"),
         (METRIC_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"),
-        (IMPERIAL_SYSTEM, DISTANCE_SENSOR_FT_ATTRIBUTES, "ft", "ft", "distance"),
+        (
+            US_CUSTOMARY_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"),
+        (US_CUSTOMARY_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"),
+        (US_CUSTOMARY_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"),
+        (US_CUSTOMARY_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"),
+        (
+            US_CUSTOMARY_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"),
+        (US_CUSTOMARY_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"),
+        (
+            US_CUSTOMARY_SYSTEM,
+            TEMPERATURE_SENSOR_C_ATTRIBUTES,
+            "°C",
+            "°C",
+            "temperature",
+        ),
         (METRIC_SYSTEM, TEMPERATURE_SENSOR_C_ATTRIBUTES, "°C", "°C", "temperature"),
-        (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_F_ATTRIBUTES, "°F", "°F", "temperature"),
+        (
+            US_CUSTOMARY_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"),
+        (US_CUSTOMARY_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"),
+        (
+            US_CUSTOMARY_SYSTEM,
+            VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL,
+            "ft³",
+            "ft³",
+            "volume",
+        ),
         (METRIC_SYSTEM, VOLUME_SENSOR_FT3_ATTRIBUTES_TOTAL, "ft³", "ft³", "volume"),
     ],
 )
diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py
index a9ea9ce0fbe..0fe60ef98c7 100644
--- a/tests/components/sensor/test_init.py
+++ b/tests/components/sensor/test_init.py
@@ -35,7 +35,7 @@ from homeassistant.helpers import entity_registry as er
 from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY
 from homeassistant.setup import async_setup_component
 from homeassistant.util import dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from tests.common import mock_restore_cache_with_extra_data
 
@@ -43,8 +43,8 @@ from tests.common import mock_restore_cache_with_extra_data
 @pytest.mark.parametrize(
     "unit_system,native_unit,state_unit,native_value,state_value",
     [
-        (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT, 100, 100),
-        (IMPERIAL_SYSTEM, TEMP_CELSIUS, TEMP_FAHRENHEIT, 38, 100),
+        (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT, 100, 100),
+        (US_CUSTOMARY_SYSTEM, TEMP_CELSIUS, TEMP_FAHRENHEIT, 38, 100),
         (METRIC_SYSTEM, TEMP_FAHRENHEIT, TEMP_CELSIUS, 100, 38),
         (METRIC_SYSTEM, TEMP_CELSIUS, TEMP_CELSIUS, 38, 38),
     ],
diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py
index a49882a0eb3..e04d41f4c2a 100644
--- a/tests/components/sensor/test_recorder.py
+++ b/tests/components/sensor/test_recorder.py
@@ -26,7 +26,7 @@ from homeassistant.components.recorder.util import get_instance, session_scope
 from homeassistant.const import STATE_UNAVAILABLE
 from homeassistant.setup import async_setup_component, setup_component
 import homeassistant.util.dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from tests.components.recorder.common import (
     async_recorder_block_till_done,
@@ -403,18 +403,18 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes)
 @pytest.mark.parametrize(
     "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", "mi", "distance", 1),
-        (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1),
-        (IMPERIAL_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1),
-        (IMPERIAL_SYSTEM, "gas", "m³", "m³", "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³", "ft³", "volume", 1),
-        (IMPERIAL_SYSTEM, "weight", "g", "g", "g", "mass", 1),
-        (IMPERIAL_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1),
+        (US_CUSTOMARY_SYSTEM, "distance", "m", "m", "m", "distance", 1),
+        (US_CUSTOMARY_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1),
+        (US_CUSTOMARY_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1),
+        (US_CUSTOMARY_SYSTEM, "energy", "Wh", "Wh", "Wh", "energy", 1),
+        (US_CUSTOMARY_SYSTEM, "gas", "m³", "m³", "m³", "volume", 1),
+        (US_CUSTOMARY_SYSTEM, "gas", "ft³", "ft³", "ft³", "volume", 1),
+        (US_CUSTOMARY_SYSTEM, "monetary", "EUR", "EUR", "EUR", None, 1),
+        (US_CUSTOMARY_SYSTEM, "monetary", "SEK", "SEK", "SEK", None, 1),
+        (US_CUSTOMARY_SYSTEM, "volume", "m³", "m³", "m³", "volume", 1),
+        (US_CUSTOMARY_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1),
+        (US_CUSTOMARY_SYSTEM, "weight", "g", "g", "g", "mass", 1),
+        (US_CUSTOMARY_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1),
         (METRIC_SYSTEM, "distance", "m", "m", "m", "distance", 1),
         (METRIC_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1),
         (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1),
@@ -3307,12 +3307,18 @@ def record_states(hass, zero, entity_id, attributes, seq=None):
 @pytest.mark.parametrize(
     "units, attributes, unit, unit2, supported_unit",
     [
-        (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"),
+        (US_CUSTOMARY_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"),
+        (
+            US_CUSTOMARY_SYSTEM,
+            TEMPERATURE_SENSOR_ATTRIBUTES,
+            "°F",
+            "K",
+            "K, °C, °F",
+        ),
         (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"),
         (
-            IMPERIAL_SYSTEM,
+            US_CUSTOMARY_SYSTEM,
             PRESSURE_SENSOR_ATTRIBUTES,
             "psi",
             "bar",
@@ -3439,7 +3445,7 @@ async def test_validate_unit_change_convertible(
 @pytest.mark.parametrize(
     "units, attributes",
     [
-        (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES),
+        (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES),
     ],
 )
 async def test_validate_statistics_unit_ignore_device_class(
@@ -3493,12 +3499,18 @@ async def test_validate_statistics_unit_ignore_device_class(
 @pytest.mark.parametrize(
     "units, attributes, unit, unit2, supported_unit",
     [
-        (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "W, kW"),
+        (US_CUSTOMARY_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"),
+        (
+            US_CUSTOMARY_SYSTEM,
+            TEMPERATURE_SENSOR_ATTRIBUTES,
+            "°F",
+            "K",
+            "K, °C, °F",
+        ),
         (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "K", "K, °C, °F"),
         (
-            IMPERIAL_SYSTEM,
+            US_CUSTOMARY_SYSTEM,
             PRESSURE_SENSOR_ATTRIBUTES,
             "psi",
             "bar",
@@ -3625,7 +3637,7 @@ async def test_validate_statistics_unit_change_no_device_class(
 @pytest.mark.parametrize(
     "units, attributes, unit",
     [
-        (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
+        (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
     ],
 )
 async def test_validate_statistics_unsupported_state_class(
@@ -3689,7 +3701,7 @@ async def test_validate_statistics_unsupported_state_class(
 @pytest.mark.parametrize(
     "units, attributes, unit",
     [
-        (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
+        (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
     ],
 )
 async def test_validate_statistics_sensor_no_longer_recorded(
@@ -3750,7 +3762,7 @@ async def test_validate_statistics_sensor_no_longer_recorded(
 @pytest.mark.parametrize(
     "units, attributes, unit",
     [
-        (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
+        (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
     ],
 )
 async def test_validate_statistics_sensor_not_recorded(
@@ -3808,7 +3820,7 @@ async def test_validate_statistics_sensor_not_recorded(
 @pytest.mark.parametrize(
     "units, attributes, unit",
     [
-        (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
+        (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
     ],
 )
 async def test_validate_statistics_sensor_removed(
diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py
index 3f0cb773461..aec9e6ede7a 100644
--- a/tests/components/subaru/test_sensor.py
+++ b/tests/components/subaru/test_sensor.py
@@ -7,7 +7,7 @@ from homeassistant.components.subaru.sensor import (
     SAFETY_SENSORS,
 )
 from homeassistant.util import slugify
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .api_responses import (
     EXPECTED_STATE_EV_IMPERIAL,
@@ -25,7 +25,7 @@ from .conftest import (
 
 async def test_sensors_ev_imperial(hass, ev_entry):
     """Test sensors supporting imperial units."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
 
     with patch(MOCK_API_FETCH), patch(
         MOCK_API_GET_DATA, return_value=VEHICLE_STATUS_EV
diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py
index 51b3db00c6e..7721e5d36ac 100644
--- a/tests/components/tomorrowio/test_sensor.py
+++ b/tests/components/tomorrowio/test_sensor.py
@@ -25,7 +25,7 @@ from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
 from homeassistant.core import HomeAssistant, State, callback
 from homeassistant.helpers.entity_registry import async_get
 from homeassistant.util import dt as dt_util
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import API_V4_ENTRY_DATA
 
@@ -172,7 +172,7 @@ async def test_v4_sensor(hass: HomeAssistant) -> None:
 
 async def test_v4_sensor_imperial(hass: HomeAssistant) -> None:
     """Test v4 sensor data."""
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
     await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA)
     check_sensor_state(hass, O3, "91.35")
     check_sensor_state(hass, CO, "0.0")
diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py
index 814d3b7857c..4b4f3a82d07 100644
--- a/tests/components/weather/test_init.py
+++ b/tests/components/weather/test_init.py
@@ -54,7 +54,7 @@ from homeassistant.util.distance import convert as convert_distance
 from homeassistant.util.pressure import convert as convert_pressure
 from homeassistant.util.speed import convert as convert_speed
 from homeassistant.util.temperature import convert as convert_temperature
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
+from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
 
 from tests.testing_config.custom_components.test import weather as WeatherPlatform
 
@@ -143,7 +143,7 @@ async def create_entity(hass: HomeAssistant, **kwargs):
 @pytest.mark.parametrize("native_unit", (TEMP_FAHRENHEIT, TEMP_CELSIUS))
 @pytest.mark.parametrize(
     "state_unit, unit_system",
-    ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)),
+    ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, US_CUSTOMARY_SYSTEM)),
 )
 async def test_temperature(
     hass: HomeAssistant,
@@ -176,7 +176,7 @@ async def test_temperature(
 @pytest.mark.parametrize("native_unit", (None,))
 @pytest.mark.parametrize(
     "state_unit, unit_system",
-    ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)),
+    ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, US_CUSTOMARY_SYSTEM)),
 )
 async def test_temperature_no_unit(
     hass: HomeAssistant,
@@ -209,7 +209,7 @@ async def test_temperature_no_unit(
 @pytest.mark.parametrize("native_unit", (PRESSURE_INHG, PRESSURE_INHG))
 @pytest.mark.parametrize(
     "state_unit, unit_system",
-    ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, IMPERIAL_SYSTEM)),
+    ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, US_CUSTOMARY_SYSTEM)),
 )
 async def test_pressure(
     hass: HomeAssistant,
@@ -237,7 +237,7 @@ async def test_pressure(
 @pytest.mark.parametrize("native_unit", (None,))
 @pytest.mark.parametrize(
     "state_unit, unit_system",
-    ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, IMPERIAL_SYSTEM)),
+    ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, US_CUSTOMARY_SYSTEM)),
 )
 async def test_pressure_no_unit(
     hass: HomeAssistant,
@@ -270,7 +270,7 @@ async def test_pressure_no_unit(
     "state_unit, unit_system",
     (
         (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM),
-        (SPEED_MILES_PER_HOUR, IMPERIAL_SYSTEM),
+        (SPEED_MILES_PER_HOUR, US_CUSTOMARY_SYSTEM),
     ),
 )
 async def test_wind_speed(
@@ -304,7 +304,7 @@ async def test_wind_speed(
     "state_unit, unit_system",
     (
         (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM),
-        (SPEED_MILES_PER_HOUR, IMPERIAL_SYSTEM),
+        (SPEED_MILES_PER_HOUR, US_CUSTOMARY_SYSTEM),
     ),
 )
 async def test_wind_speed_no_unit(
@@ -338,7 +338,7 @@ async def test_wind_speed_no_unit(
     "state_unit, unit_system",
     (
         (LENGTH_KILOMETERS, METRIC_SYSTEM),
-        (LENGTH_MILES, IMPERIAL_SYSTEM),
+        (LENGTH_MILES, US_CUSTOMARY_SYSTEM),
     ),
 )
 async def test_visibility(
@@ -369,7 +369,7 @@ async def test_visibility(
     "state_unit, unit_system",
     (
         (LENGTH_KILOMETERS, METRIC_SYSTEM),
-        (LENGTH_MILES, IMPERIAL_SYSTEM),
+        (LENGTH_MILES, US_CUSTOMARY_SYSTEM),
     ),
 )
 async def test_visibility_no_unit(
@@ -400,7 +400,7 @@ async def test_visibility_no_unit(
     "state_unit, unit_system",
     (
         (LENGTH_MILLIMETERS, METRIC_SYSTEM),
-        (LENGTH_INCHES, IMPERIAL_SYSTEM),
+        (LENGTH_INCHES, US_CUSTOMARY_SYSTEM),
     ),
 )
 async def test_precipitation(
@@ -431,7 +431,7 @@ async def test_precipitation(
     "state_unit, unit_system",
     (
         (LENGTH_MILLIMETERS, METRIC_SYSTEM),
-        (LENGTH_INCHES, IMPERIAL_SYSTEM),
+        (LENGTH_INCHES, US_CUSTOMARY_SYSTEM),
     ),
 )
 async def test_precipitation_no_unit(
@@ -719,7 +719,7 @@ async def test_backwards_compatibility_convert_values(
     precipitation_value = 1
     precipitation_unit = LENGTH_MILLIMETERS
 
-    hass.config.units = IMPERIAL_SYSTEM
+    hass.config.units = US_CUSTOMARY_SYSTEM
 
     platform: WeatherPlatform = getattr(hass.components, "test.weather")
     platform.init(empty=True)
-- 
GitLab


From 3f8362fe1c96cbdadbd0fd431bc89fe92c7426a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Huryn?= <michalhuryn@gmail.com>
Date: Thu, 20 Oct 2022 15:51:29 +0200
Subject: [PATCH 631/985] Refactor blebox sensors (#80671)

refactor: sensors, entity description pick moved to async_setup_entry, removed redundant dataclass
---
 homeassistant/components/blebox/sensor.py | 32 ++++++++++-------------
 1 file changed, 14 insertions(+), 18 deletions(-)

diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py
index 60d94507f4b..471f8c6eb86 100644
--- a/homeassistant/components/blebox/sensor.py
+++ b/homeassistant/components/blebox/sensor.py
@@ -1,5 +1,4 @@
 """BleBox sensor entities."""
-from dataclasses import dataclass
 
 from blebox_uniapi.box import Box
 import blebox_uniapi.sensor
@@ -17,29 +16,23 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from . import BleBoxEntity
 from .const import DOMAIN, PRODUCT
 
-
-@dataclass
-class BleboxSensorEntityDescription(SensorEntityDescription):
-    """Class describing Blebox sensor entities."""
-
-
 SENSOR_TYPES = (
-    BleboxSensorEntityDescription(
+    SensorEntityDescription(
         key="pm1",
         device_class=SensorDeviceClass.PM1,
         native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
     ),
-    BleboxSensorEntityDescription(
+    SensorEntityDescription(
         key="pm2_5",
         device_class=SensorDeviceClass.PM25,
         native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
     ),
-    BleboxSensorEntityDescription(
+    SensorEntityDescription(
         key="pm10",
         device_class=SensorDeviceClass.PM10,
         native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
     ),
-    BleboxSensorEntityDescription(
+    SensorEntityDescription(
         key="temperature",
         device_class=SensorDeviceClass.TEMPERATURE,
         native_unit_of_measurement=TEMP_CELSIUS,
@@ -55,7 +48,10 @@ async def async_setup_entry(
     """Set up a BleBox entry."""
     product: Box = hass.data[DOMAIN][config_entry.entry_id][PRODUCT]
     entities = [
-        BleBoxSensorEntity(feature) for feature in product.features.get("sensors", [])
+        BleBoxSensorEntity(feature, description)
+        for feature in product.features.get("sensors", [])
+        for description in SENSOR_TYPES
+        if description.key == feature.device_class
     ]
     async_add_entities(entities, True)
 
@@ -63,14 +59,14 @@ async def async_setup_entry(
 class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEntity):
     """Representation of a BleBox sensor feature."""
 
-    def __init__(self, feature: blebox_uniapi.sensor.BaseSensor) -> None:
+    def __init__(
+        self,
+        feature: blebox_uniapi.sensor.BaseSensor,
+        description: SensorEntityDescription,
+    ) -> None:
         """Initialize a BleBox sensor feature."""
         super().__init__(feature)
-
-        for description in SENSOR_TYPES:
-            if description.key == feature.device_class:
-                self.entity_description = description
-                break
+        self.entity_description = description
 
     @property
     def native_value(self):
-- 
GitLab


From eee1ede5bba4a95e33b0d9d901cbe6319a220779 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 20 Oct 2022 15:57:43 +0200
Subject: [PATCH 632/985] Add websocket type hints in lovelace (#80537)

---
 .../components/lovelace/websocket.py          | 47 +++++++++++++++----
 1 file changed, 39 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py
index 66ec7f22b09..423ba3117ea 100644
--- a/homeassistant/components/lovelace/websocket.py
+++ b/homeassistant/components/lovelace/websocket.py
@@ -1,23 +1,31 @@
 """Websocket API for Lovelace."""
+from __future__ import annotations
+
 from functools import wraps
+from typing import Any
 
 import voluptuous as vol
 
 from homeassistant.components import websocket_api
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers import config_validation as cv
 
 from .const import CONF_URL_PATH, DOMAIN, ConfigNotFound
+from .dashboard import LovelaceStorage
 
 
 def _handle_errors(func):
     """Handle error with WebSocket calls."""
 
     @wraps(func)
-    async def send_with_error_handling(hass, connection, msg):
+    async def send_with_error_handling(
+        hass: HomeAssistant,
+        connection: websocket_api.ActiveConnection,
+        msg: dict[str, Any],
+    ) -> None:
         url_path = msg.get(CONF_URL_PATH)
-        config = hass.data[DOMAIN]["dashboards"].get(url_path)
+        config: LovelaceStorage | None = hass.data[DOMAIN]["dashboards"].get(url_path)
 
         if config is None:
             connection.send_error(
@@ -44,7 +52,11 @@ def _handle_errors(func):
 
 @websocket_api.websocket_command({"type": "lovelace/resources"})
 @websocket_api.async_response
-async def websocket_lovelace_resources(hass, connection, msg):
+async def websocket_lovelace_resources(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Send Lovelace UI resources over WebSocket configuration."""
     resources = hass.data[DOMAIN]["resources"]
 
@@ -64,7 +76,12 @@ async def websocket_lovelace_resources(hass, connection, msg):
 )
 @websocket_api.async_response
 @_handle_errors
-async def websocket_lovelace_config(hass, connection, msg, config):
+async def websocket_lovelace_config(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+    config: LovelaceStorage,
+) -> None:
     """Send Lovelace UI config over WebSocket configuration."""
     return await config.async_load(msg["force"])
 
@@ -79,7 +96,12 @@ async def websocket_lovelace_config(hass, connection, msg, config):
 )
 @websocket_api.async_response
 @_handle_errors
-async def websocket_lovelace_save_config(hass, connection, msg, config):
+async def websocket_lovelace_save_config(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+    config: LovelaceStorage,
+) -> None:
     """Save Lovelace UI configuration."""
     await config.async_save(msg["config"])
 
@@ -93,14 +115,23 @@ async def websocket_lovelace_save_config(hass, connection, msg, config):
 )
 @websocket_api.async_response
 @_handle_errors
-async def websocket_lovelace_delete_config(hass, connection, msg, config):
+async def websocket_lovelace_delete_config(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+    config: LovelaceStorage,
+) -> None:
     """Delete Lovelace UI configuration."""
     await config.async_delete()
 
 
 @websocket_api.websocket_command({"type": "lovelace/dashboards/list"})
 @callback
-def websocket_lovelace_dashboards(hass, connection, msg):
+def websocket_lovelace_dashboards(
+    hass: HomeAssistant,
+    connection: websocket_api.ActiveConnection,
+    msg: dict[str, Any],
+) -> None:
     """Delete Lovelace UI configuration."""
     connection.send_result(
         msg["id"],
-- 
GitLab


From 884f8d6e2c29cec093d8f9957b6954aa1a711d3e Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 20 Oct 2022 15:58:22 +0200
Subject: [PATCH 633/985] Adjust device classes in tasmota (#79282)

* Adjust device classes in tasmota

* Remove incorrect device class
---
 homeassistant/components/tasmota/sensor.py | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py
index 1878e2794f3..09b44eca1ee 100644
--- a/homeassistant/components/tasmota/sensor.py
+++ b/homeassistant/components/tasmota/sensor.py
@@ -52,7 +52,8 @@ DEVICE_CLASS = "device_class"
 STATE_CLASS = "state_class"
 ICON = "icon"
 
-# A Tasmota sensor type may be mapped to either a device class or an icon, not both
+# A Tasmota sensor type may be mapped to either a device class or an icon,
+# both can only be set if the default device class icon is not appropriate
 SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
     hc.SENSOR_ACTIVE_ENERGYEXPORT: {
         DEVICE_CLASS: SensorDeviceClass.ENERGY,
@@ -91,6 +92,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
     hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"},
     hc.SENSOR_CURRENT: {
         ICON: "mdi:alpha-a-circle-outline",
+        DEVICE_CLASS: SensorDeviceClass.CURRENT,
         STATE_CLASS: SensorStateClass.MEASUREMENT,
     },
     hc.SENSOR_CURRENTNEUTRAL: {
@@ -105,6 +107,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
     },
     hc.SENSOR_DISTANCE: {
         ICON: "mdi:leak",
+        DEVICE_CLASS: SensorDeviceClass.DISTANCE,
         STATE_CLASS: SensorStateClass.MEASUREMENT,
     },
     hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"},
@@ -122,7 +125,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
     },
     hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"},
     hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"},
-    hc.SENSOR_MOISTURE: {ICON: "mdi:cup-water"},
+    hc.SENSOR_MOISTURE: {
+        DEVICE_CLASS: SensorDeviceClass.MOISTURE,
+        ICON: "mdi:cup-water",
+    },
     hc.SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"},
     hc.SENSOR_PB0_3: {ICON: "mdi:flask"},
     hc.SENSOR_PB0_5: {ICON: "mdi:flask"},
@@ -144,6 +150,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
     },
     hc.SENSOR_POWERFACTOR: {
         ICON: "mdi:alpha-f-circle-outline",
+        DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR,
         STATE_CLASS: SensorStateClass.MEASUREMENT,
     },
     hc.SENSOR_POWERUSAGE: {
@@ -158,7 +165,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
         DEVICE_CLASS: SensorDeviceClass.PRESSURE,
         STATE_CLASS: SensorStateClass.MEASUREMENT,
     },
-    hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"},
+    hc.SENSOR_PROXIMITY: {DEVICE_CLASS: SensorDeviceClass.DISTANCE, ICON: "mdi:ruler"},
     hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL},
     hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL},
     hc.SENSOR_REACTIVE_POWERUSAGE: {
@@ -194,7 +201,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
         ICON: "mdi:alpha-v-circle-outline",
         STATE_CLASS: SensorStateClass.MEASUREMENT,
     },
-    hc.SENSOR_WEIGHT: {ICON: "mdi:scale", STATE_CLASS: SensorStateClass.MEASUREMENT},
+    hc.SENSOR_WEIGHT: {
+        ICON: "mdi:scale",
+        DEVICE_CLASS: SensorDeviceClass.WEIGHT,
+        STATE_CLASS: SensorStateClass.MEASUREMENT,
+    },
     hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY},
 }
 
-- 
GitLab


From da14acb3b6ad7430aae980fa5aca87ff2b79271e Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 20 Oct 2022 09:35:09 -0500
Subject: [PATCH 634/985] Speed up restart when bluetooth adapter is in a
 failed state (#80640)

---
 homeassistant/components/bluetooth/scanner.py | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py
index 642bf13f7cd..8d4adfe9a3d 100644
--- a/homeassistant/components/bluetooth/scanner.py
+++ b/homeassistant/components/bluetooth/scanner.py
@@ -385,15 +385,11 @@ class HaScanner(BaseHaScanner):
 
     async def async_stop(self) -> None:
         """Stop bluetooth scanner."""
-        async with self._start_stop_lock:
-            await self._async_stop()
-
-    async def _async_stop(self) -> None:
-        """Cancel watchdog and bluetooth discovery under the lock."""
         if self._cancel_watchdog:
             self._cancel_watchdog()
             self._cancel_watchdog = None
-        await self._async_stop_scanner()
+        async with self._start_stop_lock:
+            await self._async_stop_scanner()
 
     async def _async_stop_scanner(self) -> None:
         """Stop bluetooth discovery under the lock."""
-- 
GitLab


From 8dd2d6f825c589f1a3b11440d137b00f6e05d107 Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
Date: Thu, 20 Oct 2022 17:17:21 +0200
Subject: [PATCH 635/985] Remove deprecated yaml import for waze_travel_time
 (#80669)

Remove deprecated yaml import

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>
---
 .../waze_travel_time/config_flow.py           |  2 -
 .../waze_travel_time/test_config_flow.py      | 41 -------------------
 2 files changed, 43 deletions(-)

diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py
index 45aeada2a7a..dee133e0513 100644
--- a/homeassistant/components/waze_travel_time/config_flow.py
+++ b/homeassistant/components/waze_travel_time/config_flow.py
@@ -134,5 +134,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
             ),
             errors=errors,
         )
-
-    async_step_import = async_step_user
diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py
index bc343792218..34ea985888d 100644
--- a/tests/components/waze_travel_time/test_config_flow.py
+++ b/tests/components/waze_travel_time/test_config_flow.py
@@ -101,47 +101,6 @@ async def test_options(hass):
     }
 
 
-@pytest.mark.usefixtures("validate_config_entry")
-async def test_import(hass):
-    """Test import for config flow."""
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN,
-        context={"source": config_entries.SOURCE_IMPORT},
-        data={
-            CONF_ORIGIN: "location1",
-            CONF_DESTINATION: "location2",
-            CONF_REGION: "US",
-            CONF_AVOID_FERRIES: True,
-            CONF_AVOID_SUBSCRIPTION_ROADS: True,
-            CONF_AVOID_TOLL_ROADS: True,
-            CONF_EXCL_FILTER: "exclude",
-            CONF_INCL_FILTER: "include",
-            CONF_REALTIME: False,
-            CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
-            CONF_VEHICLE_TYPE: "taxi",
-        },
-    )
-
-    assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
-    await hass.async_block_till_done()
-    entry = hass.config_entries.async_entries(DOMAIN)[0]
-    assert entry.data == {
-        CONF_ORIGIN: "location1",
-        CONF_DESTINATION: "location2",
-        CONF_REGION: "US",
-    }
-    assert entry.options == {
-        CONF_AVOID_FERRIES: True,
-        CONF_AVOID_SUBSCRIPTION_ROADS: True,
-        CONF_AVOID_TOLL_ROADS: True,
-        CONF_EXCL_FILTER: "exclude",
-        CONF_INCL_FILTER: "include",
-        CONF_REALTIME: False,
-        CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
-        CONF_VEHICLE_TYPE: "taxi",
-    }
-
-
 @pytest.mark.usefixtures("validate_config_entry")
 async def test_dupe(hass):
     """Test setting up the same entry data twice is OK."""
-- 
GitLab


From a20ac2b24615eb5ac92398d9839db0b7d8a2a99a Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
Date: Thu, 20 Oct 2022 12:09:16 -0400
Subject: [PATCH 636/985] Remove balloob as code owner Ring (#80680)

---
 CODEOWNERS                                  | 2 --
 homeassistant/components/ring/manifest.json | 2 +-
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index 60bc4d99a8d..c9e42a8becf 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -932,8 +932,6 @@ build.json @home-assistant/supervisor
 /tests/components/rhasspy/ @balloob @synesthesiam
 /homeassistant/components/ridwell/ @bachya
 /tests/components/ridwell/ @bachya
-/homeassistant/components/ring/ @balloob
-/tests/components/ring/ @balloob
 /homeassistant/components/risco/ @OnFreund
 /tests/components/risco/ @OnFreund
 /homeassistant/components/rituals_perfume_genie/ @milanmeu
diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json
index a64411e610f..25e478d60ae 100644
--- a/homeassistant/components/ring/manifest.json
+++ b/homeassistant/components/ring/manifest.json
@@ -4,7 +4,7 @@
   "documentation": "https://www.home-assistant.io/integrations/ring",
   "requirements": ["ring_doorbell==0.7.2"],
   "dependencies": ["ffmpeg"],
-  "codeowners": ["@balloob"],
+  "codeowners": [],
   "config_flow": true,
   "dhcp": [
     {
-- 
GitLab


From e510dd64f1936b3f8cd4eb4f422447676a58bfe5 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Thu, 20 Oct 2022 18:30:00 +0200
Subject: [PATCH 637/985] Pin uamqp==1.6.0 (#80678)

---
 homeassistant/package_constraints.txt | 3 +++
 script/gen_requirements_all.py        | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index dddaec8712f..483f8e6d9ee 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -131,3 +131,6 @@ iso4217!=1.10.20220401
 
 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10
 pandas==1.4.3
+
+# uamqp 1.6.1, has 1 failing test during built on armv7/armhf
+uamqp==1.6.0
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index dc55b37b956..bbc970f9178 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -141,6 +141,9 @@ iso4217!=1.10.20220401
 
 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10
 pandas==1.4.3
+
+# uamqp 1.6.1, has 1 failing test during built on armv7/armhf
+uamqp==1.6.0
 """
 
 IGNORE_PRE_COMMIT_HOOK_ID = (
-- 
GitLab


From 5cb8749ae32a694f6b766bcc4423c33fee2f57da Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
Date: Thu, 20 Oct 2022 13:25:24 -0400
Subject: [PATCH 638/985] Fix custom components not working with integration
 descriptions (#80686)

---
 homeassistant/loader.py | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index e90d0b42036..45028eef27f 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -264,11 +264,9 @@ async def async_get_integration_descriptions(
     core_flows: dict[str, Any] = json_loads(flow)
     custom_integrations = await async_get_custom_components(hass)
     custom_flows: dict[str, Any] = {
-        "device": {},
+        "integration": {},
         "hardware": {},
         "helper": {},
-        "hub": {},
-        "service": {},
     }
 
     for integration in custom_integrations.values():
@@ -276,20 +274,25 @@ async def async_get_integration_descriptions(
         if integration.integration_type in ("entity", "system"):
             continue
 
-        for integration_type in ("device", "hardware", "helper", "hub", "service"):
+        for integration_type in ("integration", "hardware", "helper"):
             if integration.domain not in core_flows[integration_type]:
                 continue
             del core_flows[integration_type][integration.domain]
         if integration.domain in core_flows["translated_name"]:
             core_flows["translated_name"].remove(integration.domain)
 
+        if integration.integration_type in ("hardware", "helper"):
+            integration_key: str = integration.integration_type
+        else:
+            integration_key = "integration"
+
         metadata = {
             "config_flow": integration.config_flow,
             "integration_type": integration.integration_type,
             "iot_class": integration.iot_class,
             "name": integration.name,
         }
-        custom_flows[integration.integration_type][integration.domain] = metadata
+        custom_flows[integration_key][integration.domain] = metadata
 
     return {"core": core_flows, "custom": custom_flows}
 
-- 
GitLab


From 92eaa539b6dbf60d9bf3a052449a03c23479df7b Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Thu, 20 Oct 2022 19:37:13 +0200
Subject: [PATCH 639/985] Simplify mocking UniFi websocket (#80652)

* Simplify mocking UniFi websocket

* Fix one more
---
 tests/components/unifi/conftest.py            |  20 ++-
 tests/components/unifi/test_controller.py     |  24 +--
 tests/components/unifi/test_device_tracker.py | 162 +++---------------
 tests/components/unifi/test_sensor.py         |  28 +--
 tests/components/unifi/test_switch.py         | 139 ++++-----------
 tests/components/unifi/test_update.py         |  18 +-
 6 files changed, 91 insertions(+), 300 deletions(-)

diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py
index 40635823836..19574a9ab42 100644
--- a/tests/components/unifi/conftest.py
+++ b/tests/components/unifi/conftest.py
@@ -3,7 +3,8 @@ from __future__ import annotations
 
 from unittest.mock import patch
 
-from aiounifi.websocket import WebsocketSignal
+from aiounifi.models.message import MessageKey
+from aiounifi.websocket import WebsocketSignal, WebsocketState
 import pytest
 
 from homeassistant.helpers import device_registry as dr
@@ -16,11 +17,24 @@ def mock_unifi_websocket():
     """No real websocket allowed."""
     with patch("aiounifi.controller.WSClient") as mock:
 
-        def make_websocket_call(data: dict | None = None, state: str = ""):
+        def make_websocket_call(
+            *,
+            message: MessageKey | None = None,
+            data: list[dict] | dict | None = None,
+            state: WebsocketState | None = None,
+        ):
             """Generate a websocket call."""
-            if data:
+            if data and not message:
                 mock.return_value.data = data
                 mock.call_args[1]["callback"](WebsocketSignal.DATA)
+            elif data and message:
+                if not isinstance(data, list):
+                    data = [data]
+                mock.return_value.data = {
+                    "meta": {"message": message.value},
+                    "data": data,
+                }
+                mock.call_args[1]["callback"](WebsocketSignal.DATA)
             elif state:
                 mock.return_value.state = state
                 mock.call_args[1]["callback"](WebsocketSignal.CONNECTION_STATE)
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
index 0079e8e984c..3861c5b38bd 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -8,6 +8,7 @@ from unittest.mock import Mock, patch
 
 import aiounifi
 from aiounifi.models.event import EventKey
+from aiounifi.models.message import MessageKey
 from aiounifi.websocket import WebsocketState
 import pytest
 
@@ -397,21 +398,14 @@ async def test_wireless_client_event_calls_update_wireless_devices(
         "homeassistant.components.unifi.controller.UniFiController.update_wireless_clients",
         return_value=None,
     ) as wireless_clients_mock:
-        mock_unifi_websocket(
-            data={
-                "meta": {"rc": "ok", "message": "events"},
-                "data": [
-                    {
-                        "datetime": "2020-01-20T19:37:04Z",
-                        "user": "00:00:00:00:00:01",
-                        "key": EventKey.WIRELESS_CLIENT_CONNECTED.value,
-                        "msg": "User[11:22:33:44:55:66] has connected to WLAN",
-                        "time": 1579549024893,
-                    }
-                ],
-            },
-        )
-
+        event = {
+            "datetime": "2020-01-20T19:37:04Z",
+            "user": "00:00:00:00:00:01",
+            "key": EventKey.WIRELESS_CLIENT_CONNECTED.value,
+            "msg": "User[11:22:33:44:55:66] has connected to WLAN",
+            "time": 1579549024893,
+        }
+        mock_unifi_websocket(message=MessageKey.EVENT, data=event)
         assert wireless_clients_mock.assert_called_once
 
 
diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py
index d004d96a73c..b8f1aa771a4 100644
--- a/tests/components/unifi/test_device_tracker.py
+++ b/tests/components/unifi/test_device_tracker.py
@@ -57,12 +57,7 @@ async def test_tracked_wireless_clients(
     # Updated timestamp marks client as home
 
     client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.client").state == STATE_HOME
@@ -78,12 +73,7 @@ async def test_tracked_wireless_clients(
 
     # Same timestamp doesn't explicitly mark client as away
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.client").state == STATE_HOME
@@ -158,12 +148,7 @@ async def test_tracked_clients(
     # State change signalling works
 
     client_1["last_seen"] += 1
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client_1)
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.client_1").state == STATE_HOME
@@ -208,14 +193,8 @@ async def test_tracked_wireless_clients_event_source(
         "msg": f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] with SSID "{client["essid"]}" on "channel 44(na)"',
         "_id": "5ea331fa30c49e00f90ddc1a",
     }
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.EVENT.value},
-            "data": [event],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.EVENT, data=event)
     await hass.async_block_till_done()
-
     assert hass.states.get("device_tracker.client").state == STATE_HOME
 
     # Disconnected event
@@ -235,12 +214,7 @@ async def test_tracked_wireless_clients_event_source(
         "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])',
         "_id": "5ea32ff730c49e00f90dca1a",
     }
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.EVENT.value},
-            "data": [event],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.EVENT, data=event)
     await hass.async_block_till_done()
     assert hass.states.get("device_tracker.client").state == STATE_HOME
 
@@ -258,14 +232,8 @@ async def test_tracked_wireless_clients_event_source(
 
     # New data
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
-
     assert hass.states.get("device_tracker.client").state == STATE_HOME
 
     # Disconnection event will be ignored
@@ -285,12 +253,7 @@ async def test_tracked_wireless_clients_event_source(
         "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])',
         "_id": "5ea32ff730c49e00f90dca1a",
     }
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.EVENT.value},
-            "data": [event],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.EVENT, data=event)
     await hass.async_block_till_done()
     assert hass.states.get("device_tracker.client").state == STATE_HOME
 
@@ -350,19 +313,8 @@ async def test_tracked_devices(
     # State change signalling work
 
     device_1["next_interval"] = 20
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_1],
-        }
-    )
     device_2["next_interval"] = 50
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_2],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=[device_1, device_2])
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.device_1").state == STATE_HOME
@@ -381,12 +333,7 @@ async def test_tracked_devices(
     # Disabled device is unavailable
 
     device_1["disabled"] = True
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.device_1").state == STATE_UNAVAILABLE
@@ -420,12 +367,7 @@ async def test_remove_clients(
 
     # Remove client
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT_REMOVED.value},
-            "data": [client_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_1)
     await hass.async_block_till_done()
     await hass.async_block_till_done()
 
@@ -723,20 +665,11 @@ async def test_option_ssid_filter(
 
     # Roams to SSID outside of filter
     client["essid"] = "other_ssid"
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
+
     # Data update while SSID filter is in effect shouldn't create the client
     client_on_ssid2["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client_on_ssid2],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2)
     await hass.async_block_till_done()
 
     # SSID filter marks client as away
@@ -754,18 +687,7 @@ async def test_option_ssid_filter(
 
     client["last_seen"] += 1
     client_on_ssid2["last_seen"] += 1
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client_on_ssid2],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=[client, client_on_ssid2])
     await hass.async_block_till_done()
 
     assert hass.states.get("device_tracker.client").state == STATE_HOME
@@ -781,12 +703,7 @@ async def test_option_ssid_filter(
     assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
 
     client_on_ssid2["last_seen"] += 1
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client_on_ssid2],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2)
     await hass.async_block_till_done()
 
     # Client won't go away until after next update
@@ -794,12 +711,7 @@ async def test_option_ssid_filter(
 
     # Trigger update to get client marked as away
     client_on_ssid2["last_seen"] += 1
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client_on_ssid2],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2)
     await hass.async_block_till_done()
 
     new_time = (
@@ -843,12 +755,7 @@ async def test_wireless_client_go_wired_issue(
     # Trigger wired bug
     client["last_seen"] += 1
     client["is_wired"] = True
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     # Wired bug fix keeps client marked as wireless
@@ -869,12 +776,7 @@ async def test_wireless_client_go_wired_issue(
 
     # Try to mark client as connected
     client["last_seen"] += 1
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     # Make sure it don't go online again until wired bug disappears
@@ -885,12 +787,7 @@ async def test_wireless_client_go_wired_issue(
     # Make client wireless
     client["last_seen"] += 1
     client["is_wired"] = False
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     # Client is no longer affected by wired bug and can be marked online
@@ -929,12 +826,7 @@ async def test_option_ignore_wired_bug(
 
     # Trigger wired bug
     client["is_wired"] = True
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     # Wired bug in effect
@@ -955,12 +847,7 @@ async def test_option_ignore_wired_bug(
 
     # Mark client as connected again
     client["last_seen"] += 1
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     # Ignoring wired bug allows client to go home again even while affected
@@ -971,12 +858,7 @@ async def test_option_ignore_wired_bug(
     # Make client wireless
     client["last_seen"] += 1
     client["is_wired"] = False
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=client)
     await hass.async_block_till_done()
 
     # Client is wireless and still connected
diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py
index 0bf03b10029..100918a93da 100644
--- a/tests/components/unifi/test_sensor.py
+++ b/tests/components/unifi/test_sensor.py
@@ -87,12 +87,7 @@ async def test_bandwidth_sensors(hass, aioclient_mock, mock_unifi_websocket):
     wireless_client["rx_bytes-r"] = 3456000000
     wireless_client["tx_bytes-r"] = 7891000000
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT.value},
-            "data": [wireless_client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client)
     await hass.async_block_till_done()
 
     assert hass.states.get("sensor.wireless_client_rx").state == "3456.0"
@@ -199,12 +194,7 @@ async def test_uptime_sensors(
     uptime_client["uptime"] = event_uptime
     now = datetime(2021, 1, 1, 1, 1, 4, tzinfo=dt_util.UTC)
     with patch("homeassistant.util.dt.now", return_value=now):
-        mock_unifi_websocket(
-            data={
-                "meta": {"message": MessageKey.CLIENT.value},
-                "data": [uptime_client],
-            }
-        )
+        mock_unifi_websocket(message=MessageKey.CLIENT, data=uptime_client)
         await hass.async_block_till_done()
 
     assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00"
@@ -215,12 +205,7 @@ async def test_uptime_sensors(
     uptime_client["uptime"] = new_uptime
     now = datetime(2021, 2, 1, 1, 1, 0, tzinfo=dt_util.UTC)
     with patch("homeassistant.util.dt.now", return_value=now):
-        mock_unifi_websocket(
-            data={
-                "meta": {"message": MessageKey.CLIENT.value},
-                "data": [uptime_client],
-            }
-        )
+        mock_unifi_websocket(message=MessageKey.CLIENT, data=uptime_client)
         await hass.async_block_till_done()
 
     assert hass.states.get("sensor.client1_uptime").state == "2021-02-01T01:00:00+00:00"
@@ -308,12 +293,7 @@ async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket):
 
     # Remove wired client
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT_REMOVED.value},
-            "data": [wired_client],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=wired_client)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_all()) == 5
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index e3ce7859e03..e0bac2c3eb3 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -742,12 +742,7 @@ async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket):
     assert hass.states.get("switch.block_client_2") is not None
     assert hass.states.get("switch.block_media_streaming") is not None
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.CLIENT_REMOVED.value},
-            "data": [CLIENT_1, UNBLOCKED],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[CLIENT_1, UNBLOCKED])
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
@@ -789,12 +784,7 @@ async def test_block_switches(hass, aioclient_mock, mock_unifi_websocket):
     assert unblocked is not None
     assert unblocked.state == "on"
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.EVENT.value},
-            "data": [EVENT_BLOCKED_CLIENT_UNBLOCKED],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_UNBLOCKED)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@@ -802,12 +792,7 @@ async def test_block_switches(hass, aioclient_mock, mock_unifi_websocket):
     assert blocked is not None
     assert blocked.state == "on"
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.EVENT.value},
-            "data": [EVENT_BLOCKED_CLIENT_BLOCKED],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_BLOCKED)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@@ -881,56 +866,44 @@ async def test_dpi_switches_add_second_app(hass, aioclient_mock, mock_unifi_webs
     assert hass.states.get("switch.block_media_streaming").state == STATE_ON
 
     second_app_event = {
-        "meta": {"rc": "ok", "message": "dpiapp:add"},
-        "data": [
-            {
-                "apps": [524292],
-                "blocked": False,
-                "cats": [],
-                "enabled": False,
-                "log": False,
-                "site_id": "name",
-                "_id": "61783e89c1773a18c0c61f00",
-            }
-        ],
+        "apps": [524292],
+        "blocked": False,
+        "cats": [],
+        "enabled": False,
+        "log": False,
+        "site_id": "name",
+        "_id": "61783e89c1773a18c0c61f00",
     }
-    mock_unifi_websocket(data=second_app_event)
+    mock_unifi_websocket(message=MessageKey.DPI_APP_ADDED, data=second_app_event)
     await hass.async_block_till_done()
 
     assert hass.states.get("switch.block_media_streaming").state == STATE_ON
 
     add_second_app_to_group = {
-        "meta": {"rc": "ok", "message": "dpigroup:sync"},
-        "data": [
-            {
-                "_id": "5f976f4ae3c58f018ec7dff6",
-                "name": "Block Media Streaming",
-                "site_id": "name",
-                "dpiapp_ids": ["5f976f62e3c58f018ec7e17d", "61783e89c1773a18c0c61f00"],
-            }
-        ],
+        "_id": "5f976f4ae3c58f018ec7dff6",
+        "name": "Block Media Streaming",
+        "site_id": "name",
+        "dpiapp_ids": ["5f976f62e3c58f018ec7e17d", "61783e89c1773a18c0c61f00"],
     }
-
-    mock_unifi_websocket(data=add_second_app_to_group)
+    mock_unifi_websocket(
+        message=MessageKey.DPI_GROUP_UPDATED, data=add_second_app_to_group
+    )
     await hass.async_block_till_done()
 
     assert hass.states.get("switch.block_media_streaming").state == STATE_OFF
 
     second_app_event_enabled = {
-        "meta": {"rc": "ok", "message": "dpiapp:sync"},
-        "data": [
-            {
-                "apps": [524292],
-                "blocked": False,
-                "cats": [],
-                "enabled": True,
-                "log": False,
-                "site_id": "name",
-                "_id": "61783e89c1773a18c0c61f00",
-            }
-        ],
+        "apps": [524292],
+        "blocked": False,
+        "cats": [],
+        "enabled": True,
+        "log": False,
+        "site_id": "name",
+        "_id": "61783e89c1773a18c0c61f00",
     }
-    mock_unifi_websocket(data=second_app_event_enabled)
+    mock_unifi_websocket(
+        message=MessageKey.DPI_APP_UPDATED, data=second_app_event_enabled
+    )
     await hass.async_block_till_done()
 
     assert hass.states.get("switch.block_media_streaming").state == STATE_ON
@@ -957,12 +930,7 @@ async def test_outlet_switches(hass, aioclient_mock, mock_unifi_websocket):
     outlet_up1 = deepcopy(OUTLET_UP1)
     outlet_up1["outlet_table"][0]["relay_state"] = True
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [outlet_up1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=outlet_up1)
     await hass.async_block_till_done()
 
     outlet = hass.states.get("switch.plug_outlet_1")
@@ -1035,22 +1003,12 @@ async def test_new_client_discovered_on_block_control(
     blocked = hass.states.get("switch.block_client_1")
     assert blocked is None
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": "sta:sync"},
-            "data": [BLOCKED],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=BLOCKED)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.EVENT.value},
-            "data": [EVENT_BLOCKED_CLIENT_CONNECTED],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_CONNECTED)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
@@ -1141,22 +1099,12 @@ async def test_new_client_discovered_on_poe_control(
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": "sta:sync"},
-            "data": [CLIENT_2],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.CLIENT, data=CLIENT_2)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
 
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.EVENT.value},
-            "data": [EVENT_CLIENT_2_CONNECTED],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_CLIENT_2_CONNECTED)
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2
@@ -1423,12 +1371,7 @@ async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket):
     # Update state object
     device_1 = deepcopy(DEVICE_1)
     device_1["port_table"][0]["poe_mode"] = "off"
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
     await hass.async_block_till_done()
     assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
 
@@ -1475,22 +1418,12 @@ async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     # Device gets disabled
     device_1["disabled"] = True
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
     await hass.async_block_till_done()
     assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_UNAVAILABLE
 
     # Device gets re-enabled
     device_1["disabled"] = False
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
     await hass.async_block_till_done()
     assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF
diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py
index 9f212b5e065..3c30da0b62d 100644
--- a/tests/components/unifi/test_update.py
+++ b/tests/components/unifi/test_update.py
@@ -64,9 +64,7 @@ async def test_no_entities(hass, aioclient_mock):
     assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 0
 
 
-async def test_device_updates(
-    hass, aioclient_mock, mock_unifi_websocket, mock_device_registry
-):
+async def test_device_updates(hass, aioclient_mock, mock_unifi_websocket):
     """Test the update_items function with some devices."""
     device_1 = deepcopy(DEVICE_1)
     await setup_unifi_integration(
@@ -102,12 +100,7 @@ async def test_device_updates(
     # Simulate start of update
 
     device_1["state"] = 4
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
     await hass.async_block_till_done()
 
     device_1_state = hass.states.get("update.device_1")
@@ -122,12 +115,7 @@ async def test_device_updates(
     device_1["version"] = "4.3.17.11279"
     device_1["upgradable"] = False
     del device_1["upgrade_to_firmware"]
-    mock_unifi_websocket(
-        data={
-            "meta": {"message": MessageKey.DEVICE.value},
-            "data": [device_1],
-        }
-    )
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
     await hass.async_block_till_done()
 
     device_1_state = hass.states.get("update.device_1")
-- 
GitLab


From 5589edd8140fb721b461dd7725e6c0679b4b2860 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 20 Oct 2022 13:35:38 -0500
Subject: [PATCH 640/985] Fix bluetooth calls from automations in esphome
 (#80683)

---
 homeassistant/components/bluetooth/models.py         | 2 ++
 homeassistant/components/esphome/bluetooth/client.py | 4 ++--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py
index 99024dd3450..a2e50fe1182 100644
--- a/homeassistant/components/bluetooth/models.py
+++ b/homeassistant/components/bluetooth/models.py
@@ -284,6 +284,7 @@ class HaBleakClientWrapper(BleakClient):
     async def connect(self, **kwargs: Any) -> bool:
         """Connect to the specified GATT server."""
         if not self._backend:
+            assert MANAGER is not None
             wrapped_backend = (
                 self._async_get_backend() or self._async_get_fallback_backend()
             )
@@ -292,6 +293,7 @@ class HaBleakClientWrapper(BleakClient):
                 or wrapped_backend.device,
                 disconnected_callback=self.__disconnected_callback,
                 timeout=self.__timeout,
+                hass=MANAGER.hass,
             )
         return await super().connect(**kwargs)
 
diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py
index 14924756074..eda75436502 100644
--- a/homeassistant/components/esphome/bluetooth/client.py
+++ b/homeassistant/components/esphome/bluetooth/client.py
@@ -15,7 +15,7 @@ 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
+from homeassistant.core import CALLBACK_TYPE
 
 from ..domain_data import DomainData
 from .characteristic import BleakGATTCharacteristicESPHome
@@ -83,7 +83,7 @@ class ESPHomeClient(BaseBleakClient):
         self._address_as_int = mac_to_int(self._ble_device.address)
         assert self._ble_device.details is not None
         self._source = self._ble_device.details["source"]
-        self.domain_data = DomainData.get(async_get_hass())
+        self.domain_data = DomainData.get(kwargs["hass"])
         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
-- 
GitLab


From 03362bec1ccef423febe62a23694619d3460e550 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 20 Oct 2022 13:56:20 -0500
Subject: [PATCH 641/985] Defer bluetooth scanner watchdog restart if one is
 already in progress (#80679)

---
 homeassistant/components/bluetooth/scanner.py |  6 ++
 tests/components/bluetooth/test_scanner.py    | 66 +++++++++++++++++++
 2 files changed, 72 insertions(+)

diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py
index 8d4adfe9a3d..fe795f7ace5 100644
--- a/homeassistant/components/bluetooth/scanner.py
+++ b/homeassistant/components/bluetooth/scanner.py
@@ -347,6 +347,12 @@ class HaScanner(BaseHaScanner):
         )
         if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT:
             return
+        if self._start_stop_lock.locked():
+            _LOGGER.debug(
+                "%s: Scanner is already restarting, deferring restart",
+                self.name,
+            )
+            return
         _LOGGER.info(
             "%s: Bluetooth scanner has gone quiet for %ss, restarting",
             self.name,
diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py
index 512815b1239..c3a08ac3361 100644
--- a/tests/components/bluetooth/test_scanner.py
+++ b/tests/components/bluetooth/test_scanner.py
@@ -1,4 +1,5 @@
 """Tests for the Bluetooth integration scanners."""
+import asyncio
 from datetime import timedelta
 import time
 from unittest.mock import MagicMock, patch
@@ -487,3 +488,68 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init(
 
     assert len(mock_recover_adapter.mock_calls) == 1
     assert "Waiting for adapter to initialize" in caplog.text
+
+
+async def test_restart_takes_longer_than_watchdog_time(hass, one_adapter, caplog):
+    """Test we do not try to recover the adapter again if the restart is still in progress."""
+
+    release_start_event = asyncio.Event()
+    called_start = 0
+
+    class MockBleakScanner:
+        async def start(self, *args, **kwargs):
+            """Mock Start."""
+            nonlocal called_start
+            called_start += 1
+            if called_start == 1:
+                return
+            await release_start_event.wait()
+
+        async def stop(self, *args, **kwargs):
+            """Mock Start."""
+
+        @property
+        def discovered_devices(self):
+            """Mock discovered_devices."""
+            return []
+
+        def register_detection_callback(self, callback: AdvertisementDataCallback):
+            """Mock Register Detection Callback."""
+
+    scanner = MockBleakScanner()
+    start_time_monotonic = time.monotonic()
+
+    with patch(
+        "homeassistant.components.bluetooth.scanner.ADAPTER_INIT_TIME",
+        0,
+    ), patch(
+        "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME",
+        return_value=start_time_monotonic,
+    ), patch(
+        "homeassistant.components.bluetooth.scanner.OriginalBleakScanner",
+        return_value=scanner,
+    ), patch(
+        "homeassistant.components.bluetooth.util.recover_adapter", return_value=True
+    ):
+        await async_setup_with_one_adapter(hass)
+
+        assert called_start == 1
+
+        # Now force a recover adapter 2x
+        for _ in range(2):
+            with patch(
+                "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME",
+                return_value=start_time_monotonic
+                + SCANNER_WATCHDOG_TIMEOUT
+                + SCANNER_WATCHDOG_INTERVAL.total_seconds(),
+            ):
+                async_fire_time_changed(
+                    hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL
+                )
+                await asyncio.sleep(0)
+
+        # Now release the start event
+        release_start_event.set()
+        await hass.async_block_till_done()
+
+    assert "already restarting" in caplog.text
-- 
GitLab


From 40d4159faf4ef33037048fc28fd6f335dff43e2d Mon Sep 17 00:00:00 2001
From: luar123 <49960470+luar123@users.noreply.github.com>
Date: Thu, 20 Oct 2022 22:25:21 +0200
Subject: [PATCH 642/985] Bump snapcast to 2.3.0 (#80688)

---
 homeassistant/components/snapcast/manifest.json | 2 +-
 requirements_all.txt                            | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json
index 675a60e4096..a88c91adff0 100644
--- a/homeassistant/components/snapcast/manifest.json
+++ b/homeassistant/components/snapcast/manifest.json
@@ -2,7 +2,7 @@
   "domain": "snapcast",
   "name": "Snapcast",
   "documentation": "https://www.home-assistant.io/integrations/snapcast",
-  "requirements": ["snapcast==2.1.3"],
+  "requirements": ["snapcast==2.3.0"],
   "codeowners": [],
   "iot_class": "local_polling",
   "loggers": ["construct", "snapcast"]
diff --git a/requirements_all.txt b/requirements_all.txt
index 90b961b0961..0dfe6e0e804 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2274,7 +2274,7 @@ smart-meter-texas==0.4.7
 smhi-pkg==1.0.16
 
 # homeassistant.components.snapcast
-snapcast==2.1.3
+snapcast==2.3.0
 
 # homeassistant.components.sonos
 soco==0.28.0
-- 
GitLab


From 0199e0a756c1758ac3a740872e7ea9aa7bdbe734 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Fri, 21 Oct 2022 00:30:37 +0000
Subject: [PATCH 643/985] [ci skip] Translation update

---
 .../amberelectric/translations/nl.json        |  1 +
 .../automation/translations/nl.json           |  5 +++
 .../components/bluetooth/translations/nl.json |  5 +++
 .../components/braviatv/translations/nl.json  |  3 +-
 .../components/deconz/translations/de.json    |  8 ++---
 .../components/ecowitt/translations/nl.json   |  5 +++
 .../components/google/translations/de.json    |  2 +-
 .../google_sheets/translations/de.json        |  2 +-
 .../google_travel_time/translations/fr.json   |  3 +-
 .../components/guardian/translations/nl.json  | 22 ++++++++++++
 .../components/ibeacon/translations/nl.json   | 14 ++++++++
 .../components/insteon/translations/de.json   |  2 +-
 .../components/lametric/translations/fr.json  |  1 +
 .../lutron_caseta/translations/de.json        |  6 ++--
 .../components/nest/translations/de.json      |  2 +-
 .../components/owntracks/translations/de.json |  2 +-
 .../plugwise/translations/select.bg.json      |  3 ++
 .../plugwise/translations/select.de.json      |  6 ++++
 .../plugwise/translations/select.es.json      |  6 ++++
 .../plugwise/translations/select.et.json      |  6 ++++
 .../plugwise/translations/select.fr.json      | 10 ++++++
 .../plugwise/translations/select.nl.json      |  4 +++
 .../plugwise/translations/select.no.json      |  6 ++++
 .../plugwise/translations/select.pl.json      |  6 ++++
 .../plugwise/translations/select.ru.json      |  6 ++++
 .../prusalink/translations/sensor.nl.json     |  1 +
 .../rainmachine/translations/nl.json          | 12 +++++++
 .../simplisafe/translations/nl.json           |  5 +++
 .../components/tilt_ble/translations/de.json  |  4 +--
 .../components/zha/translations/de.json       |  4 +--
 .../components/zha/translations/nl.json       | 36 +++++++++++++++++--
 31 files changed, 178 insertions(+), 20 deletions(-)
 create mode 100644 homeassistant/components/plugwise/translations/select.fr.json

diff --git a/homeassistant/components/amberelectric/translations/nl.json b/homeassistant/components/amberelectric/translations/nl.json
index 11f0576496e..a86af47ebf9 100644
--- a/homeassistant/components/amberelectric/translations/nl.json
+++ b/homeassistant/components/amberelectric/translations/nl.json
@@ -2,6 +2,7 @@
     "config": {
         "error": {
             "invalid_api_token": "Ongeldige API-sleutel",
+            "no_site": "Geen site opgegeven",
             "unknown_error": "Onverwachte fout"
         },
         "step": {
diff --git a/homeassistant/components/automation/translations/nl.json b/homeassistant/components/automation/translations/nl.json
index 7ef3acc9f2c..4e36f46b980 100644
--- a/homeassistant/components/automation/translations/nl.json
+++ b/homeassistant/components/automation/translations/nl.json
@@ -1,4 +1,9 @@
 {
+    "issues": {
+        "service_not_found": {
+            "title": "{name} gebruikt een onbekende service"
+        }
+    },
     "state": {
         "_": {
             "off": "Uit",
diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json
index c9db452612e..22ec504f244 100644
--- a/homeassistant/components/bluetooth/translations/nl.json
+++ b/homeassistant/components/bluetooth/translations/nl.json
@@ -12,6 +12,11 @@
             "enable_bluetooth": {
                 "description": "Wilt u Bluetooth instellen?"
             },
+            "multiple_adapters": {
+                "data": {
+                    "adapter": "Adapter"
+                }
+            },
             "user": {
                 "data": {
                     "address": "Apparaat"
diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json
index 867840c21a3..31dc4844163 100644
--- a/homeassistant/components/braviatv/translations/nl.json
+++ b/homeassistant/components/braviatv/translations/nl.json
@@ -26,7 +26,8 @@
             },
             "reauth_confirm": {
                 "data": {
-                    "pin": "Pincode"
+                    "pin": "Pincode",
+                    "use_psk": "PSK-authenticatie gebruiken"
                 }
             },
             "user": {
diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json
index 28308f10f74..626ccf613cc 100644
--- a/homeassistant/components/deconz/translations/de.json
+++ b/homeassistant/components/deconz/translations/de.json
@@ -42,10 +42,10 @@
             "button_2": "Zweite Taste",
             "button_3": "Dritte Taste",
             "button_4": "Vierte Taste",
-            "button_5": "Taste 5",
-            "button_6": "Taste 6",
-            "button_7": "Taste 7",
-            "button_8": "Taste 8",
+            "button_5": "5. Taste",
+            "button_6": "6. Taste",
+            "button_7": "7. Taste",
+            "button_8": "8. Taste",
             "close": "Schlie\u00dfen",
             "dim_down": "Dimmer runter",
             "dim_up": "Dimmer hoch",
diff --git a/homeassistant/components/ecowitt/translations/nl.json b/homeassistant/components/ecowitt/translations/nl.json
index 1090e946378..112651607f4 100644
--- a/homeassistant/components/ecowitt/translations/nl.json
+++ b/homeassistant/components/ecowitt/translations/nl.json
@@ -3,6 +3,11 @@
         "error": {
             "invalid_port": "Poort wordt al gebruikt.",
             "unknown": "Onverwachte fout"
+        },
+        "step": {
+            "user": {
+                "description": "Weet u zeker dat u Ecowitt wilt instellen?"
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json
index ec3c6d15035..377cafe035e 100644
--- a/homeassistant/components/google/translations/de.json
+++ b/homeassistant/components/google/translations/de.json
@@ -1,6 +1,6 @@
 {
     "application_credentials": {
-        "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um Home Assistant Zugriff auf deinen Google-Kalender zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Kalender verkn\u00fcpft sind:\n1. Gehe zu [Credentials]({oauth_creds_url}) und dr\u00fccke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **TV und eingeschr\u00e4nkte Eingabeger\u00e4te** f\u00fcr den Anwendungstyp.\n\n"
+        "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um Home Assistant Zugriff auf deinen Google-Kalender zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Kalender verkn\u00fcpft sind:\n1. Gehe zu [Credentials]({oauth_creds_url}) und dr\u00fccke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **TV und eingeschr\u00e4nkte Eingabeger\u00e4te** f\u00fcr den Anwendungstyp."
     },
     "config": {
         "abort": {
diff --git a/homeassistant/components/google_sheets/translations/de.json b/homeassistant/components/google_sheets/translations/de.json
index f203b3e1133..417df6bdb3b 100644
--- a/homeassistant/components/google_sheets/translations/de.json
+++ b/homeassistant/components/google_sheets/translations/de.json
@@ -1,6 +1,6 @@
 {
     "application_credentials": {
-        "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um dem Home Assistant Zugriff auf deine Google Sheets zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Konto verkn\u00fcpft sind:\n1. Gehe zu [Anmeldeinformationen]({oauth_creds_url}) und klicke auf **Anmeldeinformationen erstellen**.\n1. W\u00e4hle aus der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp.\n\n"
+        "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um dem Home Assistant Zugriff auf deine Google Sheets zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Konto verkn\u00fcpft sind:\n1. Gehe zu [Anmeldeinformationen]({oauth_creds_url}) und klicke auf **Anmeldeinformationen erstellen**.\n1. W\u00e4hle aus der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp."
     },
     "config": {
         "abort": {
diff --git a/homeassistant/components/google_travel_time/translations/fr.json b/homeassistant/components/google_travel_time/translations/fr.json
index 86761315d58..6bf4595e452 100644
--- a/homeassistant/components/google_travel_time/translations/fr.json
+++ b/homeassistant/components/google_travel_time/translations/fr.json
@@ -4,7 +4,8 @@
             "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9"
         },
         "error": {
-            "cannot_connect": "\u00c9chec de connexion"
+            "cannot_connect": "\u00c9chec de connexion",
+            "invalid_auth": "Authentification non valide"
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json
index d8ed1242e92..0fadd4dbf51 100644
--- a/homeassistant/components/guardian/translations/nl.json
+++ b/homeassistant/components/guardian/translations/nl.json
@@ -17,5 +17,27 @@
                 "description": "Configureer een lokaal Elexa Guardian-apparaat."
             }
         }
+    },
+    "issues": {
+        "deprecated_service": {
+            "fix_flow": {
+                "step": {
+                    "confirm": {
+                        "title": "De {deprecated_service}-service wordt verwijderd"
+                    }
+                }
+            },
+            "title": "De {deprecated_service}-service wordt verwijderd"
+        },
+        "replaced_old_entity": {
+            "fix_flow": {
+                "step": {
+                    "confirm": {
+                        "title": "De {old_entity_id}-entiteit wordt verwijderd"
+                    }
+                }
+            },
+            "title": "De {old_entity_id}-entiteit wordt verwijderd"
+        }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/ibeacon/translations/nl.json b/homeassistant/components/ibeacon/translations/nl.json
index 703ac8614c4..9102d3c69aa 100644
--- a/homeassistant/components/ibeacon/translations/nl.json
+++ b/homeassistant/components/ibeacon/translations/nl.json
@@ -2,6 +2,20 @@
     "config": {
         "abort": {
             "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+        },
+        "step": {
+            "user": {
+                "description": "Wilt u iBeacon Tracker instellen?"
+            }
+        }
+    },
+    "options": {
+        "step": {
+            "init": {
+                "data": {
+                    "min_rssi": "Minimale RSSI"
+                }
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json
index d7f853d9a2a..ffbad247632 100644
--- a/homeassistant/components/insteon/translations/de.json
+++ b/homeassistant/components/insteon/translations/de.json
@@ -19,7 +19,7 @@
                     "host": "IP-Adresse",
                     "port": "Port"
                 },
-                "description": "Konfiguriere den Insteon Hub Version 1 (vor 2014).",
+                "description": "Konfiguriere den Insteon Hub Version 1 (pr\u00e4-2014).",
                 "title": "Insteon Hub Version 1"
             },
             "hubv2": {
diff --git a/homeassistant/components/lametric/translations/fr.json b/homeassistant/components/lametric/translations/fr.json
index fd0125f68c9..3bd9204ad56 100644
--- a/homeassistant/components/lametric/translations/fr.json
+++ b/homeassistant/components/lametric/translations/fr.json
@@ -8,6 +8,7 @@
             "missing_configuration": "L'int\u00e9gration LaMetric n'est pas configur\u00e9e\u00a0; veuillez suivre la documentation.",
             "no_devices": "L'utilisateur autoris\u00e9 ne poss\u00e8de aucun appareil LaMetric",
             "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})",
+            "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi",
             "unknown": "Erreur inattendue"
         },
         "error": {
diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json
index d2406ef16d4..ff4f89aa512 100644
--- a/homeassistant/components/lutron_caseta/translations/de.json
+++ b/homeassistant/components/lutron_caseta/translations/de.json
@@ -33,9 +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",
+            "button_5": "5. Taste",
+            "button_6": "6. Taste",
+            "button_7": "7. Taste",
             "close_1": "Einen schlie\u00dfen",
             "close_2": "Zwei schlie\u00dfen",
             "close_3": "Drei schlie\u00dfen",
diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json
index 85fe8acd16f..efe0ee8c115 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 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",
+                "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} )).",
                 "title": "Nest: Erstelle ein Ger\u00e4tezugriffsprojekt"
             },
             "device_project_upgrade": {
diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json
index f3a7542cdd5..b755c152be4 100644
--- a/homeassistant/components/owntracks/translations/de.json
+++ b/homeassistant/components/owntracks/translations/de.json
@@ -5,7 +5,7 @@
             "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
         },
         "create_entry": {
-            "default": "Unter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n   - Benutzername: `'<Your name>'`\n   - Ger\u00e4te-ID: `'<Your device name>'`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `'<Your name>'`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})."
+            "default": "Unter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `'<Your name>'`\n - Ger\u00e4te-ID: `'<Your device name>'`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `'<Your name>'`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})."
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/plugwise/translations/select.bg.json b/homeassistant/components/plugwise/translations/select.bg.json
index 646d778981e..f27035d8884 100644
--- a/homeassistant/components/plugwise/translations/select.bg.json
+++ b/homeassistant/components/plugwise/translations/select.bg.json
@@ -1,5 +1,8 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d"
+        },
         "plugwise__regulation_mode": {
             "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435",
             "heating": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435",
diff --git a/homeassistant/components/plugwise/translations/select.de.json b/homeassistant/components/plugwise/translations/select.de.json
index 1f2ccd0a825..9ee193d6e0e 100644
--- a/homeassistant/components/plugwise/translations/select.de.json
+++ b/homeassistant/components/plugwise/translations/select.de.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Automatisch",
+            "boost": "Boost",
+            "comfort": "Komfort",
+            "off": "Aus"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "Kalt",
             "bleeding_hot": "Hei\u00df",
diff --git a/homeassistant/components/plugwise/translations/select.es.json b/homeassistant/components/plugwise/translations/select.es.json
index 38fcab309d2..09de452fda3 100644
--- a/homeassistant/components/plugwise/translations/select.es.json
+++ b/homeassistant/components/plugwise/translations/select.es.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Autom\u00e1tico",
+            "boost": "Impulso",
+            "comfort": "Confort",
+            "off": "Apagado"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "Purgando fr\u00edo",
             "bleeding_hot": "Purgando caliente",
diff --git a/homeassistant/components/plugwise/translations/select.et.json b/homeassistant/components/plugwise/translations/select.et.json
index a7eef041f0b..10605bb3ce9 100644
--- a/homeassistant/components/plugwise/translations/select.et.json
+++ b/homeassistant/components/plugwise/translations/select.et.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Automaatne",
+            "boost": "Turbo",
+            "comfort": "Mugavus",
+            "off": "V\u00e4ljas"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "Jahutuseat soenemine",
             "bleeding_hot": "K\u00fcttest jahtumine",
diff --git a/homeassistant/components/plugwise/translations/select.fr.json b/homeassistant/components/plugwise/translations/select.fr.json
new file mode 100644
index 00000000000..169a4bd3a2a
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/select.fr.json
@@ -0,0 +1,10 @@
+{
+    "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Auto",
+            "boost": "Boost",
+            "comfort": "Confort",
+            "off": "Arr\u00eat"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/select.nl.json b/homeassistant/components/plugwise/translations/select.nl.json
index 6e4ce4119fd..546719dee72 100644
--- a/homeassistant/components/plugwise/translations/select.nl.json
+++ b/homeassistant/components/plugwise/translations/select.nl.json
@@ -1,5 +1,9 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Automatisch",
+            "off": "Uit"
+        },
         "plugwise__regulation_mode": {
             "off": "Uit"
         }
diff --git a/homeassistant/components/plugwise/translations/select.no.json b/homeassistant/components/plugwise/translations/select.no.json
index 729d25c936c..4907badc6f8 100644
--- a/homeassistant/components/plugwise/translations/select.no.json
+++ b/homeassistant/components/plugwise/translations/select.no.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Auto",
+            "boost": "\u00d8ke",
+            "comfort": "Komfort",
+            "off": "Av"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "Bl\u00f8dende kaldt",
             "bleeding_hot": "Bl\u00f8dende varmt",
diff --git a/homeassistant/components/plugwise/translations/select.pl.json b/homeassistant/components/plugwise/translations/select.pl.json
index 120801ff712..92159b0fab2 100644
--- a/homeassistant/components/plugwise/translations/select.pl.json
+++ b/homeassistant/components/plugwise/translations/select.pl.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "automatycznie",
+            "boost": "dogrzewanie",
+            "comfort": "komfortowo",
+            "off": "wy\u0142\u0105czone"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "strasznie zimno",
             "bleeding_hot": "strasznie ciep\u0142o",
diff --git a/homeassistant/components/plugwise/translations/select.ru.json b/homeassistant/components/plugwise/translations/select.ru.json
index a72746cc983..9330a9e435c 100644
--- a/homeassistant/components/plugwise/translations/select.ru.json
+++ b/homeassistant/components/plugwise/translations/select.ru.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438",
+            "boost": "\u0422\u0443\u0440\u0431\u043e",
+            "comfort": "\u041a\u043e\u043c\u0444\u043e\u0440\u0442",
+            "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+        },
         "plugwise__regulation_mode": {
             "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435",
             "heating": "\u041e\u0431\u043e\u0433\u0440\u0435\u0432",
diff --git a/homeassistant/components/prusalink/translations/sensor.nl.json b/homeassistant/components/prusalink/translations/sensor.nl.json
index 0dfc3902f68..2e874b25f5a 100644
--- a/homeassistant/components/prusalink/translations/sensor.nl.json
+++ b/homeassistant/components/prusalink/translations/sensor.nl.json
@@ -4,6 +4,7 @@
             "cancelling": "Annuleren",
             "idle": "Inactief",
             "paused": "Gepauzeerd",
+            "pausing": "Pauzeren",
             "printing": "Afdrukken"
         }
     }
diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json
index cbf76b879cb..4fa3285c03b 100644
--- a/homeassistant/components/rainmachine/translations/nl.json
+++ b/homeassistant/components/rainmachine/translations/nl.json
@@ -18,6 +18,18 @@
             }
         }
     },
+    "issues": {
+        "replaced_old_entity": {
+            "fix_flow": {
+                "step": {
+                    "confirm": {
+                        "title": "De {old_entity_id}-entiteit wordt verwijderd"
+                    }
+                }
+            },
+            "title": "De {old_entity_id}-entiteit wordt verwijderd"
+        }
+    },
     "options": {
         "step": {
             "init": {
diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json
index 8c356ed9e12..b24bde70fac 100644
--- a/homeassistant/components/simplisafe/translations/nl.json
+++ b/homeassistant/components/simplisafe/translations/nl.json
@@ -35,6 +35,11 @@
             }
         }
     },
+    "issues": {
+        "deprecated_service": {
+            "title": "De {deprecated_service}-service wordt verwijderd"
+        }
+    },
     "options": {
         "step": {
             "init": {
diff --git a/homeassistant/components/tilt_ble/translations/de.json b/homeassistant/components/tilt_ble/translations/de.json
index 6b3976336d2..81dda510bc5 100644
--- a/homeassistant/components/tilt_ble/translations/de.json
+++ b/homeassistant/components/tilt_ble/translations/de.json
@@ -1,14 +1,14 @@
 {
     "config": {
         "abort": {
-            "already_configured": "Ger\u00e4t bereits konfiguriert",
+            "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}",
         "step": {
             "bluetooth_confirm": {
-                "description": "Willst du {name} einrichten?"
+                "description": "M\u00f6chtest du {name} einrichten?"
             },
             "user": {
                 "data": {
diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json
index abe872ab75c..5be0add1ae2 100644
--- a/homeassistant/components/zha/translations/de.json
+++ b/homeassistant/components/zha/translations/de.json
@@ -61,7 +61,7 @@
                 "data": {
                     "overwrite_coordinator_ieee": "Dauerhaftes Ersetzen der IEEE-Funkadresse"
                 },
-                "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.",
+                "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"
             },
             "pick_radio": {
@@ -240,7 +240,7 @@
                 "data": {
                     "overwrite_coordinator_ieee": "Dauerhaftes Ersetzen der IEEE-Funkadresse"
                 },
-                "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.",
+                "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": {
diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json
index dacddd201d9..e408a9949bc 100644
--- a/homeassistant/components/zha/translations/nl.json
+++ b/homeassistant/components/zha/translations/nl.json
@@ -6,19 +6,35 @@
             "usb_probe_failed": "Kon het USB apparaat niet onderzoeken"
         },
         "error": {
-            "cannot_connect": "Kan geen verbinding maken"
+            "cannot_connect": "Kan geen verbinding maken",
+            "invalid_backup_json": "Ongeldige back-up-JSON"
         },
         "flow_title": "{name}",
         "step": {
             "choose_automatic_backup": {
                 "title": "Automatische back-up herstellen"
             },
+            "choose_formation_strategy": {
+                "menu_options": {
+                    "choose_automatic_backup": "Een automatische back-up herstellen",
+                    "upload_manual_backup": "Upload een handmatige back-up"
+                }
+            },
+            "choose_serial_port": {
+                "description": "Selecteer de seri\u00eble poort voor je Zigbee-radio",
+                "title": "Selecteer een seri\u00eble poort"
+            },
             "confirm": {
                 "description": "Wilt u {name} instellen?"
             },
             "confirm_hardware": {
                 "description": "Wilt u {name} instellen?"
             },
+            "manual_port_config": {
+                "data": {
+                    "baudrate": "poortsnelheid"
+                }
+            },
             "pick_radio": {
                 "data": {
                     "radio_type": "Radio type"
@@ -127,17 +143,33 @@
             "usb_probe_failed": "Kon het USB apparaat niet onderzoeken"
         },
         "error": {
-            "cannot_connect": "Kan geen verbinding maken"
+            "cannot_connect": "Kan geen verbinding maken",
+            "invalid_backup_json": "Ongeldige back-up-JSON"
         },
         "flow_title": "{name}",
         "step": {
             "choose_automatic_backup": {
                 "title": "Automatische back-up herstellen"
             },
+            "choose_formation_strategy": {
+                "menu_options": {
+                    "choose_automatic_backup": "Een automatische back-up herstellen",
+                    "upload_manual_backup": "Upload een handmatige back-up"
+                }
+            },
+            "choose_serial_port": {
+                "description": "Selecteer de seri\u00eble poort voor je Zigbee-radio",
+                "title": "Selecteer een seri\u00eble poort"
+            },
             "init": {
                 "description": "ZHA wordt gestopt. Wilt u doorgaan?",
                 "title": "ZHA opnieuw configureren"
             },
+            "manual_port_config": {
+                "data": {
+                    "baudrate": "poortsnelheid"
+                }
+            },
             "upload_manual_backup": {
                 "data": {
                     "uploaded_backup_file": "Een bestand uploaden"
-- 
GitLab


From 90f69248089d1fb3e9435eb0a13b8e925d51e987 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Thu, 20 Oct 2022 19:32:32 -0500
Subject: [PATCH 644/985] Bump zeroconf to 0.39.2 (#80699)

python 3.11 support & performance improvements

changelog: https://github.com/jstasiak/python-zeroconf/compare/0.39.1...0.39.2
---
 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 5fcb514ea51..f0e2005b20e 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.1"],
+  "requirements": ["zeroconf==0.39.2"],
   "dependencies": ["network", "api"],
   "codeowners": ["@bdraco"],
   "quality_scale": "internal",
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 483f8e6d9ee..f65c14d3c17 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.1
+zeroconf==0.39.2
 
 # 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 0dfe6e0e804..6650e92b308 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2598,7 +2598,7 @@ youtube_dl==2021.12.17
 zengge==0.2
 
 # homeassistant.components.zeroconf
-zeroconf==0.39.1
+zeroconf==0.39.2
 
 # homeassistant.components.zha
 zha-quirks==0.0.83
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f2808df8629..9bd531b0a3b 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1799,7 +1799,7 @@ yolink-api==0.1.0
 youless-api==0.16
 
 # homeassistant.components.zeroconf
-zeroconf==0.39.1
+zeroconf==0.39.2
 
 # homeassistant.components.zha
 zha-quirks==0.0.83
-- 
GitLab


From 245c13e6ed37fcf27eeab072ef630533ea9dc889 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
Date: Thu, 20 Oct 2022 20:02:40 -0500
Subject: [PATCH 645/985] Add diagnostics to jellyfin (#80651)

---
 .../components/jellyfin/coordinator.py        |    2 +-
 .../components/jellyfin/diagnostics.py        |   48 +
 .../jellyfin/fixtures/sessions.json           | 2837 ++++++++++++++++-
 tests/components/jellyfin/test_diagnostics.py |  611 ++++
 tests/components/jellyfin/test_sensor.py      |    2 +-
 5 files changed, 3474 insertions(+), 26 deletions(-)
 create mode 100644 homeassistant/components/jellyfin/diagnostics.py
 create mode 100644 tests/components/jellyfin/test_diagnostics.py

diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py
index 9bcf3cf733d..3e277711f6c 100644
--- a/homeassistant/components/jellyfin/coordinator.py
+++ b/homeassistant/components/jellyfin/coordinator.py
@@ -65,7 +65,7 @@ class SessionsDataUpdateCoordinator(
             self.api_client.jellyfin.sessions
         )
 
-        sessions_by_id: dict[str, Any] = {
+        sessions_by_id: dict[str, dict[str, Any]] = {
             session["Id"]: session for session in sessions
         }
 
diff --git a/homeassistant/components/jellyfin/diagnostics.py b/homeassistant/components/jellyfin/diagnostics.py
new file mode 100644
index 00000000000..36b2882fbeb
--- /dev/null
+++ b/homeassistant/components/jellyfin/diagnostics.py
@@ -0,0 +1,48 @@
+"""Diagnostics support for Jellyfin."""
+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_PASSWORD
+from homeassistant.core import HomeAssistant
+
+from .const import DOMAIN
+from .models import JellyfinData
+
+TO_REDACT = {CONF_PASSWORD}
+
+
+async def async_get_config_entry_diagnostics(
+    hass: HomeAssistant, entry: ConfigEntry
+) -> dict[str, Any]:
+    """Return diagnostics for a config entry."""
+    data: JellyfinData = hass.data[DOMAIN][entry.entry_id]
+    sessions = data.coordinators["sessions"]
+
+    return {
+        "entry": {
+            "title": entry.title,
+            "data": async_redact_data(entry.data, TO_REDACT),
+        },
+        "server": {
+            "id": sessions.server_id,
+            "name": sessions.server_name,
+            "version": sessions.server_version,
+        },
+        "sessions": [
+            {
+                "id": session_id,
+                "user_id": session_data.get("UserId"),
+                "device_id": session_data.get("DeviceId"),
+                "device_name": session_data.get("DeviceName"),
+                "client_name": session_data.get("Client"),
+                "client_version": session_data.get("ApplicationVersion"),
+                "capabilities": session_data.get("Capabilities"),
+                "now_playing": session_data.get("NowPlayingItem"),
+                "play_state": session_data.get("PlayState"),
+            }
+            for session_id, session_data in sessions.data.items()
+        ],
+    }
diff --git a/tests/components/jellyfin/fixtures/sessions.json b/tests/components/jellyfin/fixtures/sessions.json
index c51be6a0aa4..00a1f5265db 100644
--- a/tests/components/jellyfin/fixtures/sessions.json
+++ b/tests/components/jellyfin/fixtures/sessions.json
@@ -1,7 +1,7 @@
 [
   {
     "PlayState": {
-      "PositionTicks": 0,
+      "PositionTicks": 100000000,
       "CanSeek": true,
       "IsPaused": true,
       "IsMuted": true,
@@ -20,8 +20,8 @@
       }
     ],
     "Capabilities": {
-      "PlayableMediaTypes": ["string"],
-      "SupportedCommands": ["MoveUp"],
+      "PlayableMediaTypes": ["Video"],
+      "SupportedCommands": ["VolumeSet", "Mute"],
       "SupportsMediaControl": true,
       "SupportsContentUploading": true,
       "MessageCallbackUrl": "string",
@@ -186,20 +186,20 @@
       "IconUrl": "string"
     },
     "RemoteEndPoint": "string",
-    "PlayableMediaTypes": ["string"],
-    "Id": "string",
+    "PlayableMediaTypes": ["Video"],
+    "Id": "SESSION-UUID",
     "UserId": "08ba1929-681e-4b24-929b-9245852f65c0",
     "UserName": "string",
-    "Client": "string",
+    "Client": "Jellyfin for Developers",
     "LastActivityDate": "2019-08-24T14:15:22Z",
     "LastPlaybackCheckIn": "2019-08-24T14:15:22Z",
-    "DeviceName": "string",
+    "DeviceName": "JELLYFIN-DEVICE",
     "DeviceType": "string",
     "NowPlayingItem": {
-      "Name": "string",
+      "Name": "EPISODE",
       "OriginalTitle": "string",
-      "ServerId": "string",
-      "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43",
+      "ServerId": "SERVER-UUID",
+      "Id": "EPISODE-UUID",
       "Etag": "string",
       "SourceType": "string",
       "PlaylistItemId": "string",
@@ -359,16 +359,16 @@
       "Genres": ["string"],
       "CommunityRating": 0,
       "CumulativeRunTimeTicks": 0,
-      "RunTimeTicks": 0,
+      "RunTimeTicks": 600000000,
       "PlayAccess": "Full",
       "AspectRatio": "string",
       "ProductionYear": 0,
       "IsPlaceHolder": true,
       "Number": "string",
       "ChannelNumber": "string",
-      "IndexNumber": 0,
+      "IndexNumber": 3,
       "IndexNumberEnd": 0,
-      "ParentIndexNumber": 0,
+      "ParentIndexNumber": 1,
       "RemoteTrailers": [
         {
           "Url": "string",
@@ -380,9 +380,9 @@
         "property2": "string"
       },
       "IsHD": true,
-      "IsFolder": true,
-      "ParentId": "c54e2d15-b5eb-48b7-9b04-53f376904b1e",
-      "Type": "AggregateFolder",
+      "IsFolder": false,
+      "ParentId": "PARENT-UUID",
+      "Type": "Episode",
       "People": [
         {
           "Name": "string",
@@ -477,9 +477,9 @@
       },
       "RecursiveItemCount": 0,
       "ChildCount": 0,
-      "SeriesName": "string",
-      "SeriesId": "c7b70af4-4902-4a7e-95ab-28349b6c7afc",
-      "SeasonId": "badb6463-e5b7-45c5-8141-71204420ec8f",
+      "SeriesName": "SERIES",
+      "SeriesId": "SERIES-UUID",
+      "SeasonId": "SEASON-UUID",
       "SpecialFeatureCount": 0,
       "DisplayPreferencesId": "string",
       "Status": "string",
@@ -507,7 +507,7 @@
           "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43"
         }
       ],
-      "SeasonName": "string",
+      "SeasonName": "SEASON",
       "MediaStreams": [
         {
           "Codec": "string",
@@ -637,7 +637,7 @@
           "property2": "string"
         }
       },
-      "SeriesStudio": "string",
+      "SeriesStudio": "HASS",
       "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46",
       "ParentThumbImageTag": "string",
       "ParentPrimaryImageItemId": "string",
@@ -1221,8 +1221,8 @@
       "TimerId": "string",
       "CurrentProgram": {}
     },
-    "DeviceId": "string",
-    "ApplicationVersion": "string",
+    "DeviceId": "DEVICE-UUID",
+    "ApplicationVersion": "1.0.0",
     "TranscodingInfo": {
       "AudioCodec": "string",
       "VideoCodec": "string",
@@ -1755,8 +1755,2797 @@
     ],
     "HasCustomDeviceName": true,
     "PlaylistItemId": "string",
-    "ServerId": "string",
+    "ServerId": "SERVER-UUID",
     "UserPrimaryImageTag": "string",
     "SupportedCommands": ["MoveUp"]
+  },
+  {
+    "PlayState": {
+      "PositionTicks": 230000000,
+      "CanSeek": true,
+      "IsPaused": false,
+      "IsMuted": false,
+      "VolumeLevel": 55,
+      "AudioStreamIndex": 0,
+      "SubtitleStreamIndex": 0,
+      "MediaSourceId": "string",
+      "PlayMethod": "Transcode",
+      "RepeatMode": "RepeatNone",
+      "LiveStreamId": "string"
+    },
+    "AdditionalUsers": [
+      {
+        "UserId": "08ba1929-681e-4b24-929b-9245852f65c0",
+        "UserName": "string"
+      }
+    ],
+    "Capabilities": {
+      "PlayableMediaTypes": ["Video"],
+      "SupportedCommands": ["VolumeSet", "Mute"],
+      "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": ["Video"],
+    "Id": "SESSION-UUID-TWO",
+    "UserId": "USER-UUID-TWO",
+    "UserName": "string",
+    "Client": "Jellyfin for Developers",
+    "LastActivityDate": "2019-08-24T14:15:22Z",
+    "LastPlaybackCheckIn": "2019-08-24T14:15:22Z",
+    "DeviceName": "JELLYFIN-DEVICE-TWO",
+    "DeviceType": "string",
+    "NowPlayingItem": {
+      "Name": "MOVIE",
+      "OriginalTitle": "string",
+      "ServerId": "SERVER-UUID",
+      "Id": "EPISODE-UUID",
+      "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": 2000000000,
+      "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": false,
+      "ParentId": "",
+      "Type": "Movie",
+      "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": "",
+      "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": "SERIES",
+      "SeriesId": "SERIES-UUID",
+      "SeasonId": "SEASON-UUID",
+      "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": "SEASON",
+      "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": {
+        "Backdrop": "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": "HASS",
+      "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": "DEVICE-UUID-TWO",
+    "ApplicationVersion": "1.0.0",
+    "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": "SERVER-UUID",
+    "UserPrimaryImageTag": "string",
+    "SupportedCommands": ["MoveUp"]
+  },
+  {
+    "PlayState": {
+      "PositionTicks": 0,
+      "CanSeek": true,
+      "IsPaused": false,
+      "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": ["Video"],
+      "SupportedCommands": ["MoveUp"],
+      "SupportsMediaControl": false,
+      "SupportsContentUploading": false,
+      "MessageCallbackUrl": "string",
+      "SupportsPersistentIdentifier": false,
+      "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": ["Video"],
+    "Id": "SESSION-UUID-THREE",
+    "UserId": "USER-UUID",
+    "UserName": "string",
+    "Client": "Jellyfin for Developers",
+    "LastActivityDate": "2019-08-24T14:15:22Z",
+    "LastPlaybackCheckIn": "2019-08-24T14:15:22Z",
+    "DeviceName": "JELLYFIN-DEVICE-THREE",
+    "DeviceType": "string",
+    "DeviceId": "DEVICE-UUID-THREE",
+    "ApplicationVersion": "2.0.0",
+    "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": false,
+    "SupportsRemoteControl": false,
+    "NowPlayingQueue": [
+      {
+        "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43",
+        "PlaylistItemId": "string"
+      }
+    ],
+    "NowPlayingQueueFullItems": [
+      {
+        "Name": "string",
+        "OriginalTitle": "string",
+        "ServerId": "SERVER-UUID",
+        "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": "SERVER-UUID",
+    "UserPrimaryImageTag": "string",
+    "SupportedCommands": ["MoveUp"]
+  },
+  {
+    "PlayState": {
+      "PositionTicks": 220246970,
+      "CanSeek": true,
+      "IsPaused": false,
+      "IsMuted": false,
+      "VolumeLevel": 100,
+      "MediaSourceId": "a744119f757f88858f95aab1628708c4",
+      "PlayMethod": "DirectPlay",
+      "RepeatMode": "RepeatNone"
+    },
+    "AdditionalUsers": [],
+    "Capabilities": {
+      "PlayableMediaTypes": ["Audio", "Video"],
+      "SupportedCommands": [
+        "MoveUp",
+        "MoveDown",
+        "MoveLeft",
+        "MoveRight",
+        "PageUp",
+        "PageDown",
+        "PreviousLetter",
+        "NextLetter",
+        "ToggleOsd",
+        "ToggleContextMenu",
+        "Select",
+        "Back",
+        "SendKey",
+        "SendString",
+        "GoHome",
+        "GoToSettings",
+        "VolumeUp",
+        "VolumeDown",
+        "Mute",
+        "Unmute",
+        "ToggleMute",
+        "SetVolume",
+        "SetAudioStreamIndex",
+        "SetSubtitleStreamIndex",
+        "DisplayContent",
+        "GoToSearch",
+        "DisplayMessage",
+        "SetRepeatMode",
+        "SetShuffleQueue",
+        "ChannelUp",
+        "ChannelDown",
+        "PlayMediaSource",
+        "PlayTrailers"
+      ],
+      "SupportsMediaControl": true,
+      "SupportsContentUploading": false,
+      "SupportsPersistentIdentifier": false,
+      "SupportsSync": false
+    },
+    "RemoteEndPoint": "192.168.1.254",
+    "PlayableMediaTypes": ["Audio", "Video"],
+    "Id": "SESSION-UUID-FOUR",
+    "UserId": "USER-UUID-TWO",
+    "UserName": "USER",
+    "Client": "Jellyfin Android",
+    "LastActivityDate": "2022-10-19T03:20:20.1214274Z",
+    "LastPlaybackCheckIn": "2022-10-19T03:20:18.0973168Z",
+    "DeviceName": "JELLYFIN DEVICE FOUR",
+    "NowPlayingItem": {
+      "Name": "MUSIC FILE",
+      "ServerId": "SERVER-UUID",
+      "Id": "MUSIC-UUID",
+      "DateCreated": "2022-10-19T03:09:11.392057Z",
+      "ExternalUrls": [],
+      "Path": "string",
+      "EnableMediaSourceDisplay": true,
+      "ChannelId": null,
+      "Taglines": [],
+      "Genres": [],
+      "RunTimeTicks": 736391552,
+      "IndexNumber": 1,
+      "ProviderIds": {},
+      "IsFolder": false,
+      "ParentId": "4c0343ed1bbcda094178076230051b7e",
+      "Type": "Audio",
+      "Studios": [],
+      "GenreItems": [],
+      "LocalTrailerCount": 0,
+      "SpecialFeatureCount": 0,
+      "Artists": ["Contributing Artist"],
+      "ArtistItems": [
+        {
+          "Name": "Contributing Artist",
+          "Id": "1d864900526d9a9513b489f1cc28f8ca"
+        }
+      ],
+      "Album": "ALBUM",
+      "AlbumId": "ALBUM-UUID",
+      "AlbumArtist": "Album Artist",
+      "AlbumArtists": [
+        { "Name": "Album Artist", "Id": "9a65b2c222ddb34e51f5cae360fad3a1" }
+      ],
+      "MediaStreams": [
+        {
+          "Codec": "mp3",
+          "TimeBase": "1/14112000",
+          "DisplayTitle": "MP3 - Stereo",
+          "IsInterlaced": false,
+          "ChannelLayout": "stereo",
+          "BitRate": 256000,
+          "Channels": 2,
+          "SampleRate": 44100,
+          "IsDefault": false,
+          "IsForced": false,
+          "Type": "Audio",
+          "Index": 0,
+          "IsExternal": false,
+          "IsTextSubtitleStream": false,
+          "SupportsExternalStream": false,
+          "Level": 0
+        }
+      ],
+      "ImageTags": {},
+      "BackdropImageTags": [],
+      "ImageBlurHashes": {},
+      "LocationType": "FileSystem",
+      "MediaType": "Audio"
+    },
+    "FullNowPlayingItem": {
+      "Size": 2356453,
+      "IsHD": false,
+      "IsShortcut": false,
+      "Width": 0,
+      "Height": 0,
+      "ExtraIds": [],
+      "DateLastSaved": "2022-10-19T03:10:11.9765475Z",
+      "RemoteTrailers": [],
+      "SupportsExternalTransfer": false
+    },
+    "DeviceId": "DEVICE-UUID-FOUR",
+    "ApplicationVersion": "2.4.4",
+    "IsActive": true,
+    "SupportsMediaControl": true,
+    "SupportsRemoteControl": true,
+    "NowPlayingQueue": [
+      {
+        "Id": "a744119f757f88858f95aab1628708c4",
+        "PlaylistItemId": "playlistItem2"
+      }
+    ],
+    "NowPlayingQueueFullItems": [
+      {
+        "Name": "string",
+        "ServerId": "e1012aa74e1b40c8ac50f3af79e9e83f",
+        "Id": "a744119f757f88858f95aab1628708c4",
+        "Etag": "64ed7b4ce1127c5d41e685de30090383",
+        "DateCreated": "2022-10-19T03:09:11.392057Z",
+        "CanDelete": true,
+        "CanDownload": true,
+        "SortName": "string",
+        "ExternalUrls": [],
+        "MediaSources": [
+          {
+            "Protocol": "File",
+            "Id": "a744119f757f88858f95aab1628708c4",
+            "Path": "string",
+            "Type": "Default",
+            "Container": "mp3",
+            "Size": 2356453,
+            "Name": "string",
+            "IsRemote": false,
+            "ETag": "83b0e0ece75386b479a2c3a09f71d695",
+            "RunTimeTicks": 736391552,
+            "ReadAtNativeFramerate": false,
+            "IgnoreDts": false,
+            "IgnoreIndex": false,
+            "GenPtsInput": false,
+            "SupportsTranscoding": true,
+            "SupportsDirectStream": true,
+            "SupportsDirectPlay": true,
+            "IsInfiniteStream": false,
+            "RequiresOpening": false,
+            "RequiresClosing": false,
+            "RequiresLooping": false,
+            "SupportsProbing": true,
+            "MediaStreams": [
+              {
+                "Codec": "mp3",
+                "TimeBase": "1/14112000",
+                "DisplayTitle": "MP3 - Stereo",
+                "IsInterlaced": false,
+                "ChannelLayout": "stereo",
+                "BitRate": 256000,
+                "Channels": 2,
+                "SampleRate": 44100,
+                "IsDefault": false,
+                "IsForced": false,
+                "Type": "Audio",
+                "Index": 0,
+                "IsExternal": false,
+                "IsTextSubtitleStream": false,
+                "SupportsExternalStream": false,
+                "Level": 0
+              }
+            ],
+            "MediaAttachments": [],
+            "Formats": [],
+            "Bitrate": 256000,
+            "RequiredHttpHeaders": {}
+          }
+        ],
+        "Path": "string",
+        "EnableMediaSourceDisplay": true,
+        "ChannelId": null,
+        "Taglines": [],
+        "Genres": [],
+        "RunTimeTicks": 736391552,
+        "RemoteTrailers": [],
+        "ProviderIds": {},
+        "IsFolder": false,
+        "ParentId": "4c0343ed1bbcda094178076230051b7e",
+        "Type": "Audio",
+        "People": [],
+        "Studios": [],
+        "GenreItems": [],
+        "LocalTrailerCount": 0,
+        "SpecialFeatureCount": 0,
+        "DisplayPreferencesId": "61bba315f137702baa296a1c417faada",
+        "Tags": [],
+        "Artists": [],
+        "ArtistItems": [],
+        "AlbumArtists": [],
+        "MediaStreams": [
+          {
+            "Codec": "mp3",
+            "TimeBase": "1/14112000",
+            "DisplayTitle": "MP3 - Stereo",
+            "IsInterlaced": false,
+            "ChannelLayout": "stereo",
+            "BitRate": 256000,
+            "Channels": 2,
+            "SampleRate": 44100,
+            "IsDefault": false,
+            "IsForced": false,
+            "Type": "Audio",
+            "Index": 0,
+            "IsExternal": false,
+            "IsTextSubtitleStream": false,
+            "SupportsExternalStream": false,
+            "Level": 0
+          }
+        ],
+        "ImageTags": {},
+        "BackdropImageTags": [],
+        "ImageBlurHashes": {},
+        "LocationType": "FileSystem",
+        "MediaType": "Audio",
+        "LockedFields": [],
+        "LockData": false
+      }
+    ],
+    "HasCustomDeviceName": false,
+    "PlaylistItemId": "playlistItem2",
+    "ServerId": "SERVER-UUID",
+    "SupportedCommands": [
+      "MoveUp",
+      "MoveDown",
+      "MoveLeft",
+      "MoveRight",
+      "PageUp",
+      "PageDown",
+      "PreviousLetter",
+      "NextLetter",
+      "ToggleOsd",
+      "ToggleContextMenu",
+      "Select",
+      "Back",
+      "SendKey",
+      "SendString",
+      "GoHome",
+      "GoToSettings",
+      "VolumeUp",
+      "VolumeDown",
+      "Mute",
+      "Unmute",
+      "ToggleMute",
+      "SetVolume",
+      "SetAudioStreamIndex",
+      "SetSubtitleStreamIndex",
+      "DisplayContent",
+      "GoToSearch",
+      "DisplayMessage",
+      "SetRepeatMode",
+      "SetShuffleQueue",
+      "ChannelUp",
+      "ChannelDown",
+      "PlayMediaSource",
+      "PlayTrailers"
+    ]
   }
 ]
diff --git a/tests/components/jellyfin/test_diagnostics.py b/tests/components/jellyfin/test_diagnostics.py
new file mode 100644
index 00000000000..b4c386be956
--- /dev/null
+++ b/tests/components/jellyfin/test_diagnostics.py
@@ -0,0 +1,611 @@
+"""Test Jellyfin diagnostics."""
+from aiohttp import ClientSession
+
+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,
+    init_integration: MockConfigEntry,
+    hass_client: ClientSession,
+):
+    """Test generating diagnostics for a config entry."""
+    entry = init_integration
+
+    diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
+    assert diag
+    assert diag["entry"] == {
+        "title": "Jellyfin",
+        "data": {
+            "url": "https://example.com",
+            "username": "test-username",
+            "password": "**REDACTED**",
+            "client_device_id": entry.entry_id,
+        },
+    }
+    assert diag["server"] == {
+        "id": "SERVER-UUID",
+        "name": "JELLYFIN-SERVER",
+        "version": None,
+    }
+    assert diag["sessions"]
+    assert len(diag["sessions"]) == 4
+    assert diag["sessions"][0] == {
+        "id": "SESSION-UUID",
+        "user_id": "08ba1929-681e-4b24-929b-9245852f65c0",
+        "device_id": "DEVICE-UUID",
+        "device_name": "JELLYFIN-DEVICE",
+        "client_name": "Jellyfin for Developers",
+        "client_version": "1.0.0",
+        "capabilities": {
+            "PlayableMediaTypes": ["Video"],
+            "SupportedCommands": ["VolumeSet", "Mute"],
+            "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",
+        },
+        "now_playing": {
+            "Name": "EPISODE",
+            "OriginalTitle": "string",
+            "ServerId": "SERVER-UUID",
+            "Id": "EPISODE-UUID",
+            "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": 600000000,
+            "PlayAccess": "Full",
+            "AspectRatio": "string",
+            "ProductionYear": 0,
+            "IsPlaceHolder": True,
+            "Number": "string",
+            "ChannelNumber": "string",
+            "IndexNumber": 3,
+            "IndexNumberEnd": 0,
+            "ParentIndexNumber": 1,
+            "RemoteTrailers": [{"Url": "string", "Name": "string"}],
+            "ProviderIds": {"property1": "string", "property2": "string"},
+            "IsHD": True,
+            "IsFolder": False,
+            "ParentId": "PARENT-UUID",
+            "Type": "Episode",
+            "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": "SERIES",
+            "SeriesId": "SERIES-UUID",
+            "SeasonId": "SEASON-UUID",
+            "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": "SEASON",
+            "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": "HASS",
+            "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": {},
+        },
+        "play_state": {
+            "PositionTicks": 100000000,
+            "CanSeek": True,
+            "IsPaused": True,
+            "IsMuted": True,
+            "VolumeLevel": 0,
+            "AudioStreamIndex": 0,
+            "SubtitleStreamIndex": 0,
+            "MediaSourceId": "string",
+            "PlayMethod": "Transcode",
+            "RepeatMode": "RepeatNone",
+            "LiveStreamId": "string",
+        },
+    }
diff --git a/tests/components/jellyfin/test_sensor.py b/tests/components/jellyfin/test_sensor.py
index 6b52fe442d9..087be30b70c 100644
--- a/tests/components/jellyfin/test_sensor.py
+++ b/tests/components/jellyfin/test_sensor.py
@@ -31,7 +31,7 @@ async def test_watching(
     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"
+    assert state.state == "3"
 
     entry = entity_registry.async_get(state.entity_id)
     assert entry
-- 
GitLab


From 60b3d6816b0a24a3a5043ff09cb311dba0b8a436 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Thu, 20 Oct 2022 19:37:20 -0600
Subject: [PATCH 646/985] Replace custom OpenUV data object with coordinators
 (#80705)

* Replace custom OpenUV data object with coordinators

* Typing

* Code review
---
 homeassistant/components/openuv/__init__.py   | 225 ++++++------------
 .../components/openuv/binary_sensor.py        |  30 +--
 .../components/openuv/coordinator.py          |  55 +++++
 .../components/openuv/diagnostics.py          |  20 +-
 homeassistant/components/openuv/sensor.py     |  25 +-
 tests/components/openuv/test_diagnostics.py   |   7 -
 6 files changed, 168 insertions(+), 194 deletions(-)
 create mode 100644 homeassistant/components/openuv/coordinator.py

diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py
index 365a3ab247a..1e85e70100f 100644
--- a/homeassistant/components/openuv/__init__.py
+++ b/homeassistant/components/openuv/__init__.py
@@ -2,11 +2,9 @@
 from __future__ import annotations
 
 import asyncio
-from collections.abc import Callable
 from typing import Any
 
 from pyopenuv import Client
-from pyopenuv.errors import OpenUvError
 import voluptuous as vol
 
 from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -20,20 +18,16 @@ from homeassistant.const import (
     Platform,
 )
 from homeassistant.core import HomeAssistant, ServiceCall, callback
-from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers import (
     aiohttp_client,
     config_validation as cv,
     entity_registry,
 )
-from homeassistant.helpers.debounce import Debouncer
-from homeassistant.helpers.dispatcher import (
-    async_dispatcher_connect,
-    async_dispatcher_send,
-)
-from homeassistant.helpers.entity import Entity, EntityDescription
+from homeassistant.helpers.entity import EntityDescription
 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.helpers.service import verify_domain_control
+from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed
 
 from .const import (
     CONF_FROM_WINDOW,
@@ -45,13 +39,10 @@ from .const import (
     DOMAIN,
     LOGGER,
 )
+from .coordinator import OpenUvCoordinator
 
 CONF_ENTRY_ID = "entry_id"
 
-DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60
-
-TOPIC_UPDATE = f"{DOMAIN}_data_update"
-
 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
 
 SERVICE_NAME_UPDATE_DATA = "update_data"
@@ -127,53 +118,50 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     _verify_domain_control = verify_domain_control(hass, DOMAIN)
 
     websession = aiohttp_client.async_get_clientsession(hass)
-    openuv = OpenUV(
-        hass,
-        entry,
-        Client(
-            entry.data[CONF_API_KEY],
-            entry.data.get(CONF_LATITUDE, hass.config.latitude),
-            entry.data.get(CONF_LONGITUDE, hass.config.longitude),
-            altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation),
-            session=websession,
-        ),
+    client = Client(
+        entry.data[CONF_API_KEY],
+        entry.data.get(CONF_LATITUDE, hass.config.latitude),
+        entry.data.get(CONF_LONGITUDE, hass.config.longitude),
+        altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation),
+        session=websession,
     )
 
-    # We disable the client's request retry abilities here to avoid a lengthy (and
-    # blocking) startup:
-    openuv.client.disable_request_retries()
+    async def async_update_protection_data() -> dict[str, Any]:
+        """Update binary sensor (protection window) data."""
+        low = entry.options.get(CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW)
+        high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW)
+        return await client.uv_protection_window(low=low, high=high)
 
-    try:
-        await openuv.async_update()
-    except HomeAssistantError as err:
-        LOGGER.error("Config entry failed: %s", err)
-        raise ConfigEntryNotReady from err
+    coordinators: dict[str, OpenUvCoordinator] = {
+        coordinator_name: OpenUvCoordinator(
+            hass,
+            name=coordinator_name,
+            latitude=client.latitude,
+            longitude=client.longitude,
+            update_method=update_method,
+        )
+        for coordinator_name, update_method in (
+            (DATA_UV, client.uv_index),
+            (DATA_PROTECTION_WINDOW, async_update_protection_data),
+        )
+    }
 
-    # Once we've successfully authenticated, we re-enable client request retries:
-    openuv.client.enable_request_retries()
+    # We disable the client's request retry abilities here to avoid a lengthy (and
+    # blocking) startup; then, if the initial update is successful, we re-enable client
+    # request retries:
+    client.disable_request_retries()
+    init_tasks = [
+        coordinator.async_config_entry_first_refresh()
+        for coordinator in coordinators.values()
+    ]
+    await asyncio.gather(*init_tasks)
+    client.enable_request_retries()
 
     hass.data.setdefault(DOMAIN, {})
-    hass.data[DOMAIN][entry.entry_id] = openuv
+    hass.data[DOMAIN][entry.entry_id] = coordinators
 
     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
 
-    @callback
-    def extract_openuv(func: Callable) -> Callable:
-        """Define a decorator to get the correct OpenUV object for a service call."""
-
-        async def wrapper(call: ServiceCall) -> None:
-            """Wrap the service function."""
-            openuv: OpenUV = hass.data[DOMAIN][call.data[CONF_ENTRY_ID]]
-
-            try:
-                await func(call, openuv)
-            except OpenUvError as err:
-                raise HomeAssistantError(
-                    f'Error while executing "{call.service}": {err}'
-                ) from err
-
-        return wrapper
-
     # We determine entity IDs needed to help the user migrate from deprecated services:
     current_uv_index_entity_id = async_get_entity_id_from_unique_id_suffix(
         hass, entry, "current_uv_index"
@@ -183,8 +171,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     )
 
     @_verify_domain_control
-    @extract_openuv
-    async def update_data(call: ServiceCall, openuv: OpenUV) -> None:
+    async def update_data(call: ServiceCall) -> None:
         """Refresh all OpenUV data."""
         LOGGER.debug("Refreshing all OpenUV data")
         async_log_deprecated_service_call(
@@ -194,12 +181,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
             [protection_window_entity_id, current_uv_index_entity_id],
             "2022.12.0",
         )
-        await openuv.async_update()
-        async_dispatcher_send(hass, TOPIC_UPDATE)
+
+        tasks = [coordinator.async_refresh() for coordinator in coordinators.values()]
+        try:
+            await asyncio.gather(*tasks)
+        except UpdateFailed as err:
+            raise HomeAssistantError(err) from err
 
     @_verify_domain_control
-    @extract_openuv
-    async def update_uv_index_data(call: ServiceCall, openuv: OpenUV) -> None:
+    async def update_uv_index_data(call: ServiceCall) -> None:
         """Refresh OpenUV UV index data."""
         LOGGER.debug("Refreshing OpenUV UV index data")
         async_log_deprecated_service_call(
@@ -209,12 +199,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
             [current_uv_index_entity_id],
             "2022.12.0",
         )
-        await openuv.async_update_uv_index_data()
-        async_dispatcher_send(hass, TOPIC_UPDATE)
+
+        try:
+            await coordinators[DATA_UV].async_request_refresh()
+        except UpdateFailed as err:
+            raise HomeAssistantError(err) from err
 
     @_verify_domain_control
-    @extract_openuv
-    async def update_protection_data(call: ServiceCall, openuv: OpenUV) -> None:
+    async def update_protection_data(call: ServiceCall) -> None:
         """Refresh OpenUV protection window data."""
         LOGGER.debug("Refreshing OpenUV protection window data")
         async_log_deprecated_service_call(
@@ -224,8 +216,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
             [protection_window_entity_id],
             "2022.12.0",
         )
-        await openuv.async_update_protection_data()
-        async_dispatcher_send(hass, TOPIC_UPDATE)
+
+        try:
+            await coordinators[DATA_PROTECTION_WINDOW].async_request_refresh()
+        except UpdateFailed as err:
+            raise HomeAssistantError(err) from err
 
     service_schema = vol.Schema(
         {
@@ -283,106 +278,42 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     return True
 
 
-class OpenUV:
-    """Define a generic OpenUV object."""
-
-    def __init__(self, hass: HomeAssistant, entry: ConfigEntry, client: Client) -> None:
-        """Initialize."""
-        self._update_protection_data_debouncer = Debouncer(
-            hass,
-            LOGGER,
-            cooldown=DEFAULT_DEBOUNCER_COOLDOWN_SECONDS,
-            immediate=True,
-            function=self._async_update_protection_data,
-        )
-
-        self._update_uv_index_data_debouncer = Debouncer(
-            hass,
-            LOGGER,
-            cooldown=DEFAULT_DEBOUNCER_COOLDOWN_SECONDS,
-            immediate=True,
-            function=self._async_update_uv_index_data,
-        )
-
-        self._entry = entry
-        self.client = client
-        self.data: dict[str, Any] = {DATA_PROTECTION_WINDOW: {}, DATA_UV: {}}
-
-    async def _async_update_protection_data(self) -> None:
-        """Update binary sensor (protection window) data."""
-        low = self._entry.options.get(CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW)
-        high = self._entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW)
-
-        try:
-            data = await self.client.uv_protection_window(low=low, high=high)
-        except OpenUvError as err:
-            raise HomeAssistantError(
-                f"Error during protection data update: {err}"
-            ) from err
-
-        self.data[DATA_PROTECTION_WINDOW] = data.get("result")
-
-    async def _async_update_uv_index_data(self) -> None:
-        """Update sensor (uv index, etc) data."""
-        try:
-            data = await self.client.uv_index()
-        except OpenUvError as err:
-            raise HomeAssistantError(
-                f"Error during UV index data update: {err}"
-            ) from err
-
-        self.data[DATA_UV] = data.get("result")
-
-    async def async_update_protection_data(self) -> None:
-        """Update binary sensor (protection window) data with a debouncer."""
-        await self._update_protection_data_debouncer.async_call()
-
-    async def async_update_uv_index_data(self) -> None:
-        """Update sensor (uv index, etc) data with a debouncer."""
-        await self._update_uv_index_data_debouncer.async_call()
-
-    async def async_update(self) -> None:
-        """Update sensor/binary sensor data."""
-        tasks = [self.async_update_protection_data(), self.async_update_uv_index_data()]
-        await asyncio.gather(*tasks)
-
-
-class OpenUvEntity(Entity):
+class OpenUvEntity(CoordinatorEntity):
     """Define a generic OpenUV entity."""
 
     _attr_has_entity_name = True
 
-    def __init__(self, openuv: OpenUV, description: EntityDescription) -> None:
+    def __init__(
+        self, coordinator: OpenUvCoordinator, description: EntityDescription
+    ) -> None:
         """Initialize."""
+        super().__init__(coordinator)
+
         self._attr_extra_state_attributes = {}
-        self._attr_should_poll = False
         self._attr_unique_id = (
-            f"{openuv.client.latitude}_{openuv.client.longitude}_{description.key}"
+            f"{coordinator.latitude}_{coordinator.longitude}_{description.key}"
         )
         self.entity_description = description
-        self.openuv = openuv
 
     @callback
-    def async_update_state(self) -> None:
-        """Update the state."""
-        self.update_from_latest_data()
+    def _handle_coordinator_update(self) -> None:
+        """Respond to a DataUpdateCoordinator update."""
+        self._update_from_latest_data()
         self.async_write_ha_state()
 
+    @callback
+    def _update_from_latest_data(self) -> None:
+        """Update the entity from the latest data."""
+        raise NotImplementedError
+
     async def async_added_to_hass(self) -> None:
-        """Register callbacks."""
-        self.update_from_latest_data()
-        self.async_on_remove(
-            async_dispatcher_connect(self.hass, TOPIC_UPDATE, self.async_update_state)
-        )
+        """Handle entity which will be added."""
+        await super().async_added_to_hass()
+        self._update_from_latest_data()
 
     async def async_update(self) -> None:
         """Update the entity.
 
-        Only used by the generic entity update service. Should be implemented by each
-        OpenUV platform.
+        Only used by the generic entity update service.
         """
-        raise NotImplementedError
-
-    def update_from_latest_data(self) -> None:
-        """Update the sensor using the latest data."""
-        raise NotImplementedError
+        await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py
index b1c962932b7..1e69af66eec 100644
--- a/homeassistant/components/openuv/binary_sensor.py
+++ b/homeassistant/components/openuv/binary_sensor.py
@@ -10,6 +10,7 @@ from homeassistant.util.dt import as_local, parse_datetime, utcnow
 
 from . import OpenUvEntity
 from .const import DATA_PROTECTION_WINDOW, DOMAIN, LOGGER, TYPE_PROTECTION_WINDOW
+from .coordinator import OpenUvCoordinator
 
 ATTR_PROTECTION_WINDOW_ENDING_TIME = "end_time"
 ATTR_PROTECTION_WINDOW_ENDING_UV = "end_uv"
@@ -26,32 +27,27 @@ BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW = BinarySensorEntityDescription(
 async def async_setup_entry(
     hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
 ) -> None:
+    # Once we've successfully authenticated, we re-enable client request retries:
     """Set up an OpenUV sensor based on a config entry."""
-    openuv = hass.data[DOMAIN][entry.entry_id]
+    coordinators: dict[str, OpenUvCoordinator] = hass.data[DOMAIN][entry.entry_id]
+
     async_add_entities(
-        [OpenUvBinarySensor(openuv, BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW)]
+        [
+            OpenUvBinarySensor(
+                coordinators[DATA_PROTECTION_WINDOW],
+                BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW,
+            )
+        ]
     )
 
 
 class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
     """Define a binary sensor for OpenUV."""
 
-    async def async_update(self) -> None:
-        """Update the entity.
-
-        Only used by the generic entity update service.
-        """
-        await self.openuv.async_update_protection_data()
-        self.async_update_state()
-
     @callback
-    def update_from_latest_data(self) -> None:
-        """Update the state."""
-        if not (data := self.openuv.data[DATA_PROTECTION_WINDOW]):
-            self._attr_available = False
-            return
-
-        self._attr_available = True
+    def _update_from_latest_data(self) -> None:
+        """Update the entity from the latest data."""
+        data = self.coordinator.data
 
         for key in ("from_time", "to_time", "from_uv", "to_uv"):
             if not data.get(key):
diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py
new file mode 100644
index 00000000000..993970658ef
--- /dev/null
+++ b/homeassistant/components/openuv/coordinator.py
@@ -0,0 +1,55 @@
+"""Define an update coordinator for OpenUV."""
+from __future__ import annotations
+
+from collections.abc import Awaitable, Callable
+from typing import Any, cast
+
+from pyopenuv.errors import OpenUvError
+
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.debounce import Debouncer
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import LOGGER
+
+DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60
+
+
+class OpenUvCoordinator(DataUpdateCoordinator):
+    """Define an OpenUV data coordinator."""
+
+    update_method: Callable[[], Awaitable[dict[str, Any]]]
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        *,
+        name: str,
+        latitude: str,
+        longitude: str,
+        update_method: Callable[[], Awaitable[dict[str, Any]]],
+    ) -> None:
+        """Initialize."""
+        super().__init__(
+            hass,
+            LOGGER,
+            name=name,
+            update_method=update_method,
+            request_refresh_debouncer=Debouncer(
+                hass,
+                LOGGER,
+                cooldown=DEFAULT_DEBOUNCER_COOLDOWN_SECONDS,
+                immediate=True,
+            ),
+        )
+
+        self.latitude = latitude
+        self.longitude = longitude
+
+    async def _async_update_data(self) -> dict[str, Any]:
+        """Fetch data from OpenUV."""
+        try:
+            data = await self.update_method()
+        except OpenUvError as err:
+            raise UpdateFailed(f"Error during protection data update: {err}") from err
+        return cast(dict[str, Any], data["result"])
diff --git a/homeassistant/components/openuv/diagnostics.py b/homeassistant/components/openuv/diagnostics.py
index 30443dd90fc..99c5f89d456 100644
--- a/homeassistant/components/openuv/diagnostics.py
+++ b/homeassistant/components/openuv/diagnostics.py
@@ -13,8 +13,8 @@ from homeassistant.const import (
 )
 from homeassistant.core import HomeAssistant
 
-from . import OpenUV
 from .const import DOMAIN
+from .coordinator import OpenUvCoordinator
 
 CONF_COORDINATES = "coordinates"
 CONF_TITLE = "title"
@@ -33,9 +33,15 @@ async def async_get_config_entry_diagnostics(
     hass: HomeAssistant, entry: ConfigEntry
 ) -> dict[str, Any]:
     """Return diagnostics for a config entry."""
-    openuv: OpenUV = hass.data[DOMAIN][entry.entry_id]
-
-    return {
-        "entry": async_redact_data(entry.as_dict(), TO_REDACT),
-        "data": async_redact_data(openuv.data, TO_REDACT),
-    }
+    coordinators: dict[str, OpenUvCoordinator] = hass.data[DOMAIN][entry.entry_id]
+
+    return async_redact_data(
+        {
+            "entry": entry.as_dict(),
+            "data": {
+                coordinator_name: coordinator.data
+                for coordinator_name, coordinator in coordinators.items()
+            },
+        },
+        TO_REDACT,
+    )
diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py
index ff28062da37..dd8d1587f49 100644
--- a/homeassistant/components/openuv/sensor.py
+++ b/homeassistant/components/openuv/sensor.py
@@ -28,6 +28,7 @@ from .const import (
     TYPE_SAFE_EXPOSURE_TIME_5,
     TYPE_SAFE_EXPOSURE_TIME_6,
 )
+from .coordinator import OpenUvCoordinator
 
 ATTR_MAX_UV_TIME = "time"
 
@@ -122,31 +123,23 @@ async def async_setup_entry(
     hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
 ) -> None:
     """Set up a OpenUV sensor based on a config entry."""
-    openuv = hass.data[DOMAIN][entry.entry_id]
+    coordinators: dict[str, OpenUvCoordinator] = hass.data[DOMAIN][entry.entry_id]
+
     async_add_entities(
-        [OpenUvSensor(openuv, description) for description in SENSOR_DESCRIPTIONS]
+        [
+            OpenUvSensor(coordinators[DATA_UV], description)
+            for description in SENSOR_DESCRIPTIONS
+        ]
     )
 
 
 class OpenUvSensor(OpenUvEntity, SensorEntity):
     """Define a binary sensor for OpenUV."""
 
-    async def async_update(self) -> None:
-        """Update the entity.
-
-        Only used by the generic entity update service.
-        """
-        await self.openuv.async_update_uv_index_data()
-        self.async_update_state()
-
     @callback
-    def update_from_latest_data(self) -> None:
+    def _update_from_latest_data(self) -> None:
         """Update the state."""
-        if (data := self.openuv.data[DATA_UV]) is None:
-            self._attr_available = False
-            return
-
-        self._attr_available = True
+        data = self.coordinator.data
 
         if self.entity_description.key == TYPE_CURRENT_OZONE_LEVEL:
             self._attr_native_value = data["ozone"]
diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py
index 0fb88d9cda4..84e8a691255 100644
--- a/tests/components/openuv/test_diagnostics.py
+++ b/tests/components/openuv/test_diagnostics.py
@@ -1,6 +1,5 @@
 """Test OpenUV diagnostics."""
 from homeassistant.components.diagnostics import REDACTED
-from homeassistant.const import CONF_ENTITY_ID
 from homeassistant.setup import async_setup_component
 
 from tests.components.diagnostics import get_diagnostics_for_config_entry
@@ -9,12 +8,6 @@ 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(
-        "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": {
             "entry_id": config_entry.entry_id,
-- 
GitLab


From b35cfe711a7032bc1e41b685ea180277abc99edb Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
Date: Fri, 21 Oct 2022 04:32:45 +0200
Subject: [PATCH 647/985] Move default option handling to config_flow for
 google_travel_time (#80607)

Move default option handling to config_flow
---
 .../google_travel_time/config_flow.py         | 16 ++++++++-
 .../components/google_travel_time/sensor.py   | 33 -------------------
 .../google_travel_time/test_config_flow.py    | 11 +++++--
 .../google_travel_time/test_sensor.py         | 29 ++--------------
 4 files changed, 26 insertions(+), 63 deletions(-)

diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py
index 3a4c576f218..133baddf704 100644
--- a/homeassistant/components/google_travel_time/config_flow.py
+++ b/homeassistant/components/google_travel_time/config_flow.py
@@ -5,9 +5,10 @@ import voluptuous as vol
 
 from homeassistant import config_entries
 from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
 from homeassistant.data_entry_flow import FlowResult
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import (
     ALL_LANGUAGES,
@@ -34,10 +35,22 @@ from .const import (
     TRAVEL_MODE,
     TRAVEL_MODEL,
     UNITS,
+    UNITS_IMPERIAL,
+    UNITS_METRIC,
 )
 from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
 
 
+def default_options(hass: HomeAssistant) -> dict[str, str | None]:
+    """Get the default options."""
+    return {
+        CONF_MODE: "driving",
+        CONF_UNITS: (
+            UNITS_IMPERIAL if hass.config.units is IMPERIAL_SYSTEM else UNITS_METRIC
+        ),
+    }
+
+
 class GoogleOptionsFlow(config_entries.OptionsFlow):
     """Handle an options flow for Google Travel Time."""
 
@@ -135,6 +148,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
                 return self.async_create_entry(
                     title=user_input.get(CONF_NAME, DEFAULT_NAME),
                     data=user_input,
+                    options=default_options(self.hass),
                 )
             except InvalidApiKeyException:
                 errors["base"] = "invalid_auth"
diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py
index 412f1738294..e75db1a29e9 100644
--- a/homeassistant/components/google_travel_time/sensor.py
+++ b/homeassistant/components/google_travel_time/sensor.py
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import SensorEntity
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
     CONF_API_KEY,
-    CONF_MODE,
     CONF_NAME,
     EVENT_HOMEASSISTANT_STARTED,
     TIME_MINUTES,
@@ -22,21 +21,15 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.location import find_coordinates
 import homeassistant.util.dt as dt_util
-from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     ATTRIBUTION,
     CONF_ARRIVAL_TIME,
     CONF_DEPARTURE_TIME,
     CONF_DESTINATION,
-    CONF_OPTIONS,
     CONF_ORIGIN,
-    CONF_TRAVEL_MODE,
-    CONF_UNITS,
     DEFAULT_NAME,
     DOMAIN,
-    UNITS_IMPERIAL,
-    UNITS_METRIC,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -60,32 +53,6 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a Google travel time sensor entry."""
-    if not config_entry.options:
-        new_data = config_entry.data.copy()
-        options = new_data.pop(CONF_OPTIONS, {})
-
-        if CONF_UNITS not in options:
-            options[CONF_UNITS] = UNITS_METRIC
-            if hass.config.units is US_CUSTOMARY_SYSTEM:
-                options[CONF_UNITS] = UNITS_IMPERIAL
-
-        if CONF_TRAVEL_MODE in new_data:
-            wstr = (
-                "Google Travel Time: travel_mode is deprecated, please "
-                "add mode to the options dictionary instead!"
-            )
-            _LOGGER.warning(wstr)
-            travel_mode = new_data.pop(CONF_TRAVEL_MODE)
-            if CONF_MODE not in options:
-                options[CONF_MODE] = travel_mode
-
-        if CONF_MODE not in options:
-            options[CONF_MODE] = "driving"
-
-        hass.config_entries.async_update_entry(
-            config_entry, data=new_data, options=options
-        )
-
     api_key = config_entry.data[CONF_API_KEY]
     origin = config_entry.data[CONF_ORIGIN]
     destination = config_entry.data[CONF_DESTINATION]
diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py
index ff7e0768301..9ddcee5cdac 100644
--- a/tests/components/google_travel_time/test_config_flow.py
+++ b/tests/components/google_travel_time/test_config_flow.py
@@ -141,7 +141,6 @@ async def test_malformed_api_key(hass):
             MOCK_CONFIG,
             {
                 CONF_MODE: "driving",
-                CONF_ARRIVAL_TIME: "test",
                 CONF_UNITS: UNITS_IMPERIAL,
             },
         )
@@ -198,7 +197,15 @@ async def test_options_flow(hass, mock_config):
 
 @pytest.mark.parametrize(
     "data,options",
-    [(MOCK_CONFIG, {})],
+    [
+        (
+            MOCK_CONFIG,
+            {
+                CONF_MODE: "driving",
+                CONF_UNITS: UNITS_IMPERIAL,
+            },
+        )
+    ],
 )
 @pytest.mark.usefixtures("validate_config_entry")
 async def test_options_flow_departure_time(hass, mock_config):
diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py
index 8d560d895f2..d0a94712fcb 100644
--- a/tests/components/google_travel_time/test_sensor.py
+++ b/tests/components/google_travel_time/test_sensor.py
@@ -4,10 +4,10 @@ from unittest.mock import patch
 
 import pytest
 
+from homeassistant.components.google_travel_time.config_flow import default_options
 from homeassistant.components.google_travel_time.const import (
     CONF_ARRIVAL_TIME,
     CONF_DEPARTURE_TIME,
-    CONF_TRAVEL_MODE,
     DOMAIN,
 )
 from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC
@@ -202,31 +202,6 @@ async def test_sensor_arrival_time_custom_timestamp(hass):
     assert hass.states.get("sensor.google_travel_time").state == "27"
 
 
-@pytest.mark.usefixtures("mock_update")
-async def test_sensor_deprecation_warning(hass, caplog):
-    """Test that sensor setup prints a deprecating warning for old configs.
-
-    The mock_config fixture does not work with caplog.
-    """
-    data = MOCK_CONFIG.copy()
-    data[CONF_TRAVEL_MODE] = "driving"
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        data=data,
-        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()
-
-    assert hass.states.get("sensor.google_travel_time").state == "27"
-    wstr = (
-        "Google Travel Time: travel_mode is deprecated, please "
-        "add mode to the options dictionary instead!"
-    )
-    assert wstr in caplog.text
-
-
 @pytest.mark.parametrize(
     "unit_system, expected_unit_option",
     [
@@ -245,7 +220,7 @@ async def test_sensor_unit_system(
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         data=MOCK_CONFIG,
-        options={},
+        options=default_options(hass),
         entry_id="test",
     )
     config_entry.add_to_hass(hass)
-- 
GitLab


From 57bf1308376c138b81728533dab00ff05609ecae Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
Date: Fri, 21 Oct 2022 05:06:33 +0200
Subject: [PATCH 648/985] Bump pyatmo to 7.2.0 (#80698)

---
 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 1e3354f1c27..28a4bad2dc5 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.1"],
+  "requirements": ["pyatmo==7.2.0"],
   "after_dependencies": ["cloud", "media_source"],
   "dependencies": ["application_credentials", "webhook"],
   "codeowners": ["@cgtobi"],
diff --git a/requirements_all.txt b/requirements_all.txt
index 6650e92b308..3f621e810e5 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.1
+pyatmo==7.2.0
 
 # homeassistant.components.atome
 pyatome==0.1.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 9bd531b0a3b..7a76806115f 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.1
+pyatmo==7.2.0
 
 # homeassistant.components.apple_tv
 pyatv==0.10.3
-- 
GitLab


From 6c23de94e19112b82252cb8948767e74d8e4ac69 Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
Date: Fri, 21 Oct 2022 05:06:49 +0200
Subject: [PATCH 649/985] Fix error when setting Netatmo climate preset
 (#80700)

---
 homeassistant/components/netatmo/climate.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index 400004ee4d1..15b3e35ce05 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -315,7 +315,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
         elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX):
             await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode])
         elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY):
-            await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode])
+            await self._room.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode])
         else:
             _LOGGER.error("Preset mode '%s' not available", preset_mode)
 
-- 
GitLab


From bb287dd0ed07e8aa0725162393865d2a5a5cffcf Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 21 Oct 2022 05:09:06 +0200
Subject: [PATCH 650/985] Integrations v2.1: Virtual integrations (#80613)

---
 .../components/3_day_blinds/manifest.json     |    6 +
 .../components/amp_motorization/manifest.json |    6 +
 .../components/august_ble/manifest.json       |    6 +
 .../components/bliss_automation/manifest.json |    6 +
 .../components/bloc_blinds/manifest.json      |    6 +
 .../components/brel_home/manifest.json        |    6 +
 .../components/bswitch/manifest.json          |    6 +
 .../components/bticino/manifest.json          |    6 +
 .../components/bubendorff/manifest.json       |    6 +
 .../components/cozytouch/manifest.json        |    6 +
 homeassistant/components/dacia/manifest.json  |    6 +
 .../components/denonavr/manifest.json         |    5 +-
 homeassistant/components/diaz/manifest.json   |    6 +
 .../components/digital_loggers/manifest.json  |    6 +
 homeassistant/components/dooya/manifest.json  |    6 +
 homeassistant/components/flexom/manifest.json |    6 +
 .../components/gaviota/manifest.json          |    6 +
 homeassistant/components/gree/manifest.json   |    5 +-
 .../components/havana_shade/manifest.json     |    6 +
 homeassistant/components/heiwa/manifest.json  |    6 +
 .../components/hi_kumo/manifest.json          |    6 +
 .../hunterdouglas_powerview/manifest.json     |    5 +-
 .../hurrican_shutters_wholesale/manifest.json |    6 +
 .../components/inkbird/manifest.json          |    5 +-
 .../components/inspired_shades/manifest.json  |    6 +
 .../components/ismartwindow/manifest.json     |    6 +
 .../components/legrand/manifest.json          |    6 +
 .../components/luxaflex/manifest.json         |    6 +
 .../components/marantz/manifest.json          |    6 +
 homeassistant/components/martec/manifest.json |    6 +
 .../components/motion_blinds/manifest.json    |   22 +-
 .../components/netatmo/manifest.json          |    8 +-
 homeassistant/components/nexity/manifest.json |    6 +
 .../components/nutrichef/manifest.json        |    6 +
 .../components/overkiz/manifest.json          |   10 +-
 .../components/pcs_lighting/manifest.json     |    6 +
 .../components/raven_rock_mfg/manifest.json   |    6 +
 .../components/renault/manifest.json          |    3 +-
 homeassistant/components/rexel/manifest.json  |    6 +
 .../components/roborock/manifest.json         |    6 +
 .../components/screenaway/manifest.json       |    6 +
 .../components/sensorblue/manifest.json       |    6 +
 .../components/simply_automated/manifest.json |    6 +
 .../components/smart_blinds/manifest.json     |    6 +
 .../components/smart_home/manifest.json       |    6 +
 .../components/smarther/manifest.json         |    6 +
 homeassistant/components/somfy/manifest.json  |    6 +
 .../components/switchbee/manifest.json        |    5 +-
 .../components/thermobeacon/manifest.json     |    6 +-
 .../components/thermoplus/manifest.json       |    6 +
 homeassistant/components/upb/manifest.json    |    6 +-
 .../uprise_smart_shades/manifest.json         |    6 +
 .../components/websocket_api/commands.py      |   27 -
 homeassistant/components/wemo/manifest.json   |    5 +-
 .../components/xiaomi_miio/manifest.json      |    5 +-
 .../components/yalexs_ble/manifest.json       |    5 +-
 homeassistant/generated/integrations.json     | 5109 +++++++++--------
 homeassistant/generated/supported_brands.py   |   21 -
 homeassistant/loader.py                       |    1 -
 script/hassfest/__main__.py                   |    2 -
 script/hassfest/codeowners.py                 |    5 +-
 script/hassfest/config_flow.py                |   16 +-
 script/hassfest/manifest.py                   |   45 +-
 script/hassfest/model.py                      |   20 +-
 script/hassfest/supported_brands.py           |   54 -
 .../components/websocket_api/test_commands.py |   47 +-
 66 files changed, 2968 insertions(+), 2720 deletions(-)
 create mode 100644 homeassistant/components/3_day_blinds/manifest.json
 create mode 100644 homeassistant/components/amp_motorization/manifest.json
 create mode 100644 homeassistant/components/august_ble/manifest.json
 create mode 100644 homeassistant/components/bliss_automation/manifest.json
 create mode 100644 homeassistant/components/bloc_blinds/manifest.json
 create mode 100644 homeassistant/components/brel_home/manifest.json
 create mode 100644 homeassistant/components/bswitch/manifest.json
 create mode 100644 homeassistant/components/bticino/manifest.json
 create mode 100644 homeassistant/components/bubendorff/manifest.json
 create mode 100644 homeassistant/components/cozytouch/manifest.json
 create mode 100644 homeassistant/components/dacia/manifest.json
 create mode 100644 homeassistant/components/diaz/manifest.json
 create mode 100644 homeassistant/components/digital_loggers/manifest.json
 create mode 100644 homeassistant/components/dooya/manifest.json
 create mode 100644 homeassistant/components/flexom/manifest.json
 create mode 100644 homeassistant/components/gaviota/manifest.json
 create mode 100644 homeassistant/components/havana_shade/manifest.json
 create mode 100644 homeassistant/components/heiwa/manifest.json
 create mode 100644 homeassistant/components/hi_kumo/manifest.json
 create mode 100644 homeassistant/components/hurrican_shutters_wholesale/manifest.json
 create mode 100644 homeassistant/components/inspired_shades/manifest.json
 create mode 100644 homeassistant/components/ismartwindow/manifest.json
 create mode 100644 homeassistant/components/legrand/manifest.json
 create mode 100644 homeassistant/components/luxaflex/manifest.json
 create mode 100644 homeassistant/components/marantz/manifest.json
 create mode 100644 homeassistant/components/martec/manifest.json
 create mode 100644 homeassistant/components/nexity/manifest.json
 create mode 100644 homeassistant/components/nutrichef/manifest.json
 create mode 100644 homeassistant/components/pcs_lighting/manifest.json
 create mode 100644 homeassistant/components/raven_rock_mfg/manifest.json
 create mode 100644 homeassistant/components/rexel/manifest.json
 create mode 100644 homeassistant/components/roborock/manifest.json
 create mode 100644 homeassistant/components/screenaway/manifest.json
 create mode 100644 homeassistant/components/sensorblue/manifest.json
 create mode 100644 homeassistant/components/simply_automated/manifest.json
 create mode 100644 homeassistant/components/smart_blinds/manifest.json
 create mode 100644 homeassistant/components/smart_home/manifest.json
 create mode 100644 homeassistant/components/smarther/manifest.json
 create mode 100644 homeassistant/components/somfy/manifest.json
 create mode 100644 homeassistant/components/thermoplus/manifest.json
 create mode 100644 homeassistant/components/uprise_smart_shades/manifest.json
 delete mode 100644 homeassistant/generated/supported_brands.py
 delete mode 100644 script/hassfest/supported_brands.py

diff --git a/homeassistant/components/3_day_blinds/manifest.json b/homeassistant/components/3_day_blinds/manifest.json
new file mode 100644
index 00000000000..5baf52cfac9
--- /dev/null
+++ b/homeassistant/components/3_day_blinds/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "3_day_blinds",
+  "name": "3 Day Blinds",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/amp_motorization/manifest.json b/homeassistant/components/amp_motorization/manifest.json
new file mode 100644
index 00000000000..c8f6f935a24
--- /dev/null
+++ b/homeassistant/components/amp_motorization/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "amp_motorization",
+  "name": "AMP Motorization",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/august_ble/manifest.json b/homeassistant/components/august_ble/manifest.json
new file mode 100644
index 00000000000..882759b1209
--- /dev/null
+++ b/homeassistant/components/august_ble/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "august_ble",
+  "name": "August Bluetooth",
+  "integration_type": "virtual",
+  "supported_by": "yalexs_ble"
+}
diff --git a/homeassistant/components/bliss_automation/manifest.json b/homeassistant/components/bliss_automation/manifest.json
new file mode 100644
index 00000000000..ca0ae5c7fdf
--- /dev/null
+++ b/homeassistant/components/bliss_automation/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "bliss_automation",
+  "name": "Bliss Automation",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/bloc_blinds/manifest.json b/homeassistant/components/bloc_blinds/manifest.json
new file mode 100644
index 00000000000..a0e318e2b2f
--- /dev/null
+++ b/homeassistant/components/bloc_blinds/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "bloc_blinds",
+  "name": "Bloc Blinds",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/brel_home/manifest.json b/homeassistant/components/brel_home/manifest.json
new file mode 100644
index 00000000000..02e06705de8
--- /dev/null
+++ b/homeassistant/components/brel_home/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "brel_home",
+  "name": "Brel Home",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/bswitch/manifest.json b/homeassistant/components/bswitch/manifest.json
new file mode 100644
index 00000000000..62c6efb5bcb
--- /dev/null
+++ b/homeassistant/components/bswitch/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "bswitch",
+  "name": "BSwitch",
+  "integration_type": "virtual",
+  "supported_by": "switchbee"
+}
diff --git a/homeassistant/components/bticino/manifest.json b/homeassistant/components/bticino/manifest.json
new file mode 100644
index 00000000000..39b618ad49b
--- /dev/null
+++ b/homeassistant/components/bticino/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "bticino",
+  "name": "BTicino",
+  "integration_type": "virtual",
+  "supported_by": "netatmo"
+}
diff --git a/homeassistant/components/bubendorff/manifest.json b/homeassistant/components/bubendorff/manifest.json
new file mode 100644
index 00000000000..599dace5236
--- /dev/null
+++ b/homeassistant/components/bubendorff/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "bubendorff",
+  "name": "Bubendorff",
+  "integration_type": "virtual",
+  "supported_by": "netatmo"
+}
diff --git a/homeassistant/components/cozytouch/manifest.json b/homeassistant/components/cozytouch/manifest.json
new file mode 100644
index 00000000000..83b76211be0
--- /dev/null
+++ b/homeassistant/components/cozytouch/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "cozytouch",
+  "name": "Atlantic Cozytouch",
+  "integration_type": "virtual",
+  "supported_by": "overkiz"
+}
diff --git a/homeassistant/components/dacia/manifest.json b/homeassistant/components/dacia/manifest.json
new file mode 100644
index 00000000000..d637d7cc1e4
--- /dev/null
+++ b/homeassistant/components/dacia/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "dacia",
+  "name": "Dacia",
+  "integration_type": "virtual",
+  "supported_by": "renault"
+}
diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json
index b43dbe3acb7..bb6e59053fb 100644
--- a/homeassistant/components/denonavr/manifest.json
+++ b/homeassistant/components/denonavr/manifest.json
@@ -56,8 +56,5 @@
     }
   ],
   "iot_class": "local_polling",
-  "loggers": ["denonavr"],
-  "supported_brands": {
-    "marantz": "Marantz"
-  }
+  "loggers": ["denonavr"]
 }
diff --git a/homeassistant/components/diaz/manifest.json b/homeassistant/components/diaz/manifest.json
new file mode 100644
index 00000000000..50a8d865798
--- /dev/null
+++ b/homeassistant/components/diaz/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "diaz",
+  "name": "Diaz",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/digital_loggers/manifest.json b/homeassistant/components/digital_loggers/manifest.json
new file mode 100644
index 00000000000..b9d0e24c340
--- /dev/null
+++ b/homeassistant/components/digital_loggers/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "digital_loggers",
+  "name": "Digital Loggers",
+  "integration_type": "virtual",
+  "supported_by": "wemo"
+}
diff --git a/homeassistant/components/dooya/manifest.json b/homeassistant/components/dooya/manifest.json
new file mode 100644
index 00000000000..aa05d5b5475
--- /dev/null
+++ b/homeassistant/components/dooya/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "dooya",
+  "name": "Dooya",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/flexom/manifest.json b/homeassistant/components/flexom/manifest.json
new file mode 100644
index 00000000000..9242a48af48
--- /dev/null
+++ b/homeassistant/components/flexom/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "flexom",
+  "name": "Bouygues Flexom",
+  "integration_type": "virtual",
+  "supported_by": "overkiz"
+}
diff --git a/homeassistant/components/gaviota/manifest.json b/homeassistant/components/gaviota/manifest.json
new file mode 100644
index 00000000000..2581f3a505e
--- /dev/null
+++ b/homeassistant/components/gaviota/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "gaviota",
+  "name": "Gaviota",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json
index 06b8b109175..97c0ec1780c 100644
--- a/homeassistant/components/gree/manifest.json
+++ b/homeassistant/components/gree/manifest.json
@@ -7,8 +7,5 @@
   "dependencies": ["network"],
   "codeowners": ["@cmroche"],
   "iot_class": "local_polling",
-  "loggers": ["greeclimate"],
-  "supported_brands": {
-    "heiwa": "Heiwa"
-  }
+  "loggers": ["greeclimate"]
 }
diff --git a/homeassistant/components/havana_shade/manifest.json b/homeassistant/components/havana_shade/manifest.json
new file mode 100644
index 00000000000..e9499712bf7
--- /dev/null
+++ b/homeassistant/components/havana_shade/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "havana_shade",
+  "name": "Havana Shade",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/heiwa/manifest.json b/homeassistant/components/heiwa/manifest.json
new file mode 100644
index 00000000000..950578a82c0
--- /dev/null
+++ b/homeassistant/components/heiwa/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "heiwa",
+  "name": "Heiwa",
+  "integration_type": "virtual",
+  "supported_by": "gree"
+}
diff --git a/homeassistant/components/hi_kumo/manifest.json b/homeassistant/components/hi_kumo/manifest.json
new file mode 100644
index 00000000000..6b976b4574f
--- /dev/null
+++ b/homeassistant/components/hi_kumo/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "hi_kumo",
+  "name": "Hitachi Hi Kumo",
+  "integration_type": "virtual",
+  "supported_by": "overkiz"
+}
diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json
index 9eb3019984e..15b10dca0e0 100644
--- a/homeassistant/components/hunterdouglas_powerview/manifest.json
+++ b/homeassistant/components/hunterdouglas_powerview/manifest.json
@@ -17,8 +17,5 @@
   ],
   "zeroconf": ["_powerview._tcp.local."],
   "iot_class": "local_polling",
-  "loggers": ["aiopvapi"],
-  "supported_brands": {
-    "luxaflex": "Luxaflex"
-  }
+  "loggers": ["aiopvapi"]
 }
diff --git a/homeassistant/components/hurrican_shutters_wholesale/manifest.json b/homeassistant/components/hurrican_shutters_wholesale/manifest.json
new file mode 100644
index 00000000000..1fb5bc5600d
--- /dev/null
+++ b/homeassistant/components/hurrican_shutters_wholesale/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "hurrican_shutters_wholesale",
+  "name": "Hurrican Shutters Wholesale",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json
index e8076576f6e..97234de9d6d 100644
--- a/homeassistant/components/inkbird/manifest.json
+++ b/homeassistant/components/inkbird/manifest.json
@@ -13,8 +13,5 @@
   "requirements": ["inkbird-ble==0.5.5"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco"],
-  "iot_class": "local_push",
-  "supported_brands": {
-    "nutrichef": "Nutrichef"
-  }
+  "iot_class": "local_push"
 }
diff --git a/homeassistant/components/inspired_shades/manifest.json b/homeassistant/components/inspired_shades/manifest.json
new file mode 100644
index 00000000000..5c8f3bdc10b
--- /dev/null
+++ b/homeassistant/components/inspired_shades/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "inspired_shades",
+  "name": "Inspired Shades",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/ismartwindow/manifest.json b/homeassistant/components/ismartwindow/manifest.json
new file mode 100644
index 00000000000..1e2ca875007
--- /dev/null
+++ b/homeassistant/components/ismartwindow/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "ismartwindow",
+  "name": "iSmartWindow",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/legrand/manifest.json b/homeassistant/components/legrand/manifest.json
new file mode 100644
index 00000000000..1437622e632
--- /dev/null
+++ b/homeassistant/components/legrand/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "legrand",
+  "name": "Legrand",
+  "integration_type": "virtual",
+  "supported_by": "netatmo"
+}
diff --git a/homeassistant/components/luxaflex/manifest.json b/homeassistant/components/luxaflex/manifest.json
new file mode 100644
index 00000000000..57c552c616a
--- /dev/null
+++ b/homeassistant/components/luxaflex/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "luxaflex",
+  "name": "Luxaflex",
+  "integration_type": "virtual",
+  "supported_by": "hunterdouglas_powerview"
+}
diff --git a/homeassistant/components/marantz/manifest.json b/homeassistant/components/marantz/manifest.json
new file mode 100644
index 00000000000..00a9b0675ec
--- /dev/null
+++ b/homeassistant/components/marantz/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "marantz",
+  "name": "Marantz",
+  "integration_type": "virtual",
+  "supported_by": "denonavr"
+}
diff --git a/homeassistant/components/martec/manifest.json b/homeassistant/components/martec/manifest.json
new file mode 100644
index 00000000000..67402e4722a
--- /dev/null
+++ b/homeassistant/components/martec/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "martec",
+  "name": "Martec",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json
index 16f87dcf2ff..6d80d31a69d 100644
--- a/homeassistant/components/motion_blinds/manifest.json
+++ b/homeassistant/components/motion_blinds/manifest.json
@@ -19,25 +19,5 @@
   ],
   "codeowners": ["@starkillerOG"],
   "iot_class": "local_push",
-  "loggers": ["motionblinds"],
-  "supported_brands": {
-    "amp_motorization": "AMP Motorization",
-    "bliss_automation": "Bliss Automation",
-    "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"
-  }
+  "loggers": ["motionblinds"]
 }
diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json
index 28a4bad2dc5..5ad0fca3d7a 100644
--- a/homeassistant/components/netatmo/manifest.json
+++ b/homeassistant/components/netatmo/manifest.json
@@ -11,11 +11,5 @@
     "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"]
   },
   "iot_class": "cloud_polling",
-  "loggers": ["pyatmo"],
-  "supported_brands": {
-    "legrand": "Legrand",
-    "bubendorff": "Bubendorff",
-    "smarther": "Smarther",
-    "bticino": "BTicino"
-  }
+  "loggers": ["pyatmo"]
 }
diff --git a/homeassistant/components/nexity/manifest.json b/homeassistant/components/nexity/manifest.json
new file mode 100644
index 00000000000..31275f80eb4
--- /dev/null
+++ b/homeassistant/components/nexity/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "nexity",
+  "name": "Nexity Eugénie",
+  "integration_type": "virtual",
+  "supported_by": "overkiz"
+}
diff --git a/homeassistant/components/nutrichef/manifest.json b/homeassistant/components/nutrichef/manifest.json
new file mode 100644
index 00000000000..4d81d488123
--- /dev/null
+++ b/homeassistant/components/nutrichef/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "nutrichef",
+  "name": "Nutrichef",
+  "integration_type": "virtual",
+  "supported_by": "inkbird"
+}
diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json
index 480b0b1d9ed..f09142c86f0 100644
--- a/homeassistant/components/overkiz/manifest.json
+++ b/homeassistant/components/overkiz/manifest.json
@@ -18,13 +18,5 @@
   ],
   "codeowners": ["@imicknl", "@vlebourl", "@tetienne"],
   "iot_class": "cloud_polling",
-  "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
-  "supported_brands": {
-    "cozytouch": "Atlantic Cozytouch",
-    "flexom": "Bouygues Flexom",
-    "hi_kumo": "Hitachi Hi Kumo",
-    "nexity": "Nexity Eugénie",
-    "rexel": "Rexel Energeasy Connect",
-    "somfy": "Somfy"
-  }
+  "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"]
 }
diff --git a/homeassistant/components/pcs_lighting/manifest.json b/homeassistant/components/pcs_lighting/manifest.json
new file mode 100644
index 00000000000..3655032e270
--- /dev/null
+++ b/homeassistant/components/pcs_lighting/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "pcs_lighting",
+  "name": "PCS Lighting",
+  "integration_type": "virtual",
+  "supported_by": "upb"
+}
diff --git a/homeassistant/components/raven_rock_mfg/manifest.json b/homeassistant/components/raven_rock_mfg/manifest.json
new file mode 100644
index 00000000000..75085094888
--- /dev/null
+++ b/homeassistant/components/raven_rock_mfg/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "raven_rock_mfg",
+  "name": "Raven Rock MFG",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json
index 33dc8c3dc8d..5b2fc146e92 100644
--- a/homeassistant/components/renault/manifest.json
+++ b/homeassistant/components/renault/manifest.json
@@ -6,6 +6,5 @@
   "requirements": ["renault-api==0.1.11"],
   "codeowners": ["@epenet"],
   "iot_class": "cloud_polling",
-  "loggers": ["renault_api"],
-  "supported_brands": { "dacia": "Dacia" }
+  "loggers": ["renault_api"]
 }
diff --git a/homeassistant/components/rexel/manifest.json b/homeassistant/components/rexel/manifest.json
new file mode 100644
index 00000000000..f3bfcf55c35
--- /dev/null
+++ b/homeassistant/components/rexel/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "rexel",
+  "name": "Rexel Energeasy Connect",
+  "integration_type": "virtual",
+  "supported_by": "overkiz"
+}
diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json
new file mode 100644
index 00000000000..00f90271cfe
--- /dev/null
+++ b/homeassistant/components/roborock/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "roborock",
+  "name": "Roborock",
+  "integration_type": "virtual",
+  "supported_by": "xiaomi_miio"
+}
diff --git a/homeassistant/components/screenaway/manifest.json b/homeassistant/components/screenaway/manifest.json
new file mode 100644
index 00000000000..b48e9348f48
--- /dev/null
+++ b/homeassistant/components/screenaway/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "screenaway",
+  "name": "ScreenAway",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/sensorblue/manifest.json b/homeassistant/components/sensorblue/manifest.json
new file mode 100644
index 00000000000..d74abde6fdb
--- /dev/null
+++ b/homeassistant/components/sensorblue/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "sensorblue",
+  "name": "SensorBlue",
+  "integration_type": "virtual",
+  "supported_by": "thermobeacon"
+}
diff --git a/homeassistant/components/simply_automated/manifest.json b/homeassistant/components/simply_automated/manifest.json
new file mode 100644
index 00000000000..3fce8ae27c3
--- /dev/null
+++ b/homeassistant/components/simply_automated/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "simply_automated",
+  "name": "Simply Automated",
+  "integration_type": "virtual",
+  "supported_by": "upb"
+}
diff --git a/homeassistant/components/smart_blinds/manifest.json b/homeassistant/components/smart_blinds/manifest.json
new file mode 100644
index 00000000000..d0ddb30c5ee
--- /dev/null
+++ b/homeassistant/components/smart_blinds/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "smart_blinds",
+  "name": "Smart Blinds",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/smart_home/manifest.json b/homeassistant/components/smart_home/manifest.json
new file mode 100644
index 00000000000..7e420fb5404
--- /dev/null
+++ b/homeassistant/components/smart_home/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "smart_home",
+  "name": "Smart Home",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/smarther/manifest.json b/homeassistant/components/smarther/manifest.json
new file mode 100644
index 00000000000..6e87dd866fe
--- /dev/null
+++ b/homeassistant/components/smarther/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "smarther",
+  "name": "Smarther",
+  "integration_type": "virtual",
+  "supported_by": "netatmo"
+}
diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json
new file mode 100644
index 00000000000..3090eb7b120
--- /dev/null
+++ b/homeassistant/components/somfy/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "somfy",
+  "name": "Somfy",
+  "integration_type": "virtual",
+  "supported_by": "overkiz"
+}
diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json
index 5ca066e3bc0..75e5b2e9bfd 100644
--- a/homeassistant/components/switchbee/manifest.json
+++ b/homeassistant/components/switchbee/manifest.json
@@ -5,8 +5,5 @@
   "documentation": "https://www.home-assistant.io/integrations/switchbee",
   "requirements": ["pyswitchbee==1.5.5"],
   "codeowners": ["@jafar-atili"],
-  "iot_class": "local_polling",
-  "supported_brands": {
-    "bswitch": "BSwitch"
-  }
+  "iot_class": "local_polling"
 }
diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json
index 639d2362026..34321a66681 100644
--- a/homeassistant/components/thermobeacon/manifest.json
+++ b/homeassistant/components/thermobeacon/manifest.json
@@ -27,9 +27,5 @@
   "requirements": ["thermobeacon-ble==0.3.2"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco"],
-  "iot_class": "local_push",
-  "supported_brands": {
-    "thermoplus": "ThermoPlus",
-    "sensorblue": "SensorBlue"
-  }
+  "iot_class": "local_push"
 }
diff --git a/homeassistant/components/thermoplus/manifest.json b/homeassistant/components/thermoplus/manifest.json
new file mode 100644
index 00000000000..512cf216c05
--- /dev/null
+++ b/homeassistant/components/thermoplus/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "thermoplus",
+  "name": "ThermoPlus",
+  "integration_type": "virtual",
+  "supported_by": "thermobeacon"
+}
diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json
index aaa26bfdd66..fd5d68e577f 100644
--- a/homeassistant/components/upb/manifest.json
+++ b/homeassistant/components/upb/manifest.json
@@ -6,9 +6,5 @@
   "codeowners": ["@gwww"],
   "config_flow": true,
   "iot_class": "local_push",
-  "loggers": ["upb_lib"],
-  "supported_brands": {
-    "pcs_lighting": "PCS Lighting",
-    "simply_automated": "Simply Automated"
-  }
+  "loggers": ["upb_lib"]
 }
diff --git a/homeassistant/components/uprise_smart_shades/manifest.json b/homeassistant/components/uprise_smart_shades/manifest.json
new file mode 100644
index 00000000000..a0ddc2bfb2f
--- /dev/null
+++ b/homeassistant/components/uprise_smart_shades/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "uprise_smart_shades",
+  "name": "Uprise Smart Shades",
+  "integration_type": "virtual",
+  "supported_by": "motion_blinds"
+}
diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py
index 68dbffa7440..19ec505e449 100644
--- a/homeassistant/components/websocket_api/commands.py
+++ b/homeassistant/components/websocket_api/commands.py
@@ -21,7 +21,6 @@ from homeassistant.exceptions import (
     TemplateError,
     Unauthorized,
 )
-from homeassistant.generated import supported_brands
 from homeassistant.helpers import config_validation as cv, entity, template
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.event import (
@@ -74,7 +73,6 @@ def async_register_commands(
     async_reg(hass, handle_unsubscribe_events)
     async_reg(hass, handle_validate_config)
     async_reg(hass, handle_subscribe_entities)
-    async_reg(hass, handle_supported_brands)
     async_reg(hass, handle_supported_features)
     async_reg(hass, handle_integration_descriptions)
 
@@ -705,31 +703,6 @@ async def handle_validate_config(
     connection.send_result(msg["id"], result)
 
 
-@decorators.websocket_command(
-    {
-        vol.Required("type"): "supported_brands",
-    }
-)
-@decorators.async_response
-async def handle_supported_brands(
-    hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
-) -> None:
-    """Handle supported brands command."""
-    data = {}
-
-    ints_or_excs = await async_get_integrations(
-        hass, supported_brands.HAS_SUPPORTED_BRANDS
-    )
-    for int_or_exc in ints_or_excs.values():
-        if isinstance(int_or_exc, Exception):
-            raise int_or_exc
-        # Happens if a custom component without supported brands overrides a built-in one with supported brands
-        if "supported_brands" not in int_or_exc.manifest:
-            continue
-        data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"]
-    connection.send_result(msg["id"], data)
-
-
 @callback
 @decorators.websocket_command(
     {
diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json
index 5486a192787..b324ba060ea 100644
--- a/homeassistant/components/wemo/manifest.json
+++ b/homeassistant/components/wemo/manifest.json
@@ -14,8 +14,5 @@
   },
   "codeowners": ["@esev"],
   "iot_class": "local_push",
-  "loggers": ["pywemo"],
-  "supported_brands": {
-    "digital_loggers": "Digital Loggers"
-  }
+  "loggers": ["pywemo"]
 }
diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json
index c84c6edc2e8..0f1a9dd92aa 100644
--- a/homeassistant/components/xiaomi_miio/manifest.json
+++ b/homeassistant/components/xiaomi_miio/manifest.json
@@ -7,8 +7,5 @@
   "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
   "zeroconf": ["_miio._udp.local."],
   "iot_class": "local_polling",
-  "loggers": ["micloud", "miio"],
-  "supported_brands": {
-    "roborock": "Roborock"
-  }
+  "loggers": ["micloud", "miio"]
 }
diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json
index 54638478dd5..7bc8bde5b30 100644
--- a/homeassistant/components/yalexs_ble/manifest.json
+++ b/homeassistant/components/yalexs_ble/manifest.json
@@ -12,8 +12,5 @@
       "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb"
     }
   ],
-  "iot_class": "local_push",
-  "supported_brands": {
-    "august_ble": "August Bluetooth"
-  }
+  "iot_class": "local_push"
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 019529a94f0..866ad2a54ed 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -1,88 +1,93 @@
 {
   "integration": {
+    "3_day_blinds": {
+      "name": "3 Day Blinds",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
+    },
     "abode": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Abode",
       "integration_type": "hub",
-      "name": "Abode"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "accuweather": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "AccuWeather",
       "integration_type": "hub",
-      "name": "AccuWeather"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "acer_projector": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Acer Projector",
       "integration_type": "hub",
-      "name": "Acer Projector"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "acmeda": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Rollease Acmeda Automate",
       "integration_type": "hub",
-      "name": "Rollease Acmeda Automate"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "actiontec": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Actiontec",
       "integration_type": "hub",
-      "name": "Actiontec"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "adax": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Adax",
       "integration_type": "hub",
-      "name": "Adax"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "adguard": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "AdGuard Home",
       "integration_type": "service",
-      "name": "AdGuard Home"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "ads": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "ADS",
       "integration_type": "hub",
-      "name": "ADS"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "advantage_air": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Advantage Air",
       "integration_type": "hub",
-      "name": "Advantage Air"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "aemet": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "AEMET OpenData",
       "integration_type": "hub",
-      "name": "AEMET OpenData"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "aftership": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "AfterShip",
       "integration_type": "hub",
-      "name": "AfterShip"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "agent_dvr": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Agent DVR",
       "integration_type": "hub",
-      "name": "Agent DVR"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "airly": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Airly",
       "integration_type": "hub",
-      "name": "Airly"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "airnow": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "AirNow",
       "integration_type": "hub",
-      "name": "AirNow"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "airthings": {
       "name": "Airthings",
@@ -102,52 +107,52 @@
       }
     },
     "airtouch4": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "AirTouch 4",
       "integration_type": "hub",
-      "name": "AirTouch 4"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "airvisual": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "AirVisual",
       "integration_type": "hub",
-      "name": "AirVisual"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "airzone": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Airzone",
       "integration_type": "hub",
-      "name": "Airzone"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "aladdin_connect": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Aladdin Connect",
       "integration_type": "hub",
-      "name": "Aladdin Connect"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "alarmdecoder": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "AlarmDecoder",
       "integration_type": "hub",
-      "name": "AlarmDecoder"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "alert": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Alert",
       "integration_type": "hub",
-      "name": "Alert"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "almond": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Almond",
       "integration_type": "hub",
-      "name": "Almond"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "alpha_vantage": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Alpha Vantage",
       "integration_type": "hub",
-      "name": "Alpha Vantage"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "amazon": {
       "name": "Amazon",
@@ -179,70 +184,75 @@
       }
     },
     "amberelectric": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Amber Electric",
       "integration_type": "hub",
-      "name": "Amber Electric"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ambiclimate": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Ambiclimate",
       "integration_type": "hub",
-      "name": "Ambiclimate"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ambient_station": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Ambient Weather Station",
       "integration_type": "hub",
-      "name": "Ambient Weather Station"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "amcrest": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Amcrest",
       "integration_type": "hub",
-      "name": "Amcrest"
+      "config_flow": false,
+      "iot_class": "local_polling"
+    },
+    "amp_motorization": {
+      "name": "AMP Motorization",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "ampio": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Ampio Smart Smog System",
       "integration_type": "hub",
-      "name": "Ampio Smart Smog System"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "android_ip_webcam": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Android IP Webcam",
       "integration_type": "hub",
-      "name": "Android IP Webcam"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "androidtv": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Android TV",
       "integration_type": "hub",
-      "name": "Android TV"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "anel_pwrctrl": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Anel NET-PwrCtrl",
       "integration_type": "hub",
-      "name": "Anel NET-PwrCtrl"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "anthemav": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Anthem A/V Receivers",
       "integration_type": "hub",
-      "name": "Anthem A/V Receivers"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "apache_kafka": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Apache Kafka",
       "integration_type": "hub",
-      "name": "Apache Kafka"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "apcupsd": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "APC UPS Daemon",
       "integration_type": "hub",
-      "name": "APC UPS Daemon"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "apple": {
       "name": "Apple",
@@ -285,46 +295,46 @@
       }
     },
     "apprise": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Apprise",
       "integration_type": "hub",
-      "name": "Apprise"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "aprs": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "APRS",
       "integration_type": "hub",
-      "name": "APRS"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "aqualogic": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "AquaLogic",
       "integration_type": "hub",
-      "name": "AquaLogic"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "aquostv": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Sharp Aquos TV",
       "integration_type": "hub",
-      "name": "Sharp Aquos TV"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "arcam_fmj": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Arcam FMJ Receivers",
       "integration_type": "hub",
-      "name": "Arcam FMJ Receivers"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "arest": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "aREST",
       "integration_type": "hub",
-      "name": "aREST"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "arris_tg2492lg": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Arris TG2492LG",
       "integration_type": "hub",
-      "name": "Arris TG2492LG"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "aruba": {
       "name": "Aruba",
@@ -344,16 +354,16 @@
       }
     },
     "arwn": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Ambient Radio Weather Network",
       "integration_type": "hub",
-      "name": "Ambient Radio Weather Network"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "aseko_pool_live": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Aseko Pool Live",
       "integration_type": "hub",
-      "name": "Aseko Pool Live"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "asterisk": {
       "name": "Asterisk",
@@ -373,28 +383,28 @@
       }
     },
     "asuswrt": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "ASUSWRT",
       "integration_type": "hub",
-      "name": "ASUSWRT"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "atag": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Atag",
       "integration_type": "hub",
-      "name": "Atag"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "aten_pe": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "ATEN Rack PDU",
       "integration_type": "hub",
-      "name": "ATEN Rack PDU"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "atome": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Atome Linky",
       "integration_type": "hub",
-      "name": "Atome Linky"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "august": {
       "name": "August Home",
@@ -413,261 +423,296 @@
         }
       }
     },
+    "august_ble": {
+      "name": "August Bluetooth",
+      "integration_type": "virtual",
+      "supported_by": "yalexs_ble"
+    },
     "aurora": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "cloud_polling",
-      "integration_type": "hub"
+      "iot_class": "cloud_polling"
     },
     "aurora_abb_powerone": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Aurora ABB PowerOne Solar PV",
       "integration_type": "hub",
-      "name": "Aurora ABB PowerOne Solar PV"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "aussie_broadband": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Aussie Broadband",
       "integration_type": "hub",
-      "name": "Aussie Broadband"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "avion": {
-      "config_flow": false,
-      "iot_class": "assumed_state",
+      "name": "Avi-on",
       "integration_type": "hub",
-      "name": "Avi-on"
+      "config_flow": false,
+      "iot_class": "assumed_state"
     },
     "awair": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Awair",
       "integration_type": "hub",
-      "name": "Awair"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "axis": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Axis",
       "integration_type": "device",
-      "name": "Axis"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "baf": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Big Ass Fans",
       "integration_type": "hub",
-      "name": "Big Ass Fans"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "baidu": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Baidu",
       "integration_type": "hub",
-      "name": "Baidu"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "balboa": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Balboa Spa Client",
       "integration_type": "hub",
-      "name": "Balboa Spa Client"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "bayesian": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Bayesian",
       "integration_type": "hub",
-      "name": "Bayesian"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "bbox": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Bbox",
       "integration_type": "hub",
-      "name": "Bbox"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "beewi_smartclim": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "BeeWi SmartClim BLE sensor",
       "integration_type": "hub",
-      "name": "BeeWi SmartClim BLE sensor"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "bitcoin": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Bitcoin",
       "integration_type": "hub",
-      "name": "Bitcoin"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "bizkaibus": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Bizkaibus",
       "integration_type": "hub",
-      "name": "Bizkaibus"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "blackbird": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Monoprice Blackbird Matrix Switch",
       "integration_type": "hub",
-      "name": "Monoprice Blackbird Matrix Switch"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "blebox": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "BleBox devices",
       "integration_type": "hub",
-      "name": "BleBox devices"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "blink": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Blink",
       "integration_type": "hub",
-      "name": "Blink"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "blinksticklight": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "BlinkStick",
       "integration_type": "hub",
-      "name": "BlinkStick"
+      "config_flow": false,
+      "iot_class": "local_polling"
+    },
+    "bliss_automation": {
+      "name": "Bliss Automation",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
+    },
+    "bloc_blinds": {
+      "name": "Bloc Blinds",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "blockchain": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Blockchain.com",
       "integration_type": "hub",
-      "name": "Blockchain.com"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "bloomsky": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "BloomSky",
       "integration_type": "hub",
-      "name": "BloomSky"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "bluemaestro": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "BlueMaestro",
       "integration_type": "hub",
-      "name": "BlueMaestro"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "bluesound": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Bluesound",
       "integration_type": "hub",
-      "name": "Bluesound"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "bluetooth": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Bluetooth",
       "integration_type": "hub",
-      "name": "Bluetooth"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "bluetooth_le_tracker": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Bluetooth LE Tracker",
       "integration_type": "hub",
-      "name": "Bluetooth LE Tracker"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "bluetooth_tracker": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Bluetooth Tracker",
       "integration_type": "hub",
-      "name": "Bluetooth Tracker"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "bmw_connected_drive": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "BMW Connected Drive",
       "integration_type": "hub",
-      "name": "BMW Connected Drive"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "bond": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Bond",
       "integration_type": "hub",
-      "name": "Bond"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "bosch_shc": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Bosch SHC",
       "integration_type": "hub",
-      "name": "Bosch SHC"
+      "config_flow": true,
+      "iot_class": "local_push"
+    },
+    "brel_home": {
+      "name": "Brel Home",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "broadlink": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Broadlink",
       "integration_type": "hub",
-      "name": "Broadlink"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "brother": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Brother Printer",
       "integration_type": "hub",
-      "name": "Brother Printer"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "brottsplatskartan": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Brottsplatskartan",
       "integration_type": "hub",
-      "name": "Brottsplatskartan"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "browser": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Browser",
       "integration_type": "hub",
-      "name": "Browser"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "brunt": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Brunt Blind Engine",
       "integration_type": "hub",
-      "name": "Brunt Blind Engine"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "bsblan": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "BSB-Lan",
       "integration_type": "hub",
-      "name": "BSB-Lan"
+      "config_flow": true,
+      "iot_class": "local_polling"
+    },
+    "bswitch": {
+      "name": "BSwitch",
+      "integration_type": "virtual",
+      "supported_by": "switchbee"
     },
     "bt_home_hub_5": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "BT Home Hub 5",
       "integration_type": "hub",
-      "name": "BT Home Hub 5"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "bt_smarthub": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "BT Smart Hub",
       "integration_type": "hub",
-      "name": "BT Smart Hub"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "bthome": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "BTHome",
       "integration_type": "hub",
-      "name": "BTHome"
+      "config_flow": true,
+      "iot_class": "local_push"
+    },
+    "bticino": {
+      "name": "BTicino",
+      "integration_type": "virtual",
+      "supported_by": "netatmo"
+    },
+    "bubendorff": {
+      "name": "Bubendorff",
+      "integration_type": "virtual",
+      "supported_by": "netatmo"
     },
     "buienradar": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Buienradar",
       "integration_type": "hub",
-      "name": "Buienradar"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "caldav": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "CalDAV",
       "integration_type": "hub",
-      "name": "CalDAV"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "canary": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Canary",
       "integration_type": "hub",
-      "name": "Canary"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "cert_expiry": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "cloud_polling",
-      "integration_type": "hub"
+      "iot_class": "cloud_polling"
     },
     "channels": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Channels",
       "integration_type": "hub",
-      "name": "Channels"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "circuit": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Unify Circuit",
       "integration_type": "hub",
-      "name": "Unify Circuit"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "cisco": {
       "name": "Cisco",
@@ -693,22 +738,22 @@
       }
     },
     "citybikes": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "CityBikes",
       "integration_type": "hub",
-      "name": "CityBikes"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "clementine": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Clementine Music Player",
       "integration_type": "hub",
-      "name": "Clementine Music Player"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "clickatell": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Clickatell",
       "integration_type": "hub",
-      "name": "Clickatell"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "clicksend": {
       "name": "ClickSend",
@@ -728,182 +773,191 @@
       }
     },
     "cloud": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Home Assistant Cloud",
       "integration_type": "hub",
-      "name": "Home Assistant Cloud"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "cloudflare": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Cloudflare",
       "integration_type": "hub",
-      "name": "Cloudflare"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "cmus": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "cmus",
       "integration_type": "hub",
-      "name": "cmus"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "co2signal": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "CO2 Signal",
       "integration_type": "hub",
-      "name": "CO2 Signal"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "coinbase": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Coinbase",
       "integration_type": "hub",
-      "name": "Coinbase"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "color_extractor": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "ColorExtractor",
       "integration_type": "hub",
-      "name": "ColorExtractor"
+      "config_flow": false
     },
     "comed_hourly_pricing": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "ComEd Hourly Pricing",
       "integration_type": "hub",
-      "name": "ComEd Hourly Pricing"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "comfoconnect": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Zehnder ComfoAir Q",
       "integration_type": "hub",
-      "name": "Zehnder ComfoAir Q"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "command_line": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Command Line",
       "integration_type": "hub",
-      "name": "Command Line"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "compensation": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "Compensation",
       "integration_type": "hub",
-      "name": "Compensation"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "concord232": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Concord232",
       "integration_type": "hub",
-      "name": "Concord232"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "control4": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Control4",
       "integration_type": "hub",
-      "name": "Control4"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "coolmaster": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "CoolMasterNet",
       "integration_type": "hub",
-      "name": "CoolMasterNet"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "coronavirus": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Coronavirus (COVID-19)",
       "integration_type": "hub",
-      "name": "Coronavirus (COVID-19)"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "cozytouch": {
+      "name": "Atlantic Cozytouch",
+      "integration_type": "virtual",
+      "supported_by": "overkiz"
     },
     "cpuspeed": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "hub"
+      "iot_class": "local_push"
     },
     "crownstone": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Crownstone",
       "integration_type": "hub",
-      "name": "Crownstone"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "cups": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "CUPS",
       "integration_type": "hub",
-      "name": "CUPS"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "currencylayer": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "currencylayer",
       "integration_type": "hub",
-      "name": "currencylayer"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
+    },
+    "dacia": {
+      "name": "Dacia",
+      "integration_type": "virtual",
+      "supported_by": "renault"
     },
     "daikin": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Daikin AC",
       "integration_type": "hub",
-      "name": "Daikin AC"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "danfoss_air": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Danfoss Air",
       "integration_type": "hub",
-      "name": "Danfoss Air"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "darksky": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Dark Sky",
       "integration_type": "hub",
-      "name": "Dark Sky"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "datadog": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Datadog",
       "integration_type": "hub",
-      "name": "Datadog"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "ddwrt": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "DD-WRT",
       "integration_type": "hub",
-      "name": "DD-WRT"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "debugpy": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Remote Python Debugger",
       "integration_type": "hub",
-      "name": "Remote Python Debugger"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "deconz": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "deCONZ",
       "integration_type": "hub",
-      "name": "deCONZ"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "decora": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Leviton Decora",
       "integration_type": "hub",
-      "name": "Leviton Decora"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "decora_wifi": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Leviton Decora Wi-Fi",
       "integration_type": "hub",
-      "name": "Leviton Decora Wi-Fi"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "delijn": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "De Lijn",
       "integration_type": "hub",
-      "name": "De Lijn"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "deluge": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Deluge",
       "integration_type": "hub",
-      "name": "Deluge"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "demo": {
+      "integration_type": "hub",
       "config_flow": false,
-      "iot_class": "calculated",
-      "integration_type": "hub"
+      "iot_class": "calculated"
     },
     "denon": {
       "name": "Denon",
@@ -929,16 +983,16 @@
       }
     },
     "deutsche_bahn": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Deutsche Bahn",
       "integration_type": "hub",
-      "name": "Deutsche Bahn"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "device_sun_light_trigger": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "Presence-based Lights",
       "integration_type": "hub",
-      "name": "Presence-based Lights"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "devolo": {
       "name": "devolo",
@@ -958,52 +1012,62 @@
       }
     },
     "dexcom": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Dexcom",
       "integration_type": "hub",
-      "name": "Dexcom"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "diaz": {
+      "name": "Diaz",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
+    },
+    "digital_loggers": {
+      "name": "Digital Loggers",
+      "integration_type": "virtual",
+      "supported_by": "wemo"
     },
     "digital_ocean": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Digital Ocean",
       "integration_type": "hub",
-      "name": "Digital Ocean"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "directv": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "DirecTV",
       "integration_type": "hub",
-      "name": "DirecTV"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "discogs": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Discogs",
       "integration_type": "hub",
-      "name": "Discogs"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "discord": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Discord",
       "integration_type": "hub",
-      "name": "Discord"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "dlib_face_detect": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Dlib Face Detect",
       "integration_type": "hub",
-      "name": "Dlib Face Detect"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "dlib_face_identify": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Dlib Face Identify",
       "integration_type": "hub",
-      "name": "Dlib Face Identify"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "dlink": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "D-Link Wi-Fi Smart Plugs",
       "integration_type": "hub",
-      "name": "D-Link Wi-Fi Smart Plugs"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "dlna": {
       "name": "DLNA",
@@ -1023,172 +1087,176 @@
       }
     },
     "dnsip": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "DNS IP",
       "integration_type": "hub",
-      "name": "DNS IP"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "dominos": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Dominos Pizza",
       "integration_type": "hub",
-      "name": "Dominos Pizza"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "doods": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "DOODS - Dedicated Open Object Detection Service",
       "integration_type": "hub",
-      "name": "DOODS - Dedicated Open Object Detection Service"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "doorbird": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "DoorBird",
       "integration_type": "hub",
-      "name": "DoorBird"
+      "config_flow": true,
+      "iot_class": "local_push"
+    },
+    "dooya": {
+      "name": "Dooya",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "dovado": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Dovado",
       "integration_type": "hub",
-      "name": "Dovado"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "downloader": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Downloader",
       "integration_type": "hub",
-      "name": "Downloader"
+      "config_flow": false
     },
     "dsmr": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "DSMR Slimme Meter",
       "integration_type": "hub",
-      "name": "DSMR Slimme Meter"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "dsmr_reader": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "DSMR Reader",
       "integration_type": "hub",
-      "name": "DSMR Reader"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "dte_energy_bridge": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "DTE Energy Bridge",
       "integration_type": "hub",
-      "name": "DTE Energy Bridge"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "dublin_bus_transport": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Dublin Bus",
       "integration_type": "hub",
-      "name": "Dublin Bus"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "duckdns": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Duck DNS",
       "integration_type": "hub",
-      "name": "Duck DNS"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "dunehd": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Dune HD",
       "integration_type": "hub",
-      "name": "Dune HD"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "dwd_weather_warnings": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Deutscher Wetterdienst (DWD) Weather Warnings",
       "integration_type": "hub",
-      "name": "Deutscher Wetterdienst (DWD) Weather Warnings"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "dweet": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "dweet.io",
       "integration_type": "hub",
-      "name": "dweet.io"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "eafm": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Environment Agency Flood Gauges",
       "integration_type": "hub",
-      "name": "Environment Agency Flood Gauges"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ebox": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "EBox",
       "integration_type": "hub",
-      "name": "EBox"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "ebusd": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "ebusd",
       "integration_type": "hub",
-      "name": "ebusd"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "ecoal_boiler": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "eSterownik eCoal.pl Boiler",
       "integration_type": "hub",
-      "name": "eSterownik eCoal.pl Boiler"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "ecobee": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "ecobee",
       "integration_type": "hub",
-      "name": "ecobee"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "econet": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Rheem EcoNet Products",
       "integration_type": "hub",
-      "name": "Rheem EcoNet Products"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "ecovacs": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Ecovacs",
       "integration_type": "hub",
-      "name": "Ecovacs"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "ecowitt": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Ecowitt",
       "integration_type": "hub",
-      "name": "Ecowitt"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "eddystone_temperature": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Eddystone",
       "integration_type": "hub",
-      "name": "Eddystone"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "edimax": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Edimax",
       "integration_type": "hub",
-      "name": "Edimax"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "edl21": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "EDL21",
       "integration_type": "hub",
-      "name": "EDL21"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "efergy": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Efergy",
       "integration_type": "hub",
-      "name": "Efergy"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "egardia": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Egardia",
       "integration_type": "hub",
-      "name": "Egardia"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "eight_sleep": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Eight Sleep",
       "integration_type": "hub",
-      "name": "Eight Sleep"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "elgato": {
       "name": "Elgato",
@@ -1208,34 +1276,34 @@
       }
     },
     "eliqonline": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Eliqonline",
       "integration_type": "hub",
-      "name": "Eliqonline"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "elkm1": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Elk-M1 Control",
       "integration_type": "hub",
-      "name": "Elk-M1 Control"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "elmax": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Elmax",
       "integration_type": "hub",
-      "name": "Elmax"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "elv": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "ELV PCA",
       "integration_type": "hub",
-      "name": "ELV PCA"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "emby": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Emby",
       "integration_type": "hub",
-      "name": "Emby"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "emoncms": {
       "name": "emoncms",
@@ -1255,69 +1323,69 @@
       }
     },
     "emonitor": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "SiteSage Emonitor",
       "integration_type": "hub",
-      "name": "SiteSage Emonitor"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "emulated_hue": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Emulated Hue",
       "integration_type": "hub",
-      "name": "Emulated Hue"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "emulated_kasa": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Emulated Kasa",
       "integration_type": "hub",
-      "name": "Emulated Kasa"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "emulated_roku": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "hub"
+      "iot_class": "local_push"
     },
     "enigma2": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Enigma2 (OpenWebif)",
       "integration_type": "hub",
-      "name": "Enigma2 (OpenWebif)"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "enocean": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "EnOcean",
       "integration_type": "hub",
-      "name": "EnOcean"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "enphase_envoy": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Enphase Envoy",
       "integration_type": "hub",
-      "name": "Enphase Envoy"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "entur_public_transport": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Entur",
       "integration_type": "hub",
-      "name": "Entur"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "environment_canada": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Environment Canada",
       "integration_type": "hub",
-      "name": "Environment Canada"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "envisalink": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Envisalink",
       "integration_type": "hub",
-      "name": "Envisalink"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "ephember": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "EPH Controls",
       "integration_type": "hub",
-      "name": "EPH Controls"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "epson": {
       "name": "Epson",
@@ -1354,82 +1422,82 @@
       }
     },
     "escea": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Escea",
       "integration_type": "hub",
-      "name": "Escea"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "esphome": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "ESPHome",
       "integration_type": "device",
-      "name": "ESPHome"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "etherscan": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Etherscan",
       "integration_type": "hub",
-      "name": "Etherscan"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "eufy": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "eufy",
       "integration_type": "hub",
-      "name": "eufy"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "everlights": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "EverLights",
       "integration_type": "hub",
-      "name": "EverLights"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "evil_genius_labs": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Evil Genius Labs",
       "integration_type": "hub",
-      "name": "Evil Genius Labs"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "ezviz": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "EZVIZ",
       "integration_type": "hub",
-      "name": "EZVIZ"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "faa_delays": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "FAA Delays",
       "integration_type": "hub",
-      "name": "FAA Delays"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "facebook": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Facebook Messenger",
       "integration_type": "hub",
-      "name": "Facebook Messenger"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "facebox": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Facebox",
       "integration_type": "hub",
-      "name": "Facebox"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "fail2ban": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Fail2Ban",
       "integration_type": "hub",
-      "name": "Fail2Ban"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "fastdotcom": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Fast.com",
       "integration_type": "hub",
-      "name": "Fast.com"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "feedreader": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Feedreader",
       "integration_type": "hub",
-      "name": "Feedreader"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "ffmpeg": {
       "name": "FFmpeg",
@@ -1455,207 +1523,212 @@
       }
     },
     "fibaro": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Fibaro",
       "integration_type": "hub",
-      "name": "Fibaro"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "fido": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Fido",
       "integration_type": "hub",
-      "name": "Fido"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "file": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "File",
       "integration_type": "hub",
-      "name": "File"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "filesize": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_polling",
-      "integration_type": "hub"
+      "iot_class": "local_polling"
     },
     "filter": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Filter",
       "integration_type": "hub",
-      "name": "Filter"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "fints": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "FinTS",
       "integration_type": "hub",
-      "name": "FinTS"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "fireservicerota": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "FireServiceRota",
       "integration_type": "hub",
-      "name": "FireServiceRota"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "firmata": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Firmata",
       "integration_type": "hub",
-      "name": "Firmata"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "fitbit": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Fitbit",
       "integration_type": "hub",
-      "name": "Fitbit"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "fivem": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "FiveM",
       "integration_type": "hub",
-      "name": "FiveM"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "fixer": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Fixer",
       "integration_type": "hub",
-      "name": "Fixer"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "fjaraskupan": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Fj\u00e4r\u00e5skupan",
       "integration_type": "hub",
-      "name": "Fj\u00e4r\u00e5skupan"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "fleetgo": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "FleetGO",
       "integration_type": "hub",
-      "name": "FleetGO"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "flexit": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Flexit",
       "integration_type": "hub",
-      "name": "Flexit"
+      "config_flow": false,
+      "iot_class": "local_polling"
+    },
+    "flexom": {
+      "name": "Bouygues Flexom",
+      "integration_type": "virtual",
+      "supported_by": "overkiz"
     },
     "flic": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Flic",
       "integration_type": "hub",
-      "name": "Flic"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "flick_electric": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Flick Electric",
       "integration_type": "hub",
-      "name": "Flick Electric"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "flipr": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Flipr",
       "integration_type": "hub",
-      "name": "Flipr"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "flo": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Flo",
       "integration_type": "hub",
-      "name": "Flo"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "flock": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Flock",
       "integration_type": "hub",
-      "name": "Flock"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "flume": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Flume",
       "integration_type": "hub",
-      "name": "Flume"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "flux": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "Flux",
       "integration_type": "hub",
-      "name": "Flux"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "flux_led": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Magic Home",
       "integration_type": "hub",
-      "name": "Magic Home"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "folder": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Folder",
       "integration_type": "hub",
-      "name": "Folder"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "folder_watcher": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Folder Watcher",
       "integration_type": "hub",
-      "name": "Folder Watcher"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "foobot": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Foobot",
       "integration_type": "hub",
-      "name": "Foobot"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "forecast_solar": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Forecast.Solar",
       "integration_type": "hub",
-      "name": "Forecast.Solar"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "forked_daapd": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Owntone",
       "integration_type": "hub",
-      "name": "Owntone"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "fortios": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "FortiOS",
       "integration_type": "hub",
-      "name": "FortiOS"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "foscam": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Foscam",
       "integration_type": "hub",
-      "name": "Foscam"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "foursquare": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Foursquare",
       "integration_type": "hub",
-      "name": "Foursquare"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "free_mobile": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Free Mobile",
       "integration_type": "hub",
-      "name": "Free Mobile"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "freebox": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Freebox",
       "integration_type": "hub",
-      "name": "Freebox"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "freedns": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "FreeDNS",
       "integration_type": "hub",
-      "name": "FreeDNS"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "freedompro": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Freedompro",
       "integration_type": "hub",
-      "name": "Freedompro"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "fritzbox": {
       "name": "FRITZ!Box",
@@ -1681,93 +1754,98 @@
       }
     },
     "fronius": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Fronius",
       "integration_type": "hub",
-      "name": "Fronius"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "frontier_silicon": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Frontier Silicon",
       "integration_type": "hub",
-      "name": "Frontier Silicon"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "fully_kiosk": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Fully Kiosk Browser",
       "integration_type": "hub",
-      "name": "Fully Kiosk Browser"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "futurenow": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "P5 FutureNow",
       "integration_type": "hub",
-      "name": "P5 FutureNow"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "garadget": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Garadget",
       "integration_type": "hub",
-      "name": "Garadget"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "garages_amsterdam": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "cloud_polling",
-      "integration_type": "hub"
+      "iot_class": "cloud_polling"
+    },
+    "gaviota": {
+      "name": "Gaviota",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "gdacs": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Global Disaster Alert and Coordination System (GDACS)",
       "integration_type": "hub",
-      "name": "Global Disaster Alert and Coordination System (GDACS)"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "generic": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Generic Camera",
       "integration_type": "hub",
-      "name": "Generic Camera"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "generic_hygrostat": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Generic hygrostat",
       "integration_type": "hub",
-      "name": "Generic hygrostat"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "generic_thermostat": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Generic Thermostat",
       "integration_type": "hub",
-      "name": "Generic Thermostat"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "geniushub": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Genius Hub",
       "integration_type": "hub",
-      "name": "Genius Hub"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "geo_json_events": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "GeoJSON",
       "integration_type": "hub",
-      "name": "GeoJSON"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "geo_rss_events": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "GeoRSS",
       "integration_type": "hub",
-      "name": "GeoRSS"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "geocaching": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Geocaching",
       "integration_type": "hub",
-      "name": "Geocaching"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "geofency": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Geofency",
       "integration_type": "hub",
-      "name": "Geofency"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "geonet": {
       "name": "GeoNet",
@@ -1787,34 +1865,34 @@
       }
     },
     "gios": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "GIO\u015a",
       "integration_type": "hub",
-      "name": "GIO\u015a"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "github": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "GitHub",
       "integration_type": "hub",
-      "name": "GitHub"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "gitlab_ci": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "GitLab-CI",
       "integration_type": "hub",
-      "name": "GitLab-CI"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "gitter": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Gitter",
       "integration_type": "hub",
-      "name": "Gitter"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "glances": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Glances",
       "integration_type": "hub",
-      "name": "Glances"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "globalcache": {
       "name": "Global Cach\u00e9",
@@ -1834,28 +1912,28 @@
       }
     },
     "goalfeed": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Goalfeed",
       "integration_type": "hub",
-      "name": "Goalfeed"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "goalzero": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Goal Zero Yeti",
       "integration_type": "hub",
-      "name": "Goal Zero Yeti"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "gogogate2": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Gogogate2 and ismartgate",
       "integration_type": "hub",
-      "name": "Gogogate2 and ismartgate"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "goodwe": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "GoodWe Inverter",
       "integration_type": "hub",
-      "name": "GoodWe Inverter"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "google": {
       "name": "Google",
@@ -1946,123 +2024,137 @@
       }
     },
     "govee_ble": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Govee Bluetooth",
       "integration_type": "hub",
-      "name": "Govee Bluetooth"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "gpsd": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "GPSD",
       "integration_type": "hub",
-      "name": "GPSD"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "gpslogger": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "GPSLogger",
       "integration_type": "hub",
-      "name": "GPSLogger"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "graphite": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Graphite",
       "integration_type": "hub",
-      "name": "Graphite"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "gree": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Gree Climate",
       "integration_type": "hub",
-      "name": "Gree Climate"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "greeneye_monitor": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "GreenEye Monitor (GEM)",
       "integration_type": "hub",
-      "name": "GreenEye Monitor (GEM)"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "greenwave": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Greenwave Reality",
       "integration_type": "hub",
-      "name": "Greenwave Reality"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "growatt_server": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "cloud_polling",
-      "integration_type": "hub"
+      "iot_class": "cloud_polling"
     },
     "gstreamer": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "GStreamer",
       "integration_type": "hub",
-      "name": "GStreamer"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "gtfs": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "General Transit Feed Specification (GTFS)",
       "integration_type": "hub",
-      "name": "General Transit Feed Specification (GTFS)"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "guardian": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Elexa Guardian",
       "integration_type": "hub",
-      "name": "Elexa Guardian"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "habitica": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Habitica",
       "integration_type": "hub",
-      "name": "Habitica"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "hardkernel": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Hardkernel",
       "integration_type": "hardware",
-      "name": "Hardkernel"
+      "config_flow": false
     },
     "harman_kardon_avr": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Harman Kardon AVR",
       "integration_type": "hub",
-      "name": "Harman Kardon AVR"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "hassio": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Home Assistant Supervisor",
       "integration_type": "hub",
-      "name": "Home Assistant Supervisor"
+      "config_flow": false,
+      "iot_class": "local_polling"
+    },
+    "havana_shade": {
+      "name": "Havana Shade",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "haveibeenpwned": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "HaveIBeenPwned",
       "integration_type": "hub",
-      "name": "HaveIBeenPwned"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "hddtemp": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "hddtemp",
       "integration_type": "hub",
-      "name": "hddtemp"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "hdmi_cec": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "HDMI-CEC",
       "integration_type": "hub",
-      "name": "HDMI-CEC"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "heatmiser": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Heatmiser",
       "integration_type": "hub",
-      "name": "Heatmiser"
+      "config_flow": false,
+      "iot_class": "local_polling"
+    },
+    "heiwa": {
+      "name": "Heiwa",
+      "integration_type": "virtual",
+      "supported_by": "gree"
     },
     "here_travel_time": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "HERE Travel Time",
       "integration_type": "hub",
-      "name": "HERE Travel Time"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "hi_kumo": {
+      "name": "Hitachi Hi Kumo",
+      "integration_type": "virtual",
+      "supported_by": "overkiz"
     },
     "hikvision": {
       "name": "Hikvision",
@@ -2082,64 +2174,61 @@
       }
     },
     "hisense_aehw4a1": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Hisense AEH-W4A1",
       "integration_type": "hub",
-      "name": "Hisense AEH-W4A1"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "history_stats": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "History Stats",
       "integration_type": "hub",
-      "name": "History Stats"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "hitron_coda": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Rogers Hitron CODA",
       "integration_type": "hub",
-      "name": "Rogers Hitron CODA"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "hive": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Hive",
       "integration_type": "hub",
-      "name": "Hive"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "hlk_sw16": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Hi-Link HLK-SW16",
       "integration_type": "hub",
-      "name": "Hi-Link HLK-SW16"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "home_connect": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Home Connect",
       "integration_type": "hub",
-      "name": "Home Connect"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "home_plus_control": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Legrand Home+ Control",
       "integration_type": "hub",
-      "name": "Legrand Home+ Control"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "homeassistant_alerts": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Home Assistant Alerts",
       "integration_type": "hub",
-      "name": "Home Assistant Alerts"
+      "config_flow": false
     },
     "homeassistant_sky_connect": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Home Assistant Sky Connect",
       "integration_type": "hardware",
-      "name": "Home Assistant Sky Connect"
+      "config_flow": false
     },
     "homeassistant_yellow": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Home Assistant Yellow",
       "integration_type": "hardware",
-      "name": "Home Assistant Yellow"
+      "config_flow": false
     },
     "homematic": {
       "name": "Homematic",
@@ -2159,10 +2248,10 @@
       }
     },
     "homewizard": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "HomeWizard Energy",
       "integration_type": "hub",
-      "name": "HomeWizard Energy"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "honeywell": {
       "name": "Honeywell",
@@ -2188,76 +2277,81 @@
       }
     },
     "horizon": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Unitymedia Horizon HD Recorder",
       "integration_type": "hub",
-      "name": "Unitymedia Horizon HD Recorder"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "hp_ilo": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "HP Integrated Lights-Out (ILO)",
       "integration_type": "hub",
-      "name": "HP Integrated Lights-Out (ILO)"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "html5": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "HTML5 Push Notifications",
       "integration_type": "hub",
-      "name": "HTML5 Push Notifications"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "huawei_lte": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Huawei LTE",
       "integration_type": "hub",
-      "name": "Huawei LTE"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "huisbaasje": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Huisbaasje",
       "integration_type": "hub",
-      "name": "Huisbaasje"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "hunterdouglas_powerview": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Hunter Douglas PowerView",
       "integration_type": "hub",
-      "name": "Hunter Douglas PowerView"
+      "config_flow": true,
+      "iot_class": "local_polling"
+    },
+    "hurrican_shutters_wholesale": {
+      "name": "Hurrican Shutters Wholesale",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "hvv_departures": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "HVV Departures",
       "integration_type": "hub",
-      "name": "HVV Departures"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "hydrawise": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Hunter Hydrawise",
       "integration_type": "hub",
-      "name": "Hunter Hydrawise"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "hyperion": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Hyperion",
       "integration_type": "hub",
-      "name": "Hyperion"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "ialarm": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Antifurto365 iAlarm",
       "integration_type": "hub",
-      "name": "Antifurto365 iAlarm"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "iammeter": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "IamMeter",
       "integration_type": "hub",
-      "name": "IamMeter"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "iaqualink": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Jandy iAqualink",
       "integration_type": "hub",
-      "name": "Jandy iAqualink"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ibm": {
       "name": "IBM",
@@ -2277,64 +2371,64 @@
       }
     },
     "idteck_prox": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "IDTECK Proximity Reader",
       "integration_type": "hub",
-      "name": "IDTECK Proximity Reader"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "ifttt": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "IFTTT",
       "integration_type": "hub",
-      "name": "IFTTT"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "iglo": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "iGlo",
       "integration_type": "hub",
-      "name": "iGlo"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "ign_sismologia": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "IGN Sismolog\u00eda",
       "integration_type": "hub",
-      "name": "IGN Sismolog\u00eda"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "ihc": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "IHC Controller",
       "integration_type": "hub",
-      "name": "IHC Controller"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "imap": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "IMAP",
       "integration_type": "hub",
-      "name": "IMAP"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "imap_email_content": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "IMAP Email Content",
       "integration_type": "hub",
-      "name": "IMAP Email Content"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "incomfort": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Intergas InComfort/Intouch Lan2RF gateway",
       "integration_type": "hub",
-      "name": "Intergas InComfort/Intouch Lan2RF gateway"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "influxdb": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "InfluxDB",
       "integration_type": "hub",
-      "name": "InfluxDB"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "inkbird": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "INKBIRD",
       "integration_type": "hub",
-      "name": "INKBIRD"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "inovelli": {
       "name": "Inovelli",
@@ -2343,94 +2437,103 @@
         "zwave"
       ]
     },
+    "inspired_shades": {
+      "name": "Inspired Shades",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
+    },
     "insteon": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Insteon",
       "integration_type": "hub",
-      "name": "Insteon"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "intellifire": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "IntelliFire",
       "integration_type": "hub",
-      "name": "IntelliFire"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "intent_script": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Intent Script",
       "integration_type": "hub",
-      "name": "Intent Script"
+      "config_flow": false
     },
     "intesishome": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "IntesisHome",
       "integration_type": "hub",
-      "name": "IntesisHome"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "ios": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Home Assistant iOS",
       "integration_type": "hub",
-      "name": "Home Assistant iOS"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "iotawatt": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "IoTaWatt",
       "integration_type": "hub",
-      "name": "IoTaWatt"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "iperf3": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Iperf3",
       "integration_type": "hub",
-      "name": "Iperf3"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "ipma": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)",
       "integration_type": "hub",
-      "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ipp": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Internet Printing Protocol (IPP)",
       "integration_type": "hub",
-      "name": "Internet Printing Protocol (IPP)"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "iqvia": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "IQVIA",
       "integration_type": "hub",
-      "name": "IQVIA"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "irish_rail_transport": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Irish Rail Transport",
       "integration_type": "hub",
-      "name": "Irish Rail Transport"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "islamic_prayer_times": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "cloud_polling",
-      "integration_type": "hub"
+      "iot_class": "cloud_polling"
+    },
+    "ismartwindow": {
+      "name": "iSmartWindow",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "iss": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "International Space Station (ISS)",
       "integration_type": "hub",
-      "name": "International Space Station (ISS)"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "isy994": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Universal Devices ISY994",
       "integration_type": "hub",
-      "name": "Universal Devices ISY994"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "izone": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "iZone",
       "integration_type": "hub",
-      "name": "iZone"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "jasco": {
       "name": "Jasco",
@@ -2439,214 +2542,219 @@
       ]
     },
     "jellyfin": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Jellyfin",
       "integration_type": "hub",
-      "name": "Jellyfin"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "jewish_calendar": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "Jewish Calendar",
       "integration_type": "hub",
-      "name": "Jewish Calendar"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "joaoapps_join": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Joaoapps Join",
       "integration_type": "hub",
-      "name": "Joaoapps Join"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "juicenet": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "JuiceNet",
       "integration_type": "hub",
-      "name": "JuiceNet"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "justnimbus": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "JustNimbus",
       "integration_type": "hub",
-      "name": "JustNimbus"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "kaiterra": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Kaiterra",
       "integration_type": "hub",
-      "name": "Kaiterra"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "kaleidescape": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Kaleidescape",
       "integration_type": "hub",
-      "name": "Kaleidescape"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "kankun": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Kankun",
       "integration_type": "hub",
-      "name": "Kankun"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "keba": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Keba Charging Station",
       "integration_type": "hub",
-      "name": "Keba Charging Station"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "keenetic_ndms2": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Keenetic NDMS2 Router",
       "integration_type": "hub",
-      "name": "Keenetic NDMS2 Router"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "kef": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "KEF",
       "integration_type": "hub",
-      "name": "KEF"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "kegtron": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Kegtron",
       "integration_type": "hub",
-      "name": "Kegtron"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "keyboard": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Keyboard",
       "integration_type": "hub",
-      "name": "Keyboard"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "keyboard_remote": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Keyboard Remote",
       "integration_type": "hub",
-      "name": "Keyboard Remote"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "keymitt_ble": {
-      "config_flow": true,
-      "iot_class": "assumed_state",
+      "name": "Keymitt MicroBot Push",
       "integration_type": "hub",
-      "name": "Keymitt MicroBot Push"
+      "config_flow": true,
+      "iot_class": "assumed_state"
     },
     "kira": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Kira",
       "integration_type": "hub",
-      "name": "Kira"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "kiwi": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "KIWI",
       "integration_type": "hub",
-      "name": "KIWI"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "kmtronic": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "KMtronic",
       "integration_type": "hub",
-      "name": "KMtronic"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "knx": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "KNX",
       "integration_type": "hub",
-      "name": "KNX"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "kodi": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Kodi",
       "integration_type": "hub",
-      "name": "Kodi"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "konnected": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Konnected.io",
       "integration_type": "hub",
-      "name": "Konnected.io"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "kostal_plenticore": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Kostal Plenticore Solar Inverter",
       "integration_type": "hub",
-      "name": "Kostal Plenticore Solar Inverter"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "kraken": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Kraken",
       "integration_type": "hub",
-      "name": "Kraken"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "kulersky": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Kuler Sky",
       "integration_type": "hub",
-      "name": "Kuler Sky"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "kwb": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "KWB Easyfire",
       "integration_type": "hub",
-      "name": "KWB Easyfire"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "lacrosse": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "LaCrosse",
       "integration_type": "hub",
-      "name": "LaCrosse"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "lacrosse_view": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "LaCrosse View",
       "integration_type": "hub",
-      "name": "LaCrosse View"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "lametric": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "LaMetric",
       "integration_type": "hub",
-      "name": "LaMetric"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "landisgyr_heat_meter": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Landis+Gyr Heat Meter",
       "integration_type": "hub",
-      "name": "Landis+Gyr Heat Meter"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "lannouncer": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "LANnouncer",
       "integration_type": "hub",
-      "name": "LANnouncer"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "lastfm": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Last.fm",
       "integration_type": "hub",
-      "name": "Last.fm"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "launch_library": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Launch Library",
       "integration_type": "hub",
-      "name": "Launch Library"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "laundrify": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "laundrify",
       "integration_type": "hub",
-      "name": "laundrify"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "lcn": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "LCN",
       "integration_type": "hub",
-      "name": "LCN"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "led_ble": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "LED BLE",
       "integration_type": "hub",
-      "name": "LED BLE"
+      "config_flow": true,
+      "iot_class": "local_polling"
+    },
+    "legrand": {
+      "name": "Legrand",
+      "integration_type": "virtual",
+      "supported_by": "netatmo"
     },
     "leviton": {
       "name": "Leviton",
@@ -2678,111 +2786,111 @@
       }
     },
     "lidarr": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Lidarr",
       "integration_type": "hub",
-      "name": "Lidarr"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "life360": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Life360",
       "integration_type": "hub",
-      "name": "Life360"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "lifx": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "LIFX",
       "integration_type": "hub",
-      "name": "LIFX"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "lifx_cloud": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "LIFX Cloud",
       "integration_type": "hub",
-      "name": "LIFX Cloud"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "lightwave": {
-      "config_flow": false,
-      "iot_class": "assumed_state",
+      "name": "Lightwave",
       "integration_type": "hub",
-      "name": "Lightwave"
+      "config_flow": false,
+      "iot_class": "assumed_state"
     },
     "limitlessled": {
-      "config_flow": false,
-      "iot_class": "assumed_state",
+      "name": "LimitlessLED",
       "integration_type": "hub",
-      "name": "LimitlessLED"
+      "config_flow": false,
+      "iot_class": "assumed_state"
     },
     "linksys_smart": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Linksys Smart Wi-Fi",
       "integration_type": "hub",
-      "name": "Linksys Smart Wi-Fi"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "linode": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Linode",
       "integration_type": "hub",
-      "name": "Linode"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "linux_battery": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Linux Battery",
       "integration_type": "hub",
-      "name": "Linux Battery"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "lirc": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "LIRC",
       "integration_type": "hub",
-      "name": "LIRC"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "litejet": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "LiteJet",
       "integration_type": "hub",
-      "name": "LiteJet"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "litterrobot": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Litter-Robot",
       "integration_type": "hub",
-      "name": "Litter-Robot"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "llamalab_automate": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "LlamaLab Automate",
       "integration_type": "hub",
-      "name": "LlamaLab Automate"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "local_file": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Local File",
       "integration_type": "hub",
-      "name": "Local File"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "local_ip": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_polling",
-      "integration_type": "hub"
+      "iot_class": "local_polling"
     },
     "locative": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Locative",
       "integration_type": "hub",
-      "name": "Locative"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "logentries": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Logentries",
       "integration_type": "hub",
-      "name": "Logentries"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "logi_circle": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Logi Circle",
       "integration_type": "hub",
-      "name": "Logi Circle"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "logitech": {
       "name": "Logitech",
@@ -2808,34 +2916,34 @@
       }
     },
     "london_air": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "London Air",
       "integration_type": "hub",
-      "name": "London Air"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "london_underground": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "London Underground",
       "integration_type": "hub",
-      "name": "London Underground"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "lookin": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "LOOKin",
       "integration_type": "hub",
-      "name": "LOOKin"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "luftdaten": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Sensor.Community",
       "integration_type": "hub",
-      "name": "Sensor.Community"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "lupusec": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Lupus Electronics LUPUSEC",
       "integration_type": "hub",
-      "name": "Lupus Electronics LUPUSEC"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "lutron": {
       "name": "Lutron",
@@ -2860,89 +2968,103 @@
         }
       }
     },
+    "luxaflex": {
+      "name": "Luxaflex",
+      "integration_type": "virtual",
+      "supported_by": "hunterdouglas_powerview"
+    },
     "lw12wifi": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "LAGUTE LW-12",
       "integration_type": "hub",
-      "name": "LAGUTE LW-12"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "magicseaweed": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Magicseaweed",
       "integration_type": "hub",
-      "name": "Magicseaweed"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "mailgun": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Mailgun",
       "integration_type": "hub",
-      "name": "Mailgun"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "manual": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "Manual Alarm Control Panel",
       "integration_type": "hub",
-      "name": "Manual Alarm Control Panel"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "map": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Map",
       "integration_type": "hub",
-      "name": "Map"
+      "config_flow": false
+    },
+    "marantz": {
+      "name": "Marantz",
+      "integration_type": "virtual",
+      "supported_by": "denonavr"
+    },
+    "martec": {
+      "name": "Martec",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "marytts": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "MaryTTS",
       "integration_type": "hub",
-      "name": "MaryTTS"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "mastodon": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Mastodon",
       "integration_type": "hub",
-      "name": "Mastodon"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "matrix": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Matrix",
       "integration_type": "hub",
-      "name": "Matrix"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "mazda": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Mazda Connected Services",
       "integration_type": "hub",
-      "name": "Mazda Connected Services"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "meater": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Meater",
       "integration_type": "hub",
-      "name": "Meater"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "media_extractor": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "Media Extractor",
       "integration_type": "hub",
-      "name": "Media Extractor"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "mediaroom": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Mediaroom",
       "integration_type": "hub",
-      "name": "Mediaroom"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "melcloud": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "MELCloud",
       "integration_type": "hub",
-      "name": "MELCloud"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "melissa": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Melissa",
       "integration_type": "hub",
-      "name": "Melissa"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "melnor": {
       "name": "Melnor",
@@ -2962,58 +3084,58 @@
       }
     },
     "meraki": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Meraki",
       "integration_type": "hub",
-      "name": "Meraki"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "message_bird": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "MessageBird",
       "integration_type": "hub",
-      "name": "MessageBird"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "met": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Meteorologisk institutt (Met.no)",
       "integration_type": "hub",
-      "name": "Meteorologisk institutt (Met.no)"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "met_eireann": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Met \u00c9ireann",
       "integration_type": "hub",
-      "name": "Met \u00c9ireann"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "meteo_france": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "M\u00e9t\u00e9o-France",
       "integration_type": "hub",
-      "name": "M\u00e9t\u00e9o-France"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "meteoalarm": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "MeteoAlarm",
       "integration_type": "hub",
-      "name": "MeteoAlarm"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "meteoclimatic": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Meteoclimatic",
       "integration_type": "hub",
-      "name": "Meteoclimatic"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "metoffice": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Met Office",
       "integration_type": "hub",
-      "name": "Met Office"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "mfi": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Ubiquiti mFi mPort",
       "integration_type": "hub",
-      "name": "Ubiquiti mFi mPort"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "microsoft": {
       "name": "Microsoft",
@@ -3081,121 +3203,121 @@
       }
     },
     "miflora": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Mi Flora",
       "integration_type": "hub",
-      "name": "Mi Flora"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "mikrotik": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Mikrotik",
       "integration_type": "hub",
-      "name": "Mikrotik"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "mill": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Mill",
       "integration_type": "hub",
-      "name": "Mill"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "minecraft_server": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Minecraft Server",
       "integration_type": "hub",
-      "name": "Minecraft Server"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "minio": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Minio",
       "integration_type": "hub",
-      "name": "Minio"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "mitemp_bt": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor",
       "integration_type": "hub",
-      "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "mjpeg": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "MJPEG IP Camera",
       "integration_type": "hub",
-      "name": "MJPEG IP Camera"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "moat": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Moat",
       "integration_type": "hub",
-      "name": "Moat"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "mobile_app": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "hub"
+      "iot_class": "local_push"
     },
     "mochad": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Mochad",
       "integration_type": "hub",
-      "name": "Mochad"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "modbus": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Modbus",
       "integration_type": "hub",
-      "name": "Modbus"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "modem_callerid": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Phone Modem",
       "integration_type": "hub",
-      "name": "Phone Modem"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "modern_forms": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Modern Forms",
       "integration_type": "hub",
-      "name": "Modern Forms"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "moehlenhoff_alpha2": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "hub"
+      "iot_class": "local_push"
     },
     "mold_indicator": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Mold Indicator",
       "integration_type": "hub",
-      "name": "Mold Indicator"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "monoprice": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Monoprice 6-Zone Amplifier",
       "integration_type": "hub",
-      "name": "Monoprice 6-Zone Amplifier"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "moon": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_polling",
-      "integration_type": "hub"
+      "iot_class": "local_polling"
     },
     "motion_blinds": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Motion Blinds",
       "integration_type": "hub",
-      "name": "Motion Blinds"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "motioneye": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "motionEye",
       "integration_type": "hub",
-      "name": "motionEye"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "mpd": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Music Player Daemon (MPD)",
       "integration_type": "hub",
-      "name": "Music Player Daemon (MPD)"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "mqtt": {
       "name": "MQTT",
@@ -3239,106 +3361,106 @@
       }
     },
     "mullvad": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Mullvad VPN",
       "integration_type": "hub",
-      "name": "Mullvad VPN"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "mutesync": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "mutesync",
       "integration_type": "hub",
-      "name": "mutesync"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "mvglive": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "MVG",
       "integration_type": "hub",
-      "name": "MVG"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "mycroft": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Mycroft",
       "integration_type": "hub",
-      "name": "Mycroft"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "myq": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "MyQ",
       "integration_type": "hub",
-      "name": "MyQ"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "mysensors": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "MySensors",
       "integration_type": "hub",
-      "name": "MySensors"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "mystrom": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "myStrom",
       "integration_type": "hub",
-      "name": "myStrom"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "mythicbeastsdns": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Mythic Beasts DNS",
       "integration_type": "hub",
-      "name": "Mythic Beasts DNS"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "nad": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "NAD",
       "integration_type": "hub",
-      "name": "NAD"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "nam": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Nettigo Air Monitor",
       "integration_type": "hub",
-      "name": "Nettigo Air Monitor"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "namecheapdns": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Namecheap FreeDNS",
       "integration_type": "hub",
-      "name": "Namecheap FreeDNS"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "nanoleaf": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Nanoleaf",
       "integration_type": "hub",
-      "name": "Nanoleaf"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "neato": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Neato Botvac",
       "integration_type": "hub",
-      "name": "Neato Botvac"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "nederlandse_spoorwegen": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Nederlandse Spoorwegen (NS)",
       "integration_type": "hub",
-      "name": "Nederlandse Spoorwegen (NS)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "ness_alarm": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Ness Alarm",
       "integration_type": "hub",
-      "name": "Ness Alarm"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "netatmo": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Netatmo",
       "integration_type": "hub",
-      "name": "Netatmo"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "netdata": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Netdata",
       "integration_type": "hub",
-      "name": "Netdata"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "netgear": {
       "name": "NETGEAR",
@@ -3358,345 +3480,355 @@
       }
     },
     "netio": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Netio",
       "integration_type": "hub",
-      "name": "Netio"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "neurio_energy": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Neurio energy",
       "integration_type": "hub",
-      "name": "Neurio energy"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "nexia": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Nexia/American Standard/Trane",
       "integration_type": "hub",
-      "name": "Nexia/American Standard/Trane"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "nexity": {
+      "name": "Nexity Eug\u00e9nie",
+      "integration_type": "virtual",
+      "supported_by": "overkiz"
     },
     "nextbus": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "NextBus",
       "integration_type": "hub",
-      "name": "NextBus"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "nextcloud": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Nextcloud",
       "integration_type": "hub",
-      "name": "Nextcloud"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "nextdns": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "NextDNS",
       "integration_type": "hub",
-      "name": "NextDNS"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "nfandroidtv": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Notifications for Android TV / Fire TV",
       "integration_type": "hub",
-      "name": "Notifications for Android TV / Fire TV"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "nibe_heatpump": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Nibe Heat Pump",
       "integration_type": "hub",
-      "name": "Nibe Heat Pump"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "nightscout": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Nightscout",
       "integration_type": "hub",
-      "name": "Nightscout"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "niko_home_control": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Niko Home Control",
       "integration_type": "hub",
-      "name": "Niko Home Control"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "nilu": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Norwegian Institute for Air Research (NILU)",
       "integration_type": "hub",
-      "name": "Norwegian Institute for Air Research (NILU)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "nina": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "NINA",
       "integration_type": "hub",
-      "name": "NINA"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "nissan_leaf": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Nissan Leaf",
       "integration_type": "hub",
-      "name": "Nissan Leaf"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "nmap_tracker": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_polling",
-      "integration_type": "hub"
+      "iot_class": "local_polling"
     },
     "nmbs": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "NMBS",
       "integration_type": "hub",
-      "name": "NMBS"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "no_ip": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "No-IP.com",
       "integration_type": "hub",
-      "name": "No-IP.com"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "noaa_tides": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "NOAA Tides",
       "integration_type": "hub",
-      "name": "NOAA Tides"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "nobo_hub": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Nob\u00f8 Ecohub",
       "integration_type": "hub",
-      "name": "Nob\u00f8 Ecohub"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "norway_air": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Om Luftkvalitet i Norge (Norway Air)",
       "integration_type": "hub",
-      "name": "Om Luftkvalitet i Norge (Norway Air)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "notify_events": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Notify.Events",
       "integration_type": "hub",
-      "name": "Notify.Events"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "notion": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Notion",
       "integration_type": "hub",
-      "name": "Notion"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "nsw_fuel_station": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "NSW Fuel Station Price",
       "integration_type": "hub",
-      "name": "NSW Fuel Station Price"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "nsw_rural_fire_service_feed": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "NSW Rural Fire Service Incidents",
       "integration_type": "hub",
-      "name": "NSW Rural Fire Service Incidents"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "nuheat": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "NuHeat",
       "integration_type": "hub",
-      "name": "NuHeat"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "nuki": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Nuki",
       "integration_type": "hub",
-      "name": "Nuki"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "numato": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Numato USB GPIO Expander",
       "integration_type": "hub",
-      "name": "Numato USB GPIO Expander"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "nut": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Network UPS Tools (NUT)",
       "integration_type": "hub",
-      "name": "Network UPS Tools (NUT)"
+      "config_flow": true,
+      "iot_class": "local_polling"
+    },
+    "nutrichef": {
+      "name": "Nutrichef",
+      "integration_type": "virtual",
+      "supported_by": "inkbird"
     },
     "nws": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "National Weather Service (NWS)",
       "integration_type": "hub",
-      "name": "National Weather Service (NWS)"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "nx584": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "NX584",
       "integration_type": "hub",
-      "name": "NX584"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "nzbget": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "NZBGet",
       "integration_type": "hub",
-      "name": "NZBGet"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "oasa_telematics": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "OASA Telematics",
       "integration_type": "hub",
-      "name": "OASA Telematics"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "obihai": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Obihai",
       "integration_type": "hub",
-      "name": "Obihai"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "octoprint": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "OctoPrint",
       "integration_type": "hub",
-      "name": "OctoPrint"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "oem": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "OpenEnergyMonitor WiFi Thermostat",
       "integration_type": "hub",
-      "name": "OpenEnergyMonitor WiFi Thermostat"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "ohmconnect": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "OhmConnect",
       "integration_type": "hub",
-      "name": "OhmConnect"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "ombi": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Ombi",
       "integration_type": "hub",
-      "name": "Ombi"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "omnilogic": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Hayward Omnilogic",
       "integration_type": "hub",
-      "name": "Hayward Omnilogic"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "oncue": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Oncue by Kohler",
       "integration_type": "hub",
-      "name": "Oncue by Kohler"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ondilo_ico": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Ondilo ICO",
       "integration_type": "hub",
-      "name": "Ondilo ICO"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "onewire": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "1-Wire",
       "integration_type": "hub",
-      "name": "1-Wire"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "onkyo": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Onkyo",
       "integration_type": "hub",
-      "name": "Onkyo"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "onvif": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "ONVIF",
       "integration_type": "hub",
-      "name": "ONVIF"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "open_meteo": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Open-Meteo",
       "integration_type": "hub",
-      "name": "Open-Meteo"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "openalpr_cloud": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "OpenALPR Cloud",
       "integration_type": "hub",
-      "name": "OpenALPR Cloud"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "openalpr_local": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "OpenALPR Local",
       "integration_type": "hub",
-      "name": "OpenALPR Local"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "opencv": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "OpenCV",
       "integration_type": "hub",
-      "name": "OpenCV"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "openerz": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Open ERZ",
       "integration_type": "hub",
-      "name": "Open ERZ"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "openevse": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "OpenEVSE",
       "integration_type": "hub",
-      "name": "OpenEVSE"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "openexchangerates": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Open Exchange Rates",
       "integration_type": "hub",
-      "name": "Open Exchange Rates"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "opengarage": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "OpenGarage",
       "integration_type": "hub",
-      "name": "OpenGarage"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "openhardwaremonitor": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Open Hardware Monitor",
       "integration_type": "hub",
-      "name": "Open Hardware Monitor"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "openhome": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Linn / OpenHome",
       "integration_type": "hub",
-      "name": "Linn / OpenHome"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "opensensemap": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "openSenseMap",
       "integration_type": "hub",
-      "name": "openSenseMap"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "opensky": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "OpenSky Network",
       "integration_type": "hub",
-      "name": "OpenSky Network"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "opentherm_gw": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "OpenTherm Gateway",
       "integration_type": "hub",
-      "name": "OpenTherm Gateway"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "openuv": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "OpenUV",
       "integration_type": "hub",
-      "name": "OpenUV"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "openweathermap": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "OpenWeatherMap",
       "integration_type": "hub",
-      "name": "OpenWeatherMap"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "openwrt": {
       "name": "OpenWrt",
@@ -3716,64 +3848,64 @@
       }
     },
     "opnsense": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "OPNSense",
       "integration_type": "hub",
-      "name": "OPNSense"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "opple": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Opple",
       "integration_type": "hub",
-      "name": "Opple"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "oru": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Orange and Rockland Utility (ORU)",
       "integration_type": "hub",
-      "name": "Orange and Rockland Utility (ORU)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "orvibo": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Orvibo",
       "integration_type": "hub",
-      "name": "Orvibo"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "osramlightify": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Osramlightify",
       "integration_type": "hub",
-      "name": "Osramlightify"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "otp": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "One-Time Password (OTP)",
       "integration_type": "hub",
-      "name": "One-Time Password (OTP)"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "overkiz": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Overkiz",
       "integration_type": "hub",
-      "name": "Overkiz"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ovo_energy": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "OVO Energy",
       "integration_type": "hub",
-      "name": "OVO Energy"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "owntracks": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "OwnTracks",
       "integration_type": "hub",
-      "name": "OwnTracks"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "p1_monitor": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "P1 Monitor",
       "integration_type": "hub",
-      "name": "P1 Monitor"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "panasonic": {
       "name": "Panasonic",
@@ -3793,40 +3925,43 @@
       }
     },
     "pandora": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Pandora",
       "integration_type": "hub",
-      "name": "Pandora"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "panel_custom": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Custom Panel",
       "integration_type": "hub",
-      "name": "Custom Panel"
+      "config_flow": false
     },
     "panel_iframe": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "iframe Panel",
       "integration_type": "hub",
-      "name": "iframe Panel"
+      "config_flow": false
+    },
+    "pcs_lighting": {
+      "name": "PCS Lighting",
+      "integration_type": "virtual",
+      "supported_by": "upb"
     },
     "peco": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "PECO Outage Counter",
       "integration_type": "hub",
-      "name": "PECO Outage Counter"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "pencom": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Pencom",
       "integration_type": "hub",
-      "name": "Pencom"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "persistent_notification": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Persistent Notification",
       "integration_type": "hub",
-      "name": "Persistent Notification"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "philips": {
       "name": "Philips",
@@ -3852,230 +3987,226 @@
       }
     },
     "pi_hole": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Pi-hole",
       "integration_type": "hub",
-      "name": "Pi-hole"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "picnic": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Picnic",
       "integration_type": "hub",
-      "name": "Picnic"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "picotts": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Pico TTS",
       "integration_type": "hub",
-      "name": "Pico TTS"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "pilight": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Pilight",
       "integration_type": "hub",
-      "name": "Pilight"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "ping": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Ping (ICMP)",
       "integration_type": "hub",
-      "name": "Ping (ICMP)"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "pioneer": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Pioneer",
       "integration_type": "hub",
-      "name": "Pioneer"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "pjlink": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "PJLink",
       "integration_type": "hub",
-      "name": "PJLink"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "plaato": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Plaato",
       "integration_type": "hub",
-      "name": "Plaato"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "plant": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "hub"
+      "integration_type": "hub",
+      "config_flow": false
     },
     "plex": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Plex Media Server",
       "integration_type": "hub",
-      "name": "Plex Media Server"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "plugwise": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Plugwise",
       "integration_type": "hub",
-      "name": "Plugwise"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "plum_lightpad": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Plum Lightpad",
       "integration_type": "hub",
-      "name": "Plum Lightpad"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "pocketcasts": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Pocket Casts",
       "integration_type": "hub",
-      "name": "Pocket Casts"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "point": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Minut Point",
       "integration_type": "hub",
-      "name": "Minut Point"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "poolsense": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "PoolSense",
       "integration_type": "hub",
-      "name": "PoolSense"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "profiler": {
-      "config_flow": true,
-      "iot_class": null,
+      "name": "Profiler",
       "integration_type": "hub",
-      "name": "Profiler"
+      "config_flow": true
     },
     "progettihwsw": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "ProgettiHWSW Automation",
       "integration_type": "hub",
-      "name": "ProgettiHWSW Automation"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "proliphix": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Proliphix",
       "integration_type": "hub",
-      "name": "Proliphix"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "prometheus": {
-      "config_flow": false,
-      "iot_class": "assumed_state",
+      "name": "Prometheus",
       "integration_type": "hub",
-      "name": "Prometheus"
+      "config_flow": false,
+      "iot_class": "assumed_state"
     },
     "prosegur": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Prosegur Alarm",
       "integration_type": "hub",
-      "name": "Prosegur Alarm"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "prowl": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Prowl",
       "integration_type": "hub",
-      "name": "Prowl"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "proximity": {
+      "integration_type": "hub",
       "config_flow": false,
-      "iot_class": "calculated",
-      "integration_type": "hub"
+      "iot_class": "calculated"
     },
     "proxmoxve": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Proxmox VE",
       "integration_type": "hub",
-      "name": "Proxmox VE"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "proxy": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Camera Proxy",
       "integration_type": "hub",
-      "name": "Camera Proxy"
+      "config_flow": false
     },
     "prusalink": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "PrusaLink",
       "integration_type": "hub",
-      "name": "PrusaLink"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "pulseaudio_loopback": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "PulseAudio Loopback",
       "integration_type": "hub",
-      "name": "PulseAudio Loopback"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "pure_energie": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Pure Energie",
       "integration_type": "hub",
-      "name": "Pure Energie"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "push": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Push",
       "integration_type": "hub",
-      "name": "Push"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "pushbullet": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Pushbullet",
       "integration_type": "hub",
-      "name": "Pushbullet"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "pushover": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Pushover",
       "integration_type": "hub",
-      "name": "Pushover"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "pushsafer": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Pushsafer",
       "integration_type": "hub",
-      "name": "Pushsafer"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "pvoutput": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "PVOutput",
       "integration_type": "hub",
-      "name": "PVOutput"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "pvpc_hourly_pricing": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Spain electricity hourly pricing (PVPC)",
       "integration_type": "hub",
-      "name": "Spain electricity hourly pricing (PVPC)"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "pyload": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "pyLoad",
       "integration_type": "hub",
-      "name": "pyLoad"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "python_script": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Python Scripts",
       "integration_type": "hub",
-      "name": "Python Scripts"
+      "config_flow": false
     },
     "qbittorrent": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "qBittorrent",
       "integration_type": "hub",
-      "name": "qBittorrent"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "qingping": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Qingping",
       "integration_type": "hub",
-      "name": "Qingping"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "qld_bushfire": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Queensland Bushfire Alert",
       "integration_type": "hub",
-      "name": "Queensland Bushfire Alert"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "qnap": {
       "name": "QNAP",
@@ -4095,76 +4226,76 @@
       }
     },
     "qrcode": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "QR Code",
       "integration_type": "hub",
-      "name": "QR Code"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "quantum_gateway": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Quantum Gateway",
       "integration_type": "hub",
-      "name": "Quantum Gateway"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "qvr_pro": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "QVR Pro",
       "integration_type": "hub",
-      "name": "QVR Pro"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "qwikswitch": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "QwikSwitch QSUSB",
       "integration_type": "hub",
-      "name": "QwikSwitch QSUSB"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "rachio": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Rachio",
       "integration_type": "hub",
-      "name": "Rachio"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "radarr": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Radarr",
       "integration_type": "hub",
-      "name": "Radarr"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "radio_browser": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Radio Browser",
       "integration_type": "hub",
-      "name": "Radio Browser"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "radiotherm": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Radio Thermostat",
       "integration_type": "hub",
-      "name": "Radio Thermostat"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "rainbird": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Rain Bird",
       "integration_type": "hub",
-      "name": "Rain Bird"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "rainforest_eagle": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Rainforest Eagle",
       "integration_type": "hub",
-      "name": "Rainforest Eagle"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "rainmachine": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "RainMachine",
       "integration_type": "hub",
-      "name": "RainMachine"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "random": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Random",
       "integration_type": "hub",
-      "name": "Random"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "raspberry": {
       "name": "Raspberry Pi",
@@ -4189,184 +4320,198 @@
       }
     },
     "raspberry_pi": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Raspberry Pi",
       "integration_type": "hardware",
-      "name": "Raspberry Pi"
+      "config_flow": false
     },
     "raspyrfm": {
-      "config_flow": false,
-      "iot_class": "assumed_state",
+      "name": "RaspyRFM",
       "integration_type": "hub",
-      "name": "RaspyRFM"
+      "config_flow": false,
+      "iot_class": "assumed_state"
+    },
+    "raven_rock_mfg": {
+      "name": "Raven Rock MFG",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "rdw": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "RDW",
       "integration_type": "hub",
-      "name": "RDW"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "recollect_waste": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "ReCollect Waste",
       "integration_type": "hub",
-      "name": "ReCollect Waste"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "recswitch": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Ankuoo REC Switch",
       "integration_type": "hub",
-      "name": "Ankuoo REC Switch"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "reddit": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Reddit",
       "integration_type": "hub",
-      "name": "Reddit"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "rejseplanen": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Rejseplanen",
       "integration_type": "hub",
-      "name": "Rejseplanen"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "remember_the_milk": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Remember The Milk",
       "integration_type": "hub",
-      "name": "Remember The Milk"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "renault": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Renault",
       "integration_type": "hub",
-      "name": "Renault"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "repetier": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Repetier-Server",
       "integration_type": "hub",
-      "name": "Repetier-Server"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "rest": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "RESTful",
       "integration_type": "hub",
-      "name": "RESTful"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "rest_command": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "RESTful Command",
       "integration_type": "hub",
-      "name": "RESTful Command"
+      "config_flow": false,
+      "iot_class": "local_push"
+    },
+    "rexel": {
+      "name": "Rexel Energeasy Connect",
+      "integration_type": "virtual",
+      "supported_by": "overkiz"
     },
     "rflink": {
-      "config_flow": false,
-      "iot_class": "assumed_state",
+      "name": "RFLink",
       "integration_type": "hub",
-      "name": "RFLink"
+      "config_flow": false,
+      "iot_class": "assumed_state"
     },
     "rfxtrx": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "RFXCOM RFXtrx",
       "integration_type": "hub",
-      "name": "RFXCOM RFXtrx"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "rhasspy": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Rhasspy",
       "integration_type": "hub",
-      "name": "Rhasspy"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "ridwell": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Ridwell",
       "integration_type": "hub",
-      "name": "Ridwell"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ring": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Ring",
       "integration_type": "hub",
-      "name": "Ring"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "ripple": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Ripple",
       "integration_type": "hub",
-      "name": "Ripple"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "risco": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Risco",
       "integration_type": "hub",
-      "name": "Risco"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "rituals_perfume_genie": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Rituals Perfume Genie",
       "integration_type": "hub",
-      "name": "Rituals Perfume Genie"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "rmvtransport": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "RMV",
       "integration_type": "hub",
-      "name": "RMV"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
+    },
+    "roborock": {
+      "name": "Roborock",
+      "integration_type": "virtual",
+      "supported_by": "xiaomi_miio"
     },
     "rocketchat": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Rocket.Chat",
       "integration_type": "hub",
-      "name": "Rocket.Chat"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "roku": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Roku",
       "integration_type": "hub",
-      "name": "Roku"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "roomba": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "iRobot Roomba and Braava",
       "integration_type": "hub",
-      "name": "iRobot Roomba and Braava"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "roon": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "RoonLabs music player",
       "integration_type": "hub",
-      "name": "RoonLabs music player"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "rova": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "ROVA",
       "integration_type": "hub",
-      "name": "ROVA"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "rss_feed_template": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "RSS Feed Template",
       "integration_type": "hub",
-      "name": "RSS Feed Template"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "rtorrent": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "rTorrent",
       "integration_type": "hub",
-      "name": "rTorrent"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "rtsp_to_webrtc": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "RTSPtoWebRTC",
       "integration_type": "hub",
-      "name": "RTSPtoWebRTC"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "ruckus_unleashed": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Ruckus Unleashed",
       "integration_type": "hub",
-      "name": "Ruckus Unleashed"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "russound": {
       "name": "Russound",
@@ -4386,16 +4531,16 @@
       }
     },
     "sabnzbd": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "SABnzbd",
       "integration_type": "hub",
-      "name": "SABnzbd"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "saj": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "SAJ Solar Inverter",
       "integration_type": "hub",
-      "name": "SAJ Solar Inverter"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "samsung": {
       "name": "Samsung",
@@ -4421,327 +4566,357 @@
       }
     },
     "satel_integra": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Satel Integra",
       "integration_type": "hub",
-      "name": "Satel Integra"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "schluter": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Schluter",
       "integration_type": "hub",
-      "name": "Schluter"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "scrape": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Scrape",
       "integration_type": "hub",
-      "name": "Scrape"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
+    },
+    "screenaway": {
+      "name": "ScreenAway",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "screenlogic": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Pentair ScreenLogic",
       "integration_type": "hub",
-      "name": "Pentair ScreenLogic"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "scsgate": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "SCSGate",
       "integration_type": "hub",
-      "name": "SCSGate"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "season": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Season",
       "integration_type": "hub",
-      "name": "Season"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "sendgrid": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "SendGrid",
       "integration_type": "hub",
-      "name": "SendGrid"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "sense": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Sense",
       "integration_type": "hub",
-      "name": "Sense"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "senseme": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "SenseME",
       "integration_type": "hub",
-      "name": "SenseME"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "sensibo": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Sensibo",
       "integration_type": "hub",
-      "name": "Sensibo"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "sensorblue": {
+      "name": "SensorBlue",
+      "integration_type": "virtual",
+      "supported_by": "thermobeacon"
     },
     "sensorpro": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "SensorPro",
       "integration_type": "hub",
-      "name": "SensorPro"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "sensorpush": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "SensorPush",
       "integration_type": "hub",
-      "name": "SensorPush"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "sentry": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Sentry",
       "integration_type": "hub",
-      "name": "Sentry"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "senz": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "nVent RAYCHEM SENZ",
       "integration_type": "hub",
-      "name": "nVent RAYCHEM SENZ"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "serial": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Serial",
       "integration_type": "hub",
-      "name": "Serial"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "serial_pm": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Serial Particulate Matter",
       "integration_type": "hub",
-      "name": "Serial Particulate Matter"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "sesame": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Sesame Smart Lock",
       "integration_type": "hub",
-      "name": "Sesame Smart Lock"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "seven_segments": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Seven Segments OCR",
       "integration_type": "hub",
-      "name": "Seven Segments OCR"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "seventeentrack": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "17TRACK",
       "integration_type": "hub",
-      "name": "17TRACK"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "sharkiq": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Shark IQ",
       "integration_type": "hub",
-      "name": "Shark IQ"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "shell_command": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Shell Command",
       "integration_type": "hub",
-      "name": "Shell Command"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "shelly": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Shelly",
       "integration_type": "hub",
-      "name": "Shelly"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "shiftr": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "shiftr.io",
       "integration_type": "hub",
-      "name": "shiftr.io"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "shodan": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Shodan",
       "integration_type": "hub",
-      "name": "Shodan"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "shopping_list": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "hub"
+      "iot_class": "local_push"
     },
     "sia": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "SIA Alarm Systems",
       "integration_type": "hub",
-      "name": "SIA Alarm Systems"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "sigfox": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Sigfox",
       "integration_type": "hub",
-      "name": "Sigfox"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "sighthound": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Sighthound",
       "integration_type": "hub",
-      "name": "Sighthound"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "signal_messenger": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Signal Messenger",
       "integration_type": "hub",
-      "name": "Signal Messenger"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "simplepush": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Simplepush",
       "integration_type": "hub",
-      "name": "Simplepush"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "simplisafe": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "SimpliSafe",
       "integration_type": "hub",
-      "name": "SimpliSafe"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "simply_automated": {
+      "name": "Simply Automated",
+      "integration_type": "virtual",
+      "supported_by": "upb"
     },
     "simulated": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Simulated",
       "integration_type": "hub",
-      "name": "Simulated"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "sinch": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Sinch SMS",
       "integration_type": "hub",
-      "name": "Sinch SMS"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "sisyphus": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Sisyphus",
       "integration_type": "hub",
-      "name": "Sisyphus"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "sky_hub": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Sky Hub",
       "integration_type": "hub",
-      "name": "Sky Hub"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "skybeacon": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Skybeacon",
       "integration_type": "hub",
-      "name": "Skybeacon"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "skybell": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "SkyBell",
       "integration_type": "hub",
-      "name": "SkyBell"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "slack": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Slack",
       "integration_type": "hub",
-      "name": "Slack"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "sleepiq": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "SleepIQ",
       "integration_type": "hub",
-      "name": "SleepIQ"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "slide": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Slide",
       "integration_type": "hub",
-      "name": "Slide"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "slimproto": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "SlimProto (Squeezebox players)",
       "integration_type": "hub",
-      "name": "SlimProto (Squeezebox players)"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "sma": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "SMA Solar",
       "integration_type": "hub",
-      "name": "SMA Solar"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "smappee": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Smappee",
       "integration_type": "hub",
-      "name": "Smappee"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "smart_blinds": {
+      "name": "Smart Blinds",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
+    },
+    "smart_home": {
+      "name": "Smart Home",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "smart_meter_texas": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Smart Meter Texas",
       "integration_type": "hub",
-      "name": "Smart Meter Texas"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
+    },
+    "smarther": {
+      "name": "Smarther",
+      "integration_type": "virtual",
+      "supported_by": "netatmo"
     },
     "smartthings": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "SmartThings",
       "integration_type": "hub",
-      "name": "SmartThings"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "smarttub": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "SmartTub",
       "integration_type": "hub",
-      "name": "SmartTub"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "smarty": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Salda Smarty",
       "integration_type": "hub",
-      "name": "Salda Smarty"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "smhi": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "SMHI",
       "integration_type": "hub",
-      "name": "SMHI"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "sms": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "SMS notifications via GSM-modem",
       "integration_type": "hub",
-      "name": "SMS notifications via GSM-modem"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "smtp": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "SMTP",
       "integration_type": "hub",
-      "name": "SMTP"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "snapcast": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Snapcast",
       "integration_type": "hub",
-      "name": "Snapcast"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "snips": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Snips",
       "integration_type": "hub",
-      "name": "Snips"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "snmp": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "SNMP",
       "integration_type": "hub",
-      "name": "SNMP"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "snooz": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Snooz",
       "integration_type": "hub",
-      "name": "Snooz"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "solaredge": {
       "name": "SolarEdge",
@@ -4761,40 +4936,45 @@
       }
     },
     "solarlog": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Solar-Log",
       "integration_type": "hub",
-      "name": "Solar-Log"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "solax": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "SolaX Power",
       "integration_type": "hub",
-      "name": "SolaX Power"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "soma": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Soma Connect",
       "integration_type": "hub",
-      "name": "Soma Connect"
+      "config_flow": true,
+      "iot_class": "local_polling"
+    },
+    "somfy": {
+      "name": "Somfy",
+      "integration_type": "virtual",
+      "supported_by": "overkiz"
     },
     "somfy_mylink": {
-      "config_flow": true,
-      "iot_class": "assumed_state",
+      "name": "Somfy MyLink",
       "integration_type": "hub",
-      "name": "Somfy MyLink"
+      "config_flow": true,
+      "iot_class": "assumed_state"
     },
     "sonarr": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Sonarr",
       "integration_type": "hub",
-      "name": "Sonarr"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "sonos": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Sonos",
       "integration_type": "hub",
-      "name": "Sonos"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "sony": {
       "name": "Sony",
@@ -4826,207 +5006,207 @@
       }
     },
     "soundtouch": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Bose SoundTouch",
       "integration_type": "hub",
-      "name": "Bose SoundTouch"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "spaceapi": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Space API",
       "integration_type": "hub",
-      "name": "Space API"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "spc": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Vanderbilt SPC",
       "integration_type": "hub",
-      "name": "Vanderbilt SPC"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "speedtestdotnet": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Speedtest.net",
       "integration_type": "hub",
-      "name": "Speedtest.net"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "spider": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Itho Daalderop Spider",
       "integration_type": "hub",
-      "name": "Itho Daalderop Spider"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "splunk": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Splunk",
       "integration_type": "hub",
-      "name": "Splunk"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "spotify": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Spotify",
       "integration_type": "hub",
-      "name": "Spotify"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "sql": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "SQL",
       "integration_type": "hub",
-      "name": "SQL"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "srp_energy": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "SRP Energy",
       "integration_type": "hub",
-      "name": "SRP Energy"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "starline": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "StarLine",
       "integration_type": "hub",
-      "name": "StarLine"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "starlingbank": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Starling Bank",
       "integration_type": "hub",
-      "name": "Starling Bank"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "startca": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Start.ca",
       "integration_type": "hub",
-      "name": "Start.ca"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "statistics": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Statistics",
       "integration_type": "hub",
-      "name": "Statistics"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "statsd": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "StatsD",
       "integration_type": "hub",
-      "name": "StatsD"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "steam_online": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Steam",
       "integration_type": "hub",
-      "name": "Steam"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "steamist": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Steamist",
       "integration_type": "hub",
-      "name": "Steamist"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "stiebel_eltron": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "STIEBEL ELTRON",
       "integration_type": "hub",
-      "name": "STIEBEL ELTRON"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "stookalert": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "RIVM Stookalert",
       "integration_type": "hub",
-      "name": "RIVM Stookalert"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "stream": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Stream",
       "integration_type": "hub",
-      "name": "Stream"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "streamlabswater": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "StreamLabs",
       "integration_type": "hub",
-      "name": "StreamLabs"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "subaru": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Subaru",
       "integration_type": "hub",
-      "name": "Subaru"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "suez_water": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Suez Water",
       "integration_type": "hub",
-      "name": "Suez Water"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "sun": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "calculated",
-      "integration_type": "hub"
+      "iot_class": "calculated"
     },
     "supervisord": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Supervisord",
       "integration_type": "hub",
-      "name": "Supervisord"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "supla": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Supla",
       "integration_type": "hub",
-      "name": "Supla"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "surepetcare": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Sure Petcare",
       "integration_type": "hub",
-      "name": "Sure Petcare"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "swiss_hydrological_data": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Swiss Hydrological Data",
       "integration_type": "hub",
-      "name": "Swiss Hydrological Data"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "swiss_public_transport": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Swiss public transport",
       "integration_type": "hub",
-      "name": "Swiss public transport"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "swisscom": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Swisscom Internet-Box",
       "integration_type": "hub",
-      "name": "Swisscom Internet-Box"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "switchbee": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "SwitchBee",
       "integration_type": "hub",
-      "name": "SwitchBee"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "switchbot": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "SwitchBot",
       "integration_type": "hub",
-      "name": "SwitchBot"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "switcher_kis": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Switcher",
       "integration_type": "hub",
-      "name": "Switcher"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "switchmate": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Switchmate SimplySmart Home",
       "integration_type": "hub",
-      "name": "Switchmate SimplySmart Home"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "syncthing": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Syncthing",
       "integration_type": "hub",
-      "name": "Syncthing"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "synology": {
       "name": "Synology",
@@ -5052,87 +5232,85 @@
       }
     },
     "syslog": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Syslog",
       "integration_type": "hub",
-      "name": "Syslog"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "system_bridge": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "System Bridge",
       "integration_type": "hub",
-      "name": "System Bridge"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "system_log": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "System Log",
       "integration_type": "hub",
-      "name": "System Log"
+      "config_flow": false
     },
     "systemmonitor": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "System Monitor",
       "integration_type": "hub",
-      "name": "System Monitor"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "tado": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Tado",
       "integration_type": "hub",
-      "name": "Tado"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "tag": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "hub"
+      "integration_type": "hub",
+      "config_flow": false
     },
     "tailscale": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Tailscale",
       "integration_type": "hub",
-      "name": "Tailscale"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "tank_utility": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Tank Utility",
       "integration_type": "hub",
-      "name": "Tank Utility"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "tankerkoenig": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Tankerkoenig",
       "integration_type": "hub",
-      "name": "Tankerkoenig"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "tapsaff": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Taps Aff",
       "integration_type": "hub",
-      "name": "Taps Aff"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "tasmota": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Tasmota",
       "integration_type": "hub",
-      "name": "Tasmota"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "tautulli": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Tautulli",
       "integration_type": "hub",
-      "name": "Tautulli"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "tcp": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "TCP",
       "integration_type": "hub",
-      "name": "TCP"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "ted5000": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "The Energy Detective TED5000",
       "integration_type": "hub",
-      "name": "The Energy Detective TED5000"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "telegram": {
       "name": "Telegram",
@@ -5169,28 +5347,28 @@
       }
     },
     "telnet": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Telnet",
       "integration_type": "hub",
-      "name": "Telnet"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "temper": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "TEMPer",
       "integration_type": "hub",
-      "name": "TEMPer"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "template": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Template",
       "integration_type": "hub",
-      "name": "Template"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "tensorflow": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "TensorFlow",
       "integration_type": "hub",
-      "name": "TensorFlow"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "tesla": {
       "name": "Tesla",
@@ -5210,46 +5388,51 @@
       }
     },
     "tfiac": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Tfiac",
       "integration_type": "hub",
-      "name": "Tfiac"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "thermobeacon": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "ThermoBeacon",
       "integration_type": "hub",
-      "name": "ThermoBeacon"
+      "config_flow": true,
+      "iot_class": "local_push"
+    },
+    "thermoplus": {
+      "name": "ThermoPlus",
+      "integration_type": "virtual",
+      "supported_by": "thermobeacon"
     },
     "thermopro": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "ThermoPro",
       "integration_type": "hub",
-      "name": "ThermoPro"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "thermoworks_smoke": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "ThermoWorks Smoke",
       "integration_type": "hub",
-      "name": "ThermoWorks Smoke"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "thethingsnetwork": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "The Things Network",
       "integration_type": "hub",
-      "name": "The Things Network"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "thingspeak": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "ThingSpeak",
       "integration_type": "hub",
-      "name": "ThingSpeak"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "thinkingcleaner": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Thinking Cleaner",
       "integration_type": "hub",
-      "name": "Thinking Cleaner"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "third_reality": {
       "name": "Third Reality",
@@ -5258,124 +5441,124 @@
       ]
     },
     "thomson": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Thomson",
       "integration_type": "hub",
-      "name": "Thomson"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "tibber": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Tibber",
       "integration_type": "hub",
-      "name": "Tibber"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "tikteck": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Tikteck",
       "integration_type": "hub",
-      "name": "Tikteck"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "tile": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Tile",
       "integration_type": "hub",
-      "name": "Tile"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "tilt_ble": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Tilt Hydrometer BLE",
       "integration_type": "hub",
-      "name": "Tilt Hydrometer BLE"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "time_date": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Time & Date",
       "integration_type": "hub",
-      "name": "Time & Date"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "tmb": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Transports Metropolitans de Barcelona",
       "integration_type": "hub",
-      "name": "Transports Metropolitans de Barcelona"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "todoist": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Todoist",
       "integration_type": "hub",
-      "name": "Todoist"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "tolo": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "TOLO Sauna",
       "integration_type": "hub",
-      "name": "TOLO Sauna"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "tomato": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Tomato",
       "integration_type": "hub",
-      "name": "Tomato"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "tomorrowio": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Tomorrow.io",
       "integration_type": "hub",
-      "name": "Tomorrow.io"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "toon": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Toon",
       "integration_type": "hub",
-      "name": "Toon"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "torque": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Torque",
       "integration_type": "hub",
-      "name": "Torque"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "totalconnect": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Total Connect",
       "integration_type": "hub",
-      "name": "Total Connect"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "touchline": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Roth Touchline",
       "integration_type": "hub",
-      "name": "Roth Touchline"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "tplink": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "TP-Link Kasa Smart",
       "integration_type": "hub",
-      "name": "TP-Link Kasa Smart"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "tplink_lte": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "TP-Link LTE",
       "integration_type": "hub",
-      "name": "TP-Link LTE"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "traccar": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Traccar",
       "integration_type": "hub",
-      "name": "Traccar"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "tractive": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Tractive",
       "integration_type": "hub",
-      "name": "Tractive"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "tradfri": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "IKEA TR\u00c5DFRI",
       "integration_type": "hub",
-      "name": "IKEA TR\u00c5DFRI"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "trafikverket": {
       "name": "Trafikverket",
@@ -5401,40 +5584,40 @@
       }
     },
     "transmission": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Transmission",
       "integration_type": "hub",
-      "name": "Transmission"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "transport_nsw": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Transport NSW",
       "integration_type": "hub",
-      "name": "Transport NSW"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "travisci": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Travis-CI",
       "integration_type": "hub",
-      "name": "Travis-CI"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "trend": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Trend",
       "integration_type": "hub",
-      "name": "Trend"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "tuya": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Tuya",
       "integration_type": "hub",
-      "name": "Tuya"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "twentemilieu": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Twente Milieu",
       "integration_type": "hub",
-      "name": "Twente Milieu"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "twilio": {
       "name": "Twilio",
@@ -5460,22 +5643,22 @@
       }
     },
     "twinkly": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Twinkly",
       "integration_type": "hub",
-      "name": "Twinkly"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "twitch": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Twitch",
       "integration_type": "hub",
-      "name": "Twitch"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "twitter": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Twitter",
       "integration_type": "hub",
-      "name": "Twitter"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "u_tec": {
       "name": "U-tec",
@@ -5513,159 +5696,164 @@
       }
     },
     "uk_transport": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "UK Transport",
       "integration_type": "hub",
-      "name": "UK Transport"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "ukraine_alarm": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Ukraine Alarm",
       "integration_type": "hub",
-      "name": "Ukraine Alarm"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "universal": {
-      "config_flow": false,
-      "iot_class": "calculated",
+      "name": "Universal Media Player",
       "integration_type": "hub",
-      "name": "Universal Media Player"
+      "config_flow": false,
+      "iot_class": "calculated"
     },
     "upb": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Universal Powerline Bus (UPB)",
       "integration_type": "hub",
-      "name": "Universal Powerline Bus (UPB)"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "upc_connect": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "UPC Connect Box",
       "integration_type": "hub",
-      "name": "UPC Connect Box"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "upcloud": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "UpCloud",
       "integration_type": "hub",
-      "name": "UpCloud"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "upnp": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "UPnP/IGD",
       "integration_type": "hub",
-      "name": "UPnP/IGD"
+      "config_flow": true,
+      "iot_class": "local_polling"
+    },
+    "uprise_smart_shades": {
+      "name": "Uprise Smart Shades",
+      "integration_type": "virtual",
+      "supported_by": "motion_blinds"
     },
     "uptime": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "hub"
+      "iot_class": "local_push"
     },
     "uptimerobot": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "UptimeRobot",
       "integration_type": "hub",
-      "name": "UptimeRobot"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "usgs_earthquakes_feed": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "U.S. Geological Survey Earthquake Hazards (USGS)",
       "integration_type": "hub",
-      "name": "U.S. Geological Survey Earthquake Hazards (USGS)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "uvc": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Ubiquiti UniFi Video",
       "integration_type": "hub",
-      "name": "Ubiquiti UniFi Video"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "vallox": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Vallox",
       "integration_type": "hub",
-      "name": "Vallox"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "vasttrafik": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "V\u00e4sttrafik",
       "integration_type": "hub",
-      "name": "V\u00e4sttrafik"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "velbus": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Velbus",
       "integration_type": "hub",
-      "name": "Velbus"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "velux": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Velux",
       "integration_type": "hub",
-      "name": "Velux"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "venstar": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Venstar",
       "integration_type": "hub",
-      "name": "Venstar"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "vera": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Vera",
       "integration_type": "hub",
-      "name": "Vera"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "verisure": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Verisure",
       "integration_type": "hub",
-      "name": "Verisure"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "versasense": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "VersaSense",
       "integration_type": "hub",
-      "name": "VersaSense"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "version": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Version",
       "integration_type": "hub",
-      "name": "Version"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "vesync": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "VeSync",
       "integration_type": "hub",
-      "name": "VeSync"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "viaggiatreno": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Trenitalia ViaggiaTreno",
       "integration_type": "hub",
-      "name": "Trenitalia ViaggiaTreno"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "vicare": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Viessmann ViCare",
       "integration_type": "hub",
-      "name": "Viessmann ViCare"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "vilfo": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Vilfo Router",
       "integration_type": "hub",
-      "name": "Vilfo Router"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "vivotek": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "VIVOTEK",
       "integration_type": "hub",
-      "name": "VIVOTEK"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "vizio": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "VIZIO SmartCast",
       "integration_type": "hub",
-      "name": "VIZIO SmartCast"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "vlc": {
       "name": "VideoLAN",
@@ -5685,195 +5873,194 @@
       }
     },
     "voicerss": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "VoiceRSS",
       "integration_type": "hub",
-      "name": "VoiceRSS"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "volkszaehler": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Volkszaehler",
       "integration_type": "hub",
-      "name": "Volkszaehler"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "volumio": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Volumio",
       "integration_type": "hub",
-      "name": "Volumio"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "volvooncall": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Volvo On Call",
       "integration_type": "hub",
-      "name": "Volvo On Call"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "vulcan": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Uonet+ Vulcan",
       "integration_type": "hub",
-      "name": "Uonet+ Vulcan"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "vultr": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Vultr",
       "integration_type": "hub",
-      "name": "Vultr"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "w800rf32": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "WGL Designs W800RF32",
       "integration_type": "hub",
-      "name": "WGL Designs W800RF32"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "wake_on_lan": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Wake on LAN",
       "integration_type": "hub",
-      "name": "Wake on LAN"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "wallbox": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Wallbox",
       "integration_type": "hub",
-      "name": "Wallbox"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "waqi": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "World Air Quality Index (WAQI)",
       "integration_type": "hub",
-      "name": "World Air Quality Index (WAQI)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "waterfurnace": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "WaterFurnace",
       "integration_type": "hub",
-      "name": "WaterFurnace"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "watttime": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "WattTime",
       "integration_type": "hub",
-      "name": "WattTime"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "waze_travel_time": {
+      "integration_type": "hub",
       "config_flow": true,
-      "iot_class": "cloud_polling",
-      "integration_type": "hub"
+      "iot_class": "cloud_polling"
     },
     "webhook": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Webhook",
       "integration_type": "hub",
-      "name": "Webhook"
+      "config_flow": false
     },
     "wemo": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Belkin WeMo",
       "integration_type": "hub",
-      "name": "Belkin WeMo"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "whirlpool": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "Whirlpool Sixth Sense",
       "integration_type": "hub",
-      "name": "Whirlpool Sixth Sense"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "whois": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Whois",
       "integration_type": "hub",
-      "name": "Whois"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "wiffi": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Wiffi",
       "integration_type": "hub",
-      "name": "Wiffi"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "wilight": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "WiLight",
       "integration_type": "hub",
-      "name": "WiLight"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "wirelesstag": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Wireless Sensor Tags",
       "integration_type": "hub",
-      "name": "Wireless Sensor Tags"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "withings": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Withings",
       "integration_type": "hub",
-      "name": "Withings"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "wiz": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "WiZ",
       "integration_type": "hub",
-      "name": "WiZ"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "wled": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "WLED",
       "integration_type": "hub",
-      "name": "WLED"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "wolflink": {
-      "config_flow": true,
-      "iot_class": "cloud_polling",
+      "name": "Wolf SmartSet Service",
       "integration_type": "hub",
-      "name": "Wolf SmartSet Service"
+      "config_flow": true,
+      "iot_class": "cloud_polling"
     },
     "workday": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Workday",
       "integration_type": "hub",
-      "name": "Workday"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "worldclock": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "Worldclock",
       "integration_type": "hub",
-      "name": "Worldclock"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "worldtidesinfo": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "World Tides",
       "integration_type": "hub",
-      "name": "World Tides"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "worxlandroid": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Worx Landroid",
       "integration_type": "hub",
-      "name": "Worx Landroid"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "ws66i": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Soundavo WS66i 6-Zone Amplifier",
       "integration_type": "hub",
-      "name": "Soundavo WS66i 6-Zone Amplifier"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "wsdot": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Washington State Department of Transportation (WSDOT)",
       "integration_type": "hub",
-      "name": "Washington State Department of Transportation (WSDOT)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "x10": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Heyu X10",
       "integration_type": "hub",
-      "name": "Heyu X10"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "xeoma": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Xeoma",
       "integration_type": "hub",
-      "name": "Xeoma"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "xiaomi": {
       "name": "Xiaomi",
@@ -5911,16 +6098,16 @@
       }
     },
     "xmpp": {
-      "config_flow": false,
-      "iot_class": "cloud_push",
+      "name": "Jabber (XMPP)",
       "integration_type": "hub",
-      "name": "Jabber (XMPP)"
+      "config_flow": false,
+      "iot_class": "cloud_push"
     },
     "xs1": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "EZcontrol XS1",
       "integration_type": "hub",
-      "name": "EZcontrol XS1"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "yale": {
       "name": "Yale",
@@ -5946,16 +6133,16 @@
       }
     },
     "yamaha": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Yamaha Network Receivers",
       "integration_type": "hub",
-      "name": "Yamaha Network Receivers"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "yamaha_musiccast": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "MusicCast",
       "integration_type": "hub",
-      "name": "MusicCast"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "yandex": {
       "name": "Yandex",
@@ -5992,88 +6179,87 @@
       }
     },
     "yi": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Yi Home Cameras",
       "integration_type": "hub",
-      "name": "Yi Home Cameras"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "yolink": {
-      "config_flow": true,
-      "iot_class": "cloud_push",
+      "name": "YoLink",
       "integration_type": "hub",
-      "name": "YoLink"
+      "config_flow": true,
+      "iot_class": "cloud_push"
     },
     "youless": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "YouLess",
       "integration_type": "hub",
-      "name": "YouLess"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "zabbix": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Zabbix",
       "integration_type": "hub",
-      "name": "Zabbix"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "zamg": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Zentralanstalt f\u00fcr Meteorologie und Geodynamik (ZAMG)",
       "integration_type": "hub",
-      "name": "Zentralanstalt f\u00fcr Meteorologie und Geodynamik (ZAMG)"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "zengge": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Zengge",
       "integration_type": "hub",
-      "name": "Zengge"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "zerproc": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Zerproc",
       "integration_type": "hub",
-      "name": "Zerproc"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "zestimate": {
-      "config_flow": false,
-      "iot_class": "cloud_polling",
+      "name": "Zestimate",
       "integration_type": "hub",
-      "name": "Zestimate"
+      "config_flow": false,
+      "iot_class": "cloud_polling"
     },
     "zha": {
-      "config_flow": true,
-      "iot_class": "local_polling",
+      "name": "Zigbee Home Automation",
       "integration_type": "hub",
-      "name": "Zigbee Home Automation"
+      "config_flow": true,
+      "iot_class": "local_polling"
     },
     "zhong_hong": {
-      "config_flow": false,
-      "iot_class": "local_push",
+      "name": "ZhongHong",
       "integration_type": "hub",
-      "name": "ZhongHong"
+      "config_flow": false,
+      "iot_class": "local_push"
     },
     "ziggo_mediabox_xl": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Ziggo Mediabox XL",
       "integration_type": "hub",
-      "name": "Ziggo Mediabox XL"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "zodiac": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "Zodiac",
       "integration_type": "hub",
-      "name": "Zodiac"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "zone": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Zone",
       "integration_type": "hub",
-      "name": "Zone"
+      "config_flow": false
     },
     "zoneminder": {
-      "config_flow": false,
-      "iot_class": "local_polling",
+      "name": "ZoneMinder",
       "integration_type": "hub",
-      "name": "ZoneMinder"
+      "config_flow": false,
+      "iot_class": "local_polling"
     },
     "zooz": {
       "name": "Zooz",
@@ -6082,107 +6268,98 @@
       ]
     },
     "zwave_js": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Z-Wave",
       "integration_type": "hub",
-      "name": "Z-Wave"
+      "config_flow": true,
+      "iot_class": "local_push"
     },
     "zwave_me": {
-      "config_flow": true,
-      "iot_class": "local_push",
+      "name": "Z-Wave.Me",
       "integration_type": "hub",
-      "name": "Z-Wave.Me"
+      "config_flow": true,
+      "iot_class": "local_push"
     }
   },
   "hardware": {},
   "helper": {
     "counter": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Counter",
       "integration_type": "helper",
-      "name": "Counter"
+      "config_flow": false
     },
     "derivative": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "calculated",
-      "integration_type": "helper"
+      "iot_class": "calculated"
     },
     "group": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "calculated",
-      "integration_type": "helper"
+      "iot_class": "calculated"
     },
     "input_boolean": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "helper"
+      "integration_type": "helper",
+      "config_flow": false
     },
     "input_button": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Input Button",
       "integration_type": "helper",
-      "name": "Input Button"
+      "config_flow": false
     },
     "input_datetime": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "helper"
+      "integration_type": "helper",
+      "config_flow": false
     },
     "input_number": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "helper"
+      "integration_type": "helper",
+      "config_flow": false
     },
     "input_select": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "helper"
+      "integration_type": "helper",
+      "config_flow": false
     },
     "input_text": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "helper"
+      "integration_type": "helper",
+      "config_flow": false
     },
     "integration": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "helper"
+      "iot_class": "local_push"
     },
     "min_max": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "helper"
+      "iot_class": "local_push"
     },
     "schedule": {
-      "config_flow": false,
-      "iot_class": null,
-      "integration_type": "helper"
+      "integration_type": "helper",
+      "config_flow": false
     },
     "switch_as_x": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "calculated",
-      "integration_type": "helper"
+      "iot_class": "calculated"
     },
     "threshold": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "local_polling",
-      "integration_type": "helper"
+      "iot_class": "local_polling"
     },
     "timer": {
-      "config_flow": false,
-      "iot_class": null,
+      "name": "Timer",
       "integration_type": "helper",
-      "name": "Timer"
+      "config_flow": false
     },
     "tod": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "helper"
+      "iot_class": "local_push"
     },
     "utility_meter": {
+      "integration_type": "helper",
       "config_flow": true,
-      "iot_class": "local_push",
-      "integration_type": "helper"
+      "iot_class": "local_push"
     }
   },
   "translated_name": [
diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py
deleted file mode 100644
index 30a0601861d..00000000000
--- a/homeassistant/generated/supported_brands.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Automatically generated by hassfest.
-
-To update, run python3 -m script.hassfest
-"""
-
-HAS_SUPPORTED_BRANDS = [
-    "denonavr",
-    "gree",
-    "hunterdouglas_powerview",
-    "inkbird",
-    "motion_blinds",
-    "netatmo",
-    "overkiz",
-    "renault",
-    "switchbee",
-    "thermobeacon",
-    "upb",
-    "wemo",
-    "xiaomi_miio",
-    "yalexs_ble",
-]
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index 45028eef27f..a2fc2594b61 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -152,7 +152,6 @@ class Manifest(TypedDict, total=False):
     version: str
     codeowners: list[str]
     loggers: list[str]
-    supported_brands: dict[str, str]
 
 
 def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest:
diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py
index 7fb6ad2d8d0..27252969118 100644
--- a/script/hassfest/__main__.py
+++ b/script/hassfest/__main__.py
@@ -20,7 +20,6 @@ from . import (
     requirements,
     services,
     ssdp,
-    supported_brands,
     translations,
     usb,
     zeroconf,
@@ -39,7 +38,6 @@ INTEGRATION_PLUGINS = [
     requirements,
     services,
     ssdp,
-    supported_brands,
     translations,
     usb,
     zeroconf,
diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py
index 5511bc8a518..43695a39ad4 100644
--- a/script/hassfest/codeowners.py
+++ b/script/hassfest/codeowners.py
@@ -47,7 +47,10 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config):
     for domain in sorted(integrations):
         integration = integrations[domain]
 
-        if not integration.manifest:
+        if (
+            not integration.manifest
+            or integration.manifest.get("integration_type") == "virtual"
+        ):
             continue
 
         codeowners = integration.manifest["codeowners"]
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index 6905cd60d49..3fcf946bc32 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -171,14 +171,24 @@ def _generate_integrations(
             integration = integrations[domain]
             if integration.integration_type in ("entity", "system"):
                 continue
-            metadata["config_flow"] = integration.config_flow
-            metadata["iot_class"] = integration.iot_class
-            metadata["integration_type"] = integration.integration_type
+
             if integration.translated_name:
                 result["translated_name"].add(domain)
             else:
                 metadata["name"] = integration.name
 
+            metadata["integration_type"] = integration.integration_type
+
+            if integration.integration_type == "virtual":
+                if integration.supported_by:
+                    metadata["supported_by"] = integration.supported_by
+                if integration.iot_standard:
+                    metadata["iot_standard"] = integration.iot_standard
+            else:
+                metadata["config_flow"] = integration.config_flow
+                if integration.iot_class:
+                    metadata["iot_class"] = integration.iot_class
+
             if integration.integration_type == "helper":
                 result["helper"][domain] = metadata
             else:
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index 616faeb995e..1df017da22d 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 from pathlib import Path
+from typing import Any
 from urllib.parse import urlparse
 
 from awesomeversion import (
@@ -158,7 +159,7 @@ def verify_wildcard(value: str):
     return value
 
 
-MANIFEST_SCHEMA = vol.Schema(
+INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
     {
         vol.Required("domain"): str,
         vol.Required("name"): str,
@@ -254,14 +255,32 @@ MANIFEST_SCHEMA = vol.Schema(
         vol.Optional("loggers"): [str],
         vol.Optional("disabled"): str,
         vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES),
-        vol.Optional("supported_brands"): vol.Schema({str: str}),
     }
 )
 
-CUSTOM_INTEGRATION_MANIFEST_SCHEMA = MANIFEST_SCHEMA.extend(
+VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
+    {
+        vol.Required("domain"): str,
+        vol.Required("name"): str,
+        vol.Required("integration_type"): "virtual",
+        vol.Exclusive("iot_standard", "virtual_integration"): vol.Any(
+            "homekit", "zigbee", "zwave"
+        ),
+        vol.Exclusive("supported_by", "virtual_integration"): str,
+    }
+)
+
+
+def manifest_schema(value: dict[str, Any]) -> vol.Schema:
+    """Validate integration manifest."""
+    if value.get("integration_type") == "virtual":
+        return VIRTUAL_INTEGRATION_MANIFEST_SCHEMA(value)
+    return INTEGRATION_MANIFEST_SCHEMA(value)
+
+
+CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend(
     {
         vol.Optional("version"): vol.All(str, verify_version),
-        vol.Remove("supported_brands"): dict,
     }
 )
 
@@ -284,7 +303,7 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No
 
     try:
         if integration.core:
-            MANIFEST_SCHEMA(integration.manifest)
+            manifest_schema(integration.manifest)
         else:
             CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
     except vol.Invalid as err:
@@ -312,15 +331,19 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No
     if (
         integration.manifest["domain"] not in NO_IOT_CLASS
         and "iot_class" not in integration.manifest
+        and integration.manifest.get("integration_type") != "virtual"
     ):
         integration.add_error("manifest", "Domain is missing an IoT Class")
 
-    for domain, _name in integration.manifest.get("supported_brands", {}).items():
-        if (core_components_dir / domain).exists():
-            integration.add_warning(
-                "manifest",
-                f"Supported brand domain {domain} collides with built-in core integration",
-            )
+    if (
+        integration.manifest.get("integration_type") == "virtual"
+        and (supported_by := integration.manifest.get("supported_by"))
+        and not (core_components_dir / supported_by).exists()
+    ):
+        integration.add_error(
+            "manifest",
+            "Virtual integration points to non-existing supported_by integration",
+        )
 
     if not integration.core:
         validate_version(integration)
diff --git a/script/hassfest/model.py b/script/hassfest/model.py
index b2459f931c6..61004bae006 100644
--- a/script/hassfest/model.py
+++ b/script/hassfest/model.py
@@ -109,11 +109,12 @@ class Integration:
                 continue
 
             init = fil / "__init__.py"
-            if not init.exists():
+            manifest = fil / "manifest.json"
+            if not init.exists() and not manifest.exists():
                 print(
-                    f"Warning: {init} missing, skipping directory. "
-                    "If this is your development environment, "
-                    "you can safely delete this folder."
+                    f"Warning: {init} and manifest.json missing, "
+                    "skipping directory. If this is your development "
+                    "environment, you can safely delete this folder."
                 )
                 continue
 
@@ -170,9 +171,9 @@ class Integration:
         return self.manifest.get("dependencies", [])
 
     @property
-    def supported_brands(self) -> dict[str]:
-        """Return dict of supported brands."""
-        return self.manifest.get("supported_brands", {})
+    def supported_by(self) -> str:
+        """Return the integration supported by this virtual integration."""
+        return self.manifest.get("supported_by", {})
 
     @property
     def integration_type(self) -> str:
@@ -184,6 +185,11 @@ class Integration:
         """Return the integration IoT Class."""
         return self.manifest.get("iot_class")
 
+    @property
+    def iot_standard(self) -> str:
+        """Return the IoT standard supported by this virtual integration."""
+        return self.manifest.get("iot_standard", {})
+
     def add_error(self, *args: Any, **kwargs: Any) -> None:
         """Add an error."""
         self.errors.append(Error(*args, **kwargs))
diff --git a/script/hassfest/supported_brands.py b/script/hassfest/supported_brands.py
deleted file mode 100644
index 4ac2feb4032..00000000000
--- a/script/hassfest/supported_brands.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""Generate supported_brands data."""
-from __future__ import annotations
-
-import black
-
-from .model import Config, Integration
-from .serializer import to_string
-
-BASE = """
-\"\"\"Automatically generated by hassfest.
-
-To update, run python3 -m script.hassfest
-\"\"\"
-
-HAS_SUPPORTED_BRANDS = {}
-""".strip()
-
-
-def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str:
-    """Validate and generate supported_brands data."""
-
-    brands = [
-        domain
-        for domain, integration in sorted(integrations.items())
-        if integration.supported_brands
-    ]
-
-    return black.format_str(BASE.format(to_string(brands)), mode=black.Mode())
-
-
-def validate(integrations: dict[str, Integration], config: Config) -> None:
-    """Validate supported_brands data."""
-    supported_brands_path = config.root / "homeassistant/generated/supported_brands.py"
-    config.cache["supported_brands"] = content = generate_and_validate(
-        integrations, config
-    )
-
-    if config.specific_integrations:
-        return
-
-    if supported_brands_path.read_text(encoding="utf-8") != content:
-        config.add_error(
-            "supported_brands",
-            "File supported_brands.py is not up to date. Run python3 -m script.hassfest",
-            fixable=True,
-        )
-
-
-def generate(integrations: dict[str, Integration], config: Config):
-    """Generate supported_brands data."""
-    supported_brands_path = config.root / "homeassistant/generated/supported_brands.py"
-    supported_brands_path.write_text(
-        f"{config.cache['supported_brands']}", encoding="utf-8"
-    )
diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py
index 135882d9aed..a865776f74d 100644
--- a/tests/components/websocket_api/test_commands.py
+++ b/tests/components/websocket_api/test_commands.py
@@ -23,13 +23,7 @@ from homeassistant.helpers.json import json_loads
 from homeassistant.loader import async_get_integration
 from homeassistant.setup import DATA_SETUP_TIME, async_setup_component
 
-from tests.common import (
-    MockEntity,
-    MockEntityPlatform,
-    MockModule,
-    async_mock_service,
-    mock_integration,
-)
+from tests.common import MockEntity, MockEntityPlatform, async_mock_service
 
 STATE_KEY_SHORT_NAMES = {
     "entity_id": "e",
@@ -1794,45 +1788,6 @@ async def test_validate_config_invalid(websocket_client, key, config, error):
     assert msg["result"] == {key: {"valid": False, "error": error}}
 
 
-async def test_supported_brands(hass, websocket_client):
-    """Test supported brands."""
-    # Custom components without supported brands that override a built-in component with
-    # supported brand will still be listed in HAS_SUPPORTED_BRANDS and should be ignored.
-    mock_integration(
-        hass,
-        MockModule("override_without_brands"),
-    )
-    mock_integration(
-        hass,
-        MockModule("test", partial_manifest={"supported_brands": {"hello": "World"}}),
-    )
-    mock_integration(
-        hass,
-        MockModule(
-            "abcd", partial_manifest={"supported_brands": {"something": "Something"}}
-        ),
-    )
-
-    with patch(
-        "homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS",
-        ("abcd", "test", "override_without_brands"),
-    ):
-        await websocket_client.send_json({"id": 7, "type": "supported_brands"})
-        msg = await websocket_client.receive_json()
-
-    assert msg["id"] == 7
-    assert msg["type"] == const.TYPE_RESULT
-    assert msg["success"]
-    assert msg["result"] == {
-        "abcd": {
-            "something": "Something",
-        },
-        "test": {
-            "hello": "World",
-        },
-    }
-
-
 async def test_message_coalescing(hass, websocket_client, hass_admin_user):
     """Test enabling message coalescing."""
     await websocket_client.send_json(
-- 
GitLab


From bce273660d3768a80e9e4abd876d630ddde1c65d Mon Sep 17 00:00:00 2001
From: Dusan Cervenka <Cervenka.dusan@gmail.com>
Date: Fri, 21 Oct 2022 08:26:14 +0200
Subject: [PATCH 651/985] Fix nextcloud 'ncm' referenced before assignment
 (#80711)

* #80673 Fix 'ncm' referenced before assignmen

 UnboundLocalError: local variable 'ncm' referenced before assignmen

Signed-off-by: Dusan Cervenka <cervenka.dusan@gmail.com>

* #80673 changes based on review

Signed-off-by: Dusan Cervenka <cervenka.dusan@gmail.com>

* Changes made based on review

Signed-off-by: Dusan Cervenka <cervenka.dusan@gmail.com>

Signed-off-by: Dusan Cervenka <cervenka.dusan@gmail.com>
---
 homeassistant/components/nextcloud/__init__.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py
index 6ca6096458e..269bd96aa31 100644
--- a/homeassistant/components/nextcloud/__init__.py
+++ b/homeassistant/components/nextcloud/__init__.py
@@ -100,6 +100,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
         ncm = NextcloudMonitor(conf[CONF_URL], conf[CONF_USERNAME], conf[CONF_PASSWORD])
     except NextcloudMonitorError:
         _LOGGER.error("Nextcloud setup failed - Check configuration")
+        return False
 
     hass.data[DOMAIN] = get_data_points(ncm.data)
     hass.data[DOMAIN]["instance"] = conf[CONF_URL]
-- 
GitLab


From 9cd2b834223482cbe42b6b14f9e595cc59f6c28b Mon Sep 17 00:00:00 2001
From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com>
Date: Fri, 21 Oct 2022 10:04:16 +0200
Subject: [PATCH 652/985] Add entity_registry_enabled_default and missing
 EntityCategories in Plugwise (#80629)

---
 .../components/plugwise/binary_sensor.py      |  7 +++++
 homeassistant/components/plugwise/sensor.py   | 31 +++++++++++++++++++
 2 files changed, 38 insertions(+)

diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py
index 6f751b82b35..164135a607b 100644
--- a/homeassistant/components/plugwise/binary_sensor.py
+++ b/homeassistant/components/plugwise/binary_sensor.py
@@ -29,6 +29,13 @@ class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription):
 
 
 BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
+    PlugwiseBinarySensorEntityDescription(
+        key="compressor_state",
+        name="Compressor state",
+        icon="mdi:hvac",
+        icon_off="mdi:hvac-off",
+        entity_category=EntityCategory.DIAGNOSTIC,
+    ),
     PlugwiseBinarySensorEntityDescription(
         key="dhw_state",
         name="DHW state",
diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py
index 0e65f617c25..b6e2a3aa413 100644
--- a/homeassistant/components/plugwise/sensor.py
+++ b/homeassistant/components/plugwise/sensor.py
@@ -18,6 +18,7 @@ from homeassistant.const import (
     VOLUME_CUBIC_METERS,
 )
 from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import DOMAIN, UNIT_LUMEN
@@ -31,12 +32,30 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
         state_class=SensorStateClass.MEASUREMENT,
+        entity_category=EntityCategory.DIAGNOSTIC,
+    ),
+    SensorEntityDescription(
+        key="setpoint_high",
+        name="Cooling setpoint",
+        native_unit_of_measurement=TEMP_CELSIUS,
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        entity_category=EntityCategory.DIAGNOSTIC,
+    ),
+    SensorEntityDescription(
+        key="setpoint_low",
+        name="Heating setpoint",
+        native_unit_of_measurement=TEMP_CELSIUS,
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        entity_category=EntityCategory.DIAGNOSTIC,
     ),
     SensorEntityDescription(
         key="temperature",
         name="Temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -44,6 +63,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Intended boiler temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -51,6 +71,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Temperature difference",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -65,6 +86,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Outdoor air temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -72,6 +94,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Water temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -79,6 +102,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Return temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -94,6 +118,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         native_unit_of_measurement=POWER_WATT,
         device_class=SensorDeviceClass.POWER,
         state_class=SensorStateClass.MEASUREMENT,
+        entity_registry_enabled_default=False,
     ),
     SensorEntityDescription(
         key="electricity_consumed_interval",
@@ -122,6 +147,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         native_unit_of_measurement=ENERGY_WATT_HOUR,
         device_class=SensorDeviceClass.ENERGY,
         state_class=SensorStateClass.TOTAL,
+        entity_registry_enabled_default=False,
     ),
     SensorEntityDescription(
         key="electricity_produced_peak_interval",
@@ -226,6 +252,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Battery",
         native_unit_of_measurement=PERCENTAGE,
         device_class=SensorDeviceClass.BATTERY,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -240,12 +267,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Modulation level",
         icon="mdi:percent",
         native_unit_of_measurement=PERCENTAGE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
         key="valve_position",
         name="Valve position",
         icon="mdi:valve",
+        entity_category=EntityCategory.DIAGNOSTIC,
         native_unit_of_measurement=PERCENTAGE,
         state_class=SensorStateClass.MEASUREMENT,
     ),
@@ -254,6 +283,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="Water pressure",
         native_unit_of_measurement=PRESSURE_BAR,
         device_class=SensorDeviceClass.PRESSURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
@@ -268,6 +298,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
         name="DHW temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
+        entity_category=EntityCategory.DIAGNOSTIC,
         state_class=SensorStateClass.MEASUREMENT,
     ),
 )
-- 
GitLab


From 4abe5aec6cffa174065c4f8865f091aed871eb19 Mon Sep 17 00:00:00 2001
From: Ryan Miguel <1818590+renegaderyu@users.noreply.github.com>
Date: Fri, 21 Oct 2022 01:07:45 -0700
Subject: [PATCH 653/985] Fix ZeroDivisionError for Fritz!Smarthome electric
 current sensor (#80682)

Fixes ZeroDivisionError for fritzbox sensor. Fixes #80618.
---
 homeassistant/components/fritzbox/sensor.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py
index 7253fdcf36e..ab341fb1520 100644
--- a/homeassistant/components/fritzbox/sensor.py
+++ b/homeassistant/components/fritzbox/sensor.py
@@ -76,7 +76,11 @@ def suitable_temperature(device: FritzhomeDevice) -> bool:
 
 def value_electric_current(device: FritzhomeDevice) -> float:
     """Return native value for electric current sensor."""
-    if isinstance(device.power, int) and isinstance(device.voltage, int):
+    if (
+        isinstance(device.power, int)
+        and isinstance(device.voltage, int)
+        and device.voltage > 0
+    ):
         return round(device.power / device.voltage, 3)
     return 0.0
 
-- 
GitLab


From d1d218444b91dd860072898391aef366de7e7169 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Fri, 21 Oct 2022 01:56:17 -0700
Subject: [PATCH 654/985] Improve calendar trigger test quality (#79451)

Improve calendar test quality
---
 tests/components/calendar/test_trigger.py | 93 +++++++++++++++++++----
 1 file changed, 77 insertions(+), 16 deletions(-)

diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py
index ebe5e9185e4..98a21db8ef7 100644
--- a/tests/components/calendar/test_trigger.py
+++ b/tests/components/calendar/test_trigger.py
@@ -62,14 +62,15 @@ class FakeSchedule:
         self,
         start: datetime.timedelta,
         end: datetime.timedelta,
-        description: str = None,
-        location: str = None,
+        summary: str | None = None,
+        description: str | None = None,
+        location: str | None = None,
     ) -> dict[str, Any]:
         """Create a new fake event, used by tests."""
         event = calendar.CalendarEvent(
             start=start,
             end=end,
-            summary=f"Event {secrets.token_hex(16)}",  # Arbitrary unique data
+            summary=summary if summary else f"Event {secrets.token_hex(16)}",
             description=description,
             location=location,
         )
@@ -85,8 +86,13 @@ class FakeSchedule:
         """Get all events in a specific time frame, used by the demo calendar."""
         assert start_date < end_date
         values = []
+        local_start_date = dt_util.as_local(start_date)
+        local_end_date = dt_util.as_local(end_date)
         for event in self.events:
-            if start_date < event.start < end_date or start_date < event.end < end_date:
+            if (
+                event.start_datetime_local < local_end_date
+                and local_start_date < event.end_datetime_local
+            ):
                 values.append(event)
         return values
 
@@ -104,6 +110,14 @@ class FakeSchedule:
             await self.fire_time(dt_util.utcnow())
 
 
+@pytest.fixture
+def set_time_zone(hass):
+    """Set the time zone for the tests."""
+    # Set our timezone to CST/Regina so we can check calculations
+    # This keeps UTC-6 all year round
+    hass.config.set_time_zone("America/Regina")
+
+
 @pytest.fixture
 def fake_schedule(hass, freezer):
     """Fixture that tests can use to make fake events."""
@@ -534,25 +548,72 @@ async def test_update_missed(hass, calls, fake_schedule):
     ]
 
 
-async def test_event_payload(hass, calls, fake_schedule):
-    """Test the a calendar trigger based on start time."""
-    event_data = fake_schedule.create_event(
-        start=datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"),
-        end=datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00"),
-        description="Description",
-        location="Location",
-    )
+@pytest.mark.parametrize(
+    "create_data,fire_time,payload_data",
+    [
+        (
+            {
+                "start": datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"),
+                "end": datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00"),
+                "summary": "Summary",
+            },
+            datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00"),
+            {
+                "summary": "Summary",
+                "start": "2022-04-19T11:00:00+00:00",
+                "end": "2022-04-19T11:30:00+00:00",
+                "all_day": False,
+            },
+        ),
+        (
+            {
+                "start": datetime.datetime.fromisoformat("2022-04-19 11:00:00+00:00"),
+                "end": datetime.datetime.fromisoformat("2022-04-19 11:30:00+00:00"),
+                "summary": "Summary",
+                "description": "Description",
+                "location": "Location",
+            },
+            datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00"),
+            {
+                "summary": "Summary",
+                "start": "2022-04-19T11:00:00+00:00",
+                "end": "2022-04-19T11:30:00+00:00",
+                "all_day": False,
+                "description": "Description",
+                "location": "Location",
+            },
+        ),
+        (
+            {
+                "summary": "Summary",
+                "start": datetime.date.fromisoformat("2022-04-20"),
+                "end": datetime.date.fromisoformat("2022-04-21"),
+            },
+            datetime.datetime.fromisoformat("2022-04-20 00:00:01-06:00"),
+            {
+                "summary": "Summary",
+                "start": "2022-04-20",
+                "end": "2022-04-21",
+                "all_day": True,
+            },
+        ),
+    ],
+    ids=["basic", "more-fields", "all-day"],
+)
+async def test_event_payload(
+    hass, calls, fake_schedule, set_time_zone, create_data, fire_time, payload_data
+):
+    """Test the fields in the calendar event payload are set."""
+    fake_schedule.create_event(**create_data)
     await create_automation(hass, EVENT_START)
     assert len(calls()) == 0
 
-    await fake_schedule.fire_until(
-        datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00")
-    )
+    await fake_schedule.fire_until(fire_time)
     assert calls() == [
         {
             "platform": "calendar",
             "event": EVENT_START,
-            "calendar_event": event_data,
+            "calendar_event": payload_data,
         }
     ]
 
-- 
GitLab


From 9318d833a43cf45c1f96672bd8838dc892cfa10b Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Fri, 21 Oct 2022 02:03:25 -0700
Subject: [PATCH 655/985] Streamline calendar dataclass API/attribute
 conversions (#79598)

* Streamline calendar dataclass API/attribute conversions

* Fix attribute conversions
---
 homeassistant/components/calendar/__init__.py | 49 ++++++++++++-------
 1 file changed, 31 insertions(+), 18 deletions(-)

diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py
index 213376b2081..cfc09df667a 100644
--- a/homeassistant/components/calendar/__init__.py
+++ b/homeassistant/components/calendar/__init__.py
@@ -1,7 +1,8 @@
 """Support for Google Calendar event device sensors."""
 from __future__ import annotations
 
-from dataclasses import dataclass
+from collections.abc import Iterable
+import dataclasses
 import datetime
 from http import HTTPStatus
 import logging
@@ -77,7 +78,7 @@ def get_date(date: dict[str, Any]) -> datetime.datetime:
     return dt.as_local(parsed_datetime)
 
 
-@dataclass
+@dataclasses.dataclass
 class CalendarEvent:
     """An event on a calendar."""
 
@@ -104,17 +105,34 @@ class CalendarEvent:
 
     def as_dict(self) -> dict[str, Any]:
         """Return a dict representation of the event."""
-        data = {
-            "start": self.start.isoformat(),
-            "end": self.end.isoformat(),
-            "summary": self.summary,
+        return {
+            **dataclasses.asdict(self, dict_factory=_event_dict_factory),
             "all_day": self.all_day,
         }
-        if self.description:
-            data["description"] = self.description
-        if self.location:
-            data["location"] = self.location
-        return data
+
+
+def _event_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
+    """Convert CalendarEvent dataclass items to dictionary of attributes."""
+    result: dict[str, str] = {}
+    for name, value in obj:
+        if isinstance(value, (datetime.datetime, datetime.date)):
+            result[name] = value.isoformat()
+        elif value is not None:
+            result[name] = str(value)
+    return result
+
+
+def _api_event_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, Any]:
+    """Convert CalendarEvent dataclass items to the API format."""
+    result: dict[str, Any] = {}
+    for name, value in obj:
+        if isinstance(value, datetime.datetime):
+            result[name] = {"dateTime": dt.as_local(value).isoformat()}
+        elif isinstance(value, datetime.date):
+            result[name] = {"date": value.isoformat()}
+        else:
+            result[name] = value
+    return result
 
 
 def _get_datetime_local(
@@ -250,15 +268,10 @@ class CalendarEventView(http.HomeAssistantView):
             return self.json_message(
                 f"Error reading events: {err}", HTTPStatus.INTERNAL_SERVER_ERROR
             )
+
         return self.json(
             [
-                {
-                    "summary": event.summary,
-                    "description": event.description,
-                    "location": event.location,
-                    "start": _get_api_date(event.start),
-                    "end": _get_api_date(event.end),
-                }
+                dataclasses.asdict(event, dict_factory=_api_event_dict_factory)
                 for event in calendar_event_list
             ]
         )
-- 
GitLab


From 78f71c46da41e58e122d4818726ea6c168643345 Mon Sep 17 00:00:00 2001
From: Guido Schmitz <Shutgun@users.noreply.github.com>
Date: Fri, 21 Oct 2022 11:25:35 +0200
Subject: [PATCH 656/985] Add iot_standards to devolo brand (#80332)

---
 homeassistant/brands/devolo.json          | 3 ++-
 homeassistant/generated/integrations.json | 5 ++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/homeassistant/brands/devolo.json b/homeassistant/brands/devolo.json
index 86dc7a3b100..021c62b930b 100644
--- a/homeassistant/brands/devolo.json
+++ b/homeassistant/brands/devolo.json
@@ -1,5 +1,6 @@
 {
   "domain": "devolo",
   "name": "devolo",
-  "integrations": ["devolo_home_control", "devolo_home_network"]
+  "integrations": ["devolo_home_control", "devolo_home_network"],
+  "iot_standards": ["zwave"]
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 866ad2a54ed..e9323fe8c5d 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -1009,7 +1009,10 @@
           "integration_type": "hub",
           "name": "devolo Home Network"
         }
-      }
+      },
+      "iot_standards": [
+        "zwave"
+      ]
     },
     "dexcom": {
       "name": "Dexcom",
-- 
GitLab


From 5f27e2fe011f9bed216af8260ce5cba061135685 Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Fri, 21 Oct 2022 12:25:21 +0200
Subject: [PATCH 657/985] Improve typing hints MQTT __init__ (#80674)

* Improve typing __init__

* Follow up suggestions
---
 homeassistant/components/mqtt/__init__.py | 34 ++++++++++++-----------
 1 file changed, 18 insertions(+), 16 deletions(-)

diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index ba1af354071..5d26c78da99 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 
 import asyncio
 from collections.abc import Callable
+from datetime import datetime
 import logging
 from typing import Any, cast
 
@@ -236,7 +237,7 @@ def _merge_basic_config(
         hass.config_entries.async_update_entry(entry, data=entry_config)
 
 
-def _merge_extended_config(entry, conf):
+def _merge_extended_config(entry: ConfigEntry, conf: ConfigType) -> dict[str, Any]:
     """Merge advanced options in configuration.yaml config with config entry."""
     # Add default values
     conf = {**DEFAULT_VALUES, **conf}
@@ -251,7 +252,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
     await hass.config_entries.async_reload(entry.entry_id)
 
 
-async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None:
+async def async_fetch_config(
+    hass: HomeAssistant, entry: ConfigEntry
+) -> dict[str, Any] | None:
     """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry."""
     mqtt_data = get_mqtt_data(hass)
     if mqtt_data.reload_entry:
@@ -366,20 +369,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
     async def async_dump_service(call: ServiceCall) -> None:
         """Handle MQTT dump service calls."""
-        messages = []
+        messages: list[tuple[str, str]] = []
 
         @callback
-        def collect_msg(msg):
-            messages.append((msg.topic, msg.payload.replace("\n", "")))
+        def collect_msg(msg: ReceiveMessage) -> None:
+            messages.append((msg.topic, str(msg.payload).replace("\n", "")))
 
         unsub = await async_subscribe(hass, call.data["topic"], collect_msg)
 
-        def write_dump():
+        def write_dump() -> None:
             with open(hass.config.path("mqtt_dump.txt"), "w", encoding="utf8") as fp:
                 for msg in messages:
                     fp.write(",".join(msg) + "\n")
 
-        async def finish_dump(_):
+        async def finish_dump(_: datetime) -> None:
             """Write dump to file."""
             unsub()
             await hass.async_add_executor_job(write_dump)
@@ -439,7 +442,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
         async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config)
 
-    async def async_forward_entry_setup_and_setup_discovery(config_entry):
+    async def async_forward_entry_setup_and_setup_discovery(
+        config_entry: ConfigEntry,
+        conf: ConfigType,
+    ) -> None:
         """Forward the config entry setup to the platforms and set up discovery."""
         reload_manual_setup: bool = False
         # Local import to avoid circular dependencies
@@ -477,7 +483,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         if reload_manual_setup:
             await async_reload_manual_mqtt_items(hass)
 
-    await async_forward_entry_setup_and_setup_discovery(entry)
+    await async_forward_entry_setup_and_setup_discovery(entry, conf)
 
     return True
 
@@ -497,9 +503,7 @@ async def async_reload_manual_mqtt_items(hass: HomeAssistant) -> None:
 )
 @callback
 def websocket_mqtt_info(
-    hass: HomeAssistant,
-    connection: websocket_api.ActiveConnection,
-    msg: dict[str, Any],
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Get MQTT debug info for device."""
     device_id = msg["device_id"]
@@ -516,15 +520,13 @@ def websocket_mqtt_info(
 )
 @websocket_api.async_response
 async def websocket_subscribe(
-    hass: HomeAssistant,
-    connection: websocket_api.ActiveConnection,
-    msg: dict[str, Any],
+    hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
 ) -> None:
     """Subscribe to a MQTT topic."""
     if not connection.user.is_admin:
         raise Unauthorized
 
-    async def forward_messages(mqttmsg: ReceiveMessage):
+    async def forward_messages(mqttmsg: ReceiveMessage) -> None:
         """Forward events to websocket."""
         try:
             payload = cast(bytes, mqttmsg.payload).decode(
-- 
GitLab


From 09eff3c242b48ac3eedf9ac04e5c389a7d73b5a7 Mon Sep 17 00:00:00 2001
From: Fredrik Erlandsson <fredrik.e@gmail.com>
Date: Fri, 21 Oct 2022 12:27:31 +0200
Subject: [PATCH 658/985] Add Bluesound unique id (#80559)

* add unique_id & device_info

* remove broken image

* use a combination of mac and port (for multizone devices)

* use the typed dataclass

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* Don't use get

* fix device_info

* remove device_info

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>
---
 homeassistant/components/bluesound/media_player.py | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py
index 23611e5bef5..833050ba089 100644
--- a/homeassistant/components/bluesound/media_player.py
+++ b/homeassistant/components/bluesound/media_player.py
@@ -38,6 +38,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant, ServiceCall, callback
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.device_registry import format_mac
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.event import async_track_time_interval
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -210,7 +211,6 @@ class BluesoundPlayer(MediaPlayerEntity):
         self._polling_task = None  # The actual polling task.
         self._name = name
         self._id = None
-        self._icon = None
         self._capture_items = []
         self._services_items = []
         self._preset_items = []
@@ -258,8 +258,6 @@ class BluesoundPlayer(MediaPlayerEntity):
             self._id = self._sync_status.get("@id", None)
         if not self._bluesound_device_name:
             self._bluesound_device_name = self._sync_status.get("@name", self.host)
-        if not self._icon:
-            self._icon = self._sync_status.get("@icon", self.host)
 
         if (master := self._sync_status.get("master")) is not None:
             self._is_master = False
@@ -449,6 +447,11 @@ class BluesoundPlayer(MediaPlayerEntity):
             _LOGGER.info("Client connection error, marking %s as offline", self._name)
             raise
 
+    @property
+    def unique_id(self):
+        """Return an unique ID."""
+        return f"{format_mac(self._sync_status['@mac'])}-{self.port}"
+
     async def async_trigger_sync_on_all(self):
         """Trigger sync status update on all devices."""
         _LOGGER.debug("Trigger sync status on all devices")
@@ -678,11 +681,6 @@ class BluesoundPlayer(MediaPlayerEntity):
         """Return the device name as returned by the device."""
         return self._bluesound_device_name
 
-    @property
-    def icon(self):
-        """Return the icon of the device."""
-        return self._icon
-
     @property
     def source_list(self):
         """List of available input sources."""
-- 
GitLab


From 3a92123ac645841fe8c88424e71ac8d49b9ffbf3 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 21 Oct 2022 13:08:19 +0200
Subject: [PATCH 659/985] Update home-assistant/wheels to 2022.10.1 (#80723)

---
 .github/workflows/wheels.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 2bd808f0e8a..0447a702e01 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -92,7 +92,7 @@ jobs:
           name: requirements_diff
 
       - name: Build wheels
-        uses: home-assistant/wheels@2022.06.7
+        uses: home-assistant/wheels@2022.10.1
         with:
           abi: cp310
           tag: musllinux_1_2
@@ -161,7 +161,7 @@ jobs:
           sed -i "/numpy/d" homeassistant/package_constraints.txt
 
       - name: Build wheels
-        uses: home-assistant/wheels@2022.06.7
+        uses: home-assistant/wheels@2022.10.1
         with:
           abi: cp310
           tag: musllinux_1_2
-- 
GitLab


From d42fde63f892c90b604b2fefc69ce345a398b873 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 21 Oct 2022 13:52:29 +0200
Subject: [PATCH 660/985] Update sentry-sdk to 1.10.0 (#80721)

---
 homeassistant/components/sentry/manifest.json | 2 +-
 requirements_all.txt                          | 2 +-
 requirements_test_all.txt                     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json
index a3c5664cd80..cc28c2b7ff5 100644
--- a/homeassistant/components/sentry/manifest.json
+++ b/homeassistant/components/sentry/manifest.json
@@ -3,7 +3,7 @@
   "name": "Sentry",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/sentry",
-  "requirements": ["sentry-sdk==1.9.10"],
+  "requirements": ["sentry-sdk==1.10.0"],
   "codeowners": ["@dcramer", "@frenck"],
   "iot_class": "cloud_polling"
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index 3f621e810e5..f10a2b37f91 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2238,7 +2238,7 @@ sensorpro-ble==0.5.0
 sensorpush-ble==1.5.2
 
 # homeassistant.components.sentry
-sentry-sdk==1.9.10
+sentry-sdk==1.10.0
 
 # homeassistant.components.sharkiq
 sharkiq==0.0.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 7a76806115f..7b6ba1d4b5b 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1541,7 +1541,7 @@ sensorpro-ble==0.5.0
 sensorpush-ble==1.5.2
 
 # homeassistant.components.sentry
-sentry-sdk==1.9.10
+sentry-sdk==1.10.0
 
 # homeassistant.components.sharkiq
 sharkiq==0.0.1
-- 
GitLab


From 319d35626aaecfee89326510a8ccf6cda352f5e7 Mon Sep 17 00:00:00 2001
From: Pascal Vizeli <pvizeli@syshack.ch>
Date: Fri, 21 Oct 2022 14:27:50 +0200
Subject: [PATCH 661/985] Remove building wheels for face detection (#80728)

---
 .github/workflows/wheels.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 0447a702e01..aa578c8b115 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -140,7 +140,6 @@ jobs:
             sed -i "s|# pycups|pycups|g" ${requirement_file}
             sed -i "s|# homekit|homekit|g" ${requirement_file}
             sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
-            sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
             sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
             sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file}
           done
-- 
GitLab


From e6892a613e2ac249913d5c447fd7014d61e33b1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20Sch=C3=A4uble?= <tymmm1@gmail.com>
Date: Fri, 21 Oct 2022 16:13:46 +0200
Subject: [PATCH 662/985] Bump simplepush to 2.1.1 (#80608)

* Update to new library version

* Remove test for removed send_encrypted function

* Bump simplepush to 2.1.1
---
 .../components/simplepush/config_flow.py      | 20 +++++++++++--------
 .../components/simplepush/manifest.json       |  2 +-
 homeassistant/components/simplepush/notify.py | 16 +++++++--------
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 .../components/simplepush/test_config_flow.py |  4 +---
 6 files changed, 24 insertions(+), 22 deletions(-)

diff --git a/homeassistant/components/simplepush/config_flow.py b/homeassistant/components/simplepush/config_flow.py
index 0f0073c5099..01ca508a5c4 100644
--- a/homeassistant/components/simplepush/config_flow.py
+++ b/homeassistant/components/simplepush/config_flow.py
@@ -3,7 +3,7 @@ from __future__ import annotations
 
 from typing import Any
 
-from simplepush import UnknownError, send, send_encrypted
+from simplepush import UnknownError, send
 import voluptuous as vol
 
 from homeassistant import config_entries
@@ -17,15 +17,19 @@ def validate_input(entry: dict[str, str]) -> dict[str, str] | None:
     """Validate user input."""
     try:
         if CONF_PASSWORD in entry:
-            send_encrypted(
-                entry[CONF_DEVICE_KEY],
-                entry[CONF_PASSWORD],
-                entry[CONF_PASSWORD],
-                "HA test",
-                "Message delivered successfully",
+            send(
+                key=entry[CONF_DEVICE_KEY],
+                password=entry[CONF_PASSWORD],
+                salt=entry[CONF_PASSWORD],
+                title="HA test",
+                message="Message delivered successfully",
             )
         else:
-            send(entry[CONF_DEVICE_KEY], "HA test", "Message delivered successfully")
+            send(
+                key=entry[CONF_DEVICE_KEY],
+                title="HA test",
+                message="Message delivered successfully",
+            )
     except UnknownError:
         return {"base": "cannot_connect"}
 
diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json
index 7c37546485a..5b2ad37d92a 100644
--- a/homeassistant/components/simplepush/manifest.json
+++ b/homeassistant/components/simplepush/manifest.json
@@ -2,7 +2,7 @@
   "domain": "simplepush",
   "name": "Simplepush",
   "documentation": "https://www.home-assistant.io/integrations/simplepush",
-  "requirements": ["simplepush==1.1.4"],
+  "requirements": ["simplepush==2.1.1"],
   "codeowners": ["@engrbm87"],
   "config_flow": true,
   "iot_class": "cloud_polling",
diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py
index 108aaf3cbf6..b1c2eb5680e 100644
--- a/homeassistant/components/simplepush/notify.py
+++ b/homeassistant/components/simplepush/notify.py
@@ -4,7 +4,7 @@ from __future__ import annotations
 import logging
 from typing import Any
 
-from simplepush import BadRequest, UnknownError, send, send_encrypted
+from simplepush import BadRequest, UnknownError, send
 
 from homeassistant.components.notify import (
     ATTR_DATA,
@@ -71,16 +71,16 @@ class SimplePushNotificationService(BaseNotificationService):
 
         try:
             if self._password:
-                send_encrypted(
-                    self._device_key,
-                    self._password,
-                    self._salt,
-                    title,
-                    message,
+                send(
+                    key=self._device_key,
+                    password=self._password,
+                    salt=self._salt,
+                    title=title,
+                    message=message,
                     event=event,
                 )
             else:
-                send(self._device_key, title, message, event=event)
+                send(key=self._device_key, title=title, message=message, event=event)
 
         except BadRequest:
             _LOGGER.error("Bad request. Title or message are too long")
diff --git a/requirements_all.txt b/requirements_all.txt
index f10a2b37f91..d82ce4cb9bc 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2253,7 +2253,7 @@ shodan==1.28.0
 simplehound==0.3
 
 # homeassistant.components.simplepush
-simplepush==1.1.4
+simplepush==2.1.1
 
 # homeassistant.components.simplisafe
 simplisafe-python==2022.07.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 7b6ba1d4b5b..105592a6995 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1550,7 +1550,7 @@ sharkiq==0.0.1
 simplehound==0.3
 
 # homeassistant.components.simplepush
-simplepush==1.1.4
+simplepush==2.1.1
 
 # homeassistant.components.simplisafe
 simplisafe-python==2022.07.1
diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py
index 6f0d6a73aa4..02db81ceaa7 100644
--- a/tests/components/simplepush/test_config_flow.py
+++ b/tests/components/simplepush/test_config_flow.py
@@ -29,9 +29,7 @@ def simplepush_setup_fixture():
 @pytest.fixture(autouse=True)
 def mock_api_request():
     """Patch simplepush api request."""
-    with patch("homeassistant.components.simplepush.config_flow.send"), patch(
-        "homeassistant.components.simplepush.config_flow.send_encrypted"
-    ):
+    with patch("homeassistant.components.simplepush.config_flow.send"):
         yield
 
 
-- 
GitLab


From 3b78df07dec8d59674a4bf9052830dbd08565012 Mon Sep 17 00:00:00 2001
From: uvjustin <46082645+uvjustin@users.noreply.github.com>
Date: Fri, 21 Oct 2022 07:34:03 -0700
Subject: [PATCH 663/985] Use empty_moov in stream recorder (#80726)

---
 homeassistant/components/stream/recorder.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py
index 1eb7a6feedb..e917292251a 100644
--- a/homeassistant/components/stream/recorder.py
+++ b/homeassistant/components/stream/recorder.py
@@ -106,7 +106,7 @@ class RecorderOutput(StreamOutput):
                     format=RECORDER_CONTAINER_FORMAT,
                     container_options={
                         "video_track_timescale": str(int(1 / source_v.time_base)),
-                        "movflags": "frag_keyframe",
+                        "movflags": "frag_keyframe+empty_moov",
                         "min_frag_duration": str(
                             self.stream_settings.min_segment_duration
                         ),
-- 
GitLab


From f311c0374182cc93a02cc09a22ff730134e62857 Mon Sep 17 00:00:00 2001
From: Phil Bruckner <pnbruckner@gmail.com>
Date: Fri, 21 Oct 2022 09:41:34 -0500
Subject: [PATCH 664/985] Increase life360 timeout (#80692)

---
 homeassistant/components/life360/const.py       | 2 +-
 homeassistant/components/life360/coordinator.py | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py
index 21bf9a89c5e..d148a06c634 100644
--- a/homeassistant/components/life360/const.py
+++ b/homeassistant/components/life360/const.py
@@ -7,7 +7,7 @@ DOMAIN = "life360"
 LOGGER = logging.getLogger(__package__)
 
 ATTRIBUTION = "Data provided by life360.com"
-COMM_TIMEOUT = 3.05
+COMM_TIMEOUT = 10
 SPEED_FACTOR_MPH = 2.25
 SPEED_DIGITS = 1
 UPDATE_INTERVAL = timedelta(seconds=10)
diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py
index ba3e7672cdb..0b9641bfcae 100644
--- a/homeassistant/components/life360/coordinator.py
+++ b/homeassistant/components/life360/coordinator.py
@@ -116,10 +116,10 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]):
             return await getattr(self._api, func)(*args)
         except LoginError as exc:
             LOGGER.debug("Login error: %s", exc)
-            raise ConfigEntryAuthFailed from exc
+            raise ConfigEntryAuthFailed(exc) from exc
         except Life360Error as exc:
             LOGGER.debug("%s: %s", exc.__class__.__name__, exc)
-            raise UpdateFailed from exc
+            raise UpdateFailed(exc) from exc
 
     async def _async_update_data(self) -> Life360Data:
         """Get & process data from Life360."""
-- 
GitLab


From 19138047552e3d9d5025c20a4ce47683e542de49 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Fri, 21 Oct 2022 16:47:04 +0200
Subject: [PATCH 665/985] Small typing improvements for Plugwise (#80722)

---
 .../components/plugwise/config_flow.py        | 20 ++++++++++---------
 .../components/plugwise/coordinator.py        | 12 +++++++----
 homeassistant/components/plugwise/entity.py   |  4 ++--
 homeassistant/components/plugwise/gateway.py  |  2 +-
 4 files changed, 22 insertions(+), 16 deletions(-)

diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py
index 8cf2456c0b4..09b919e9d1d 100644
--- a/homeassistant/components/plugwise/config_flow.py
+++ b/homeassistant/components/plugwise/config_flow.py
@@ -41,20 +41,22 @@ from .const import (
 )
 
 
-def _base_gw_schema(discovery_info):
+def _base_gw_schema(discovery_info: ZeroconfServiceInfo | None) -> vol.Schema:
     """Generate base schema for gateways."""
-    base_gw_schema = {}
+    base_gw_schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
 
     if not discovery_info:
-        base_gw_schema[vol.Required(CONF_HOST)] = str
-        base_gw_schema[vol.Optional(CONF_PORT, default=DEFAULT_PORT)] = int
-        base_gw_schema[vol.Required(CONF_USERNAME, default=SMILE)] = vol.In(
-            {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
+        base_gw_schema = base_gw_schema.extend(
+            {
+                vol.Required(CONF_HOST): str,
+                vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
+                vol.Required(CONF_USERNAME, default=SMILE): vol.In(
+                    {SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
+                ),
+            }
         )
 
-    base_gw_schema.update({vol.Required(CONF_PASSWORD): str})
-
-    return vol.Schema(base_gw_schema)
+    return base_gw_schema
 
 
 async def validate_gw_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py
index 1c8de0c6544..6fd44efda84 100644
--- a/homeassistant/components/plugwise/coordinator.py
+++ b/homeassistant/components/plugwise/coordinator.py
@@ -1,8 +1,9 @@
 """DataUpdateCoordinator for Plugwise."""
 from datetime import timedelta
-from typing import Any, NamedTuple
+from typing import NamedTuple, cast
 
 from plugwise import Smile
+from plugwise.constants import DeviceData, GatewayData
 from plugwise.exceptions import PlugwiseException, XMLDataMissingError
 
 from homeassistant.core import HomeAssistant
@@ -15,8 +16,8 @@ from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
 class PlugwiseData(NamedTuple):
     """Plugwise data stored in the DataUpdateCoordinator."""
 
-    gateway: dict[str, Any]
-    devices: dict[str, dict[str, Any]]
+    gateway: GatewayData
+    devices: dict[str, DeviceData]
 
 
 class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
@@ -52,4 +53,7 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[PlugwiseData]):
             ) from err
         except PlugwiseException as err:
             raise UpdateFailed(f"Updated failed for: {self.api.smile_name}") from err
-        return PlugwiseData(*data)
+        return PlugwiseData(
+            gateway=cast(GatewayData, data[0]),
+            devices=cast(dict[str, DeviceData], data[1]),
+        )
diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py
index 4d5f78f5202..e2ab5445f07 100644
--- a/homeassistant/components/plugwise/entity.py
+++ b/homeassistant/components/plugwise/entity.py
@@ -1,7 +1,7 @@
 """Generic Plugwise Entity Class."""
 from __future__ import annotations
 
-from typing import Any
+from plugwise.constants import DeviceData
 
 from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
 from homeassistant.helpers.device_registry import (
@@ -72,7 +72,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]):
         )
 
     @property
-    def device(self) -> dict[str, Any]:
+    def device(self) -> DeviceData:
         """Return data for this device."""
         return self.coordinator.data.devices[self._dev_id]
 
diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py
index 5d951e6f997..16b6a977569 100644
--- a/homeassistant/components/plugwise/gateway.py
+++ b/homeassistant/components/plugwise/gateway.py
@@ -82,7 +82,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     return True
 
 
-async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Unload a config entry."""
     if unload_ok := await hass.config_entries.async_unload_platforms(
         entry, PLATFORMS_GATEWAY
-- 
GitLab


From 69dab4acfeac8357d045b5000d42c4f6c66a2f5e Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Fri, 21 Oct 2022 07:47:37 -0700
Subject: [PATCH 666/985] Reduce unnecessary alarm firing to speed up calendar
 trigger test (#80732)

---
 tests/components/calendar/test_trigger.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py
index 98a21db8ef7..24b4b06b493 100644
--- a/tests/components/calendar/test_trigger.py
+++ b/tests/components/calendar/test_trigger.py
@@ -105,6 +105,15 @@ class FakeSchedule:
 
     async def fire_until(self, end: datetime.timedelta) -> None:
         """Simulate the passage of time by firing alarms until the time is reached."""
+
+        current_time = dt_util.as_utc(self.freezer())
+        if (end - current_time) > (TEST_UPDATE_INTERVAL * 2):
+            # Jump ahead to right before the target alarm them to remove
+            # unnecessary waiting, before advancing in smaller increments below.
+            # This leaves time for multiple update intervals to refresh the set
+            # of upcoming events
+            await self.fire_time(end - TEST_UPDATE_INTERVAL * 2)
+
         while dt_util.utcnow() < end:
             self.freezer.tick(TEST_TIME_ADVANCE_INTERVAL)
             await self.fire_time(dt_util.utcnow())
-- 
GitLab


From fe67703e13b4be487497eeea2147f2a6dac2513d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 21 Oct 2022 09:52:03 -0500
Subject: [PATCH 667/985] Log invalid messages instead of raising in system_log
 (#80645)

---
 .../components/system_log/__init__.py         | 50 +++++++++++++++----
 tests/components/system_log/test_init.py      | 33 ++++++++++++
 2 files changed, 72 insertions(+), 11 deletions(-)

diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py
index fae8598407e..9f00009b322 100644
--- a/homeassistant/components/system_log/__init__.py
+++ b/homeassistant/components/system_log/__init__.py
@@ -1,9 +1,11 @@
 """Support for system log."""
+from __future__ import annotations
+
 from collections import OrderedDict, deque
 import logging
 import re
 import traceback
-from typing import Any
+from typing import Any, cast
 
 import voluptuous as vol
 
@@ -56,7 +58,9 @@ SERVICE_WRITE_SCHEMA = vol.Schema(
 )
 
 
-def _figure_out_source(record, call_stack, paths_re):
+def _figure_out_source(
+    record: logging.LogRecord, call_stack: list[tuple[str, int]], paths_re: re.Pattern
+) -> tuple[str, int]:
 
     # If a stack trace exists, extract file names from the entire call stack.
     # The other case is when a regular "log" is made (without an attached
@@ -81,20 +85,44 @@ def _figure_out_source(record, call_stack, paths_re):
 
         # Try to match with a file within Home Assistant
         if match := paths_re.match(pathname[0]):
-            return [match.group(1), pathname[1]]
+            return (cast(str, match.group(1)), pathname[1])
     # Ok, we don't know what this is
     return (record.pathname, record.lineno)
 
 
+def _safe_get_message(record: logging.LogRecord) -> str:
+    """Get message from record and handle exceptions.
+
+    This code will be unreachable during a pytest run
+    because pytest installs a logging handler that
+    will prevent this code from being reached.
+
+    Calling record.getMessage() can raise an exception
+    if the log message does not contain sufficient arguments.
+
+    As there is no guarantees about which exceptions
+    that can be raised, we catch all exceptions and
+    return a generic message.
+
+    This must be manually tested when changing the code.
+    """
+    try:
+        return record.getMessage()
+    except Exception:  # pylint: disable=broad-except
+        return f"Bad logger message: {record.msg} ({record.args})"
+
+
 class LogEntry:
     """Store HA log entries."""
 
-    def __init__(self, record, stack, source):
+    def __init__(self, record: logging.LogRecord, source: tuple[str, int]) -> None:
         """Initialize a log entry."""
         self.first_occurred = self.timestamp = record.created
         self.name = record.name
         self.level = record.levelname
-        self.message = deque([record.getMessage()], maxlen=5)
+        # See the docstring of _safe_get_message for why we need to do this.
+        # This must be manually tested when changing the code.
+        self.message = deque([_safe_get_message(record)], maxlen=5)
         self.exception = ""
         self.root_cause = None
         if record.exc_info:
@@ -129,7 +157,7 @@ class DedupStore(OrderedDict):
         super().__init__()
         self.maxlen = maxlen
 
-    def add_entry(self, entry):
+    def add_entry(self, entry: LogEntry) -> None:
         """Add a new entry."""
         key = entry.hash
 
@@ -158,7 +186,9 @@ class DedupStore(OrderedDict):
 class LogErrorHandler(logging.Handler):
     """Log handler for error messages."""
 
-    def __init__(self, hass, maxlen, fire_event, paths_re):
+    def __init__(
+        self, hass: HomeAssistant, maxlen: int, fire_event: bool, paths_re: re.Pattern
+    ) -> None:
         """Initialize a new LogErrorHandler."""
         super().__init__()
         self.hass = hass
@@ -166,7 +196,7 @@ class LogErrorHandler(logging.Handler):
         self.fire_event = fire_event
         self.paths_re = paths_re
 
-    def emit(self, record):
+    def emit(self, record: logging.LogRecord) -> None:
         """Save error and warning logs.
 
         Everything logged with error or warning is saved in local buffer. A
@@ -177,9 +207,7 @@ class LogErrorHandler(logging.Handler):
         if not record.exc_info:
             stack = [(f[0], f[1]) for f in traceback.extract_stack()]
 
-        entry = LogEntry(
-            record, stack, _figure_out_source(record, stack, self.paths_re)
-        )
+        entry = LogEntry(record, _figure_out_source(record, stack, self.paths_re))
         self.records.add_entry(entry)
         if self.fire_event:
             self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict())
diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py
index 96e5480acb5..18693aee448 100644
--- a/tests/components/system_log/test_init.py
+++ b/tests/components/system_log/test_init.py
@@ -136,6 +136,28 @@ async def test_warning(hass, hass_ws_client):
     assert_log(log, "", "warning message", "WARNING")
 
 
+async def test_warning_good_format(hass, hass_ws_client):
+    """Test that warning with good format arguments are logged and retrieved correctly."""
+    await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
+    await hass.async_block_till_done()
+    _LOGGER.warning("warning message: %s", "test")
+    await hass.async_block_till_done()
+
+    log = find_log(await get_error_log(hass_ws_client), "WARNING")
+    assert_log(log, "", "warning message: test", "WARNING")
+
+
+async def test_warning_missing_format_args(hass, hass_ws_client):
+    """Test that warning with missing format arguments are logged and retrieved correctly."""
+    await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
+    await hass.async_block_till_done()
+    _LOGGER.warning("warning message missing a format arg %s")
+    await hass.async_block_till_done()
+
+    log = find_log(await get_error_log(hass_ws_client), "WARNING")
+    assert_log(log, "", ["warning message missing a format arg %s"], "WARNING")
+
+
 async def test_error(hass, hass_ws_client):
     """Test that errors are logged and retrieved correctly."""
     await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
@@ -195,6 +217,17 @@ async def test_critical(hass, hass_ws_client):
     assert_log(log, "", "critical message", "CRITICAL")
 
 
+async def test_critical_with_missing_format_args(hass, hass_ws_client):
+    """Test that critical messages with missing format args are logged and retrieved correctly."""
+    await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
+    await hass.async_block_till_done()
+
+    try:
+        _LOGGER.critical("critical message %s = %s", "one_but_needs_two")
+    except TypeError:
+        pass
+
+
 async def test_remove_older_logs(hass, hass_ws_client):
     """Test that older logs are rotated out."""
     await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
-- 
GitLab


From c70614fd7c969ccb2b01c77cc93935fbd3f2864a Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
Date: Fri, 21 Oct 2022 17:02:41 +0200
Subject: [PATCH 668/985] Move default options to config_flow for
 waze_travel_time (#80681)

Move default options to config_flow

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>
---
 .../waze_travel_time/config_flow.py           | 19 +++++++--
 .../components/waze_travel_time/const.py      | 11 +++++
 .../components/waze_travel_time/sensor.py     | 40 -------------------
 .../waze_travel_time/test_config_flow.py      | 11 +++--
 .../waze_travel_time/test_sensor.py           | 15 ++++---
 5 files changed, 44 insertions(+), 52 deletions(-)

diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py
index dee133e0513..348b48e3368 100644
--- a/homeassistant/components/waze_travel_time/config_flow.py
+++ b/homeassistant/components/waze_travel_time/config_flow.py
@@ -5,8 +5,10 @@ import voluptuous as vol
 
 from homeassistant import config_entries
 from homeassistant.const import CONF_NAME, CONF_REGION
-from homeassistant.core import callback
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.data_entry_flow import FlowResult
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
 
 from .const import (
     CONF_AVOID_FERRIES,
@@ -20,7 +22,9 @@ from .const import (
     CONF_UNITS,
     CONF_VEHICLE_TYPE,
     DEFAULT_NAME,
+    DEFAULT_OPTIONS,
     DOMAIN,
+    IMPERIAL_UNITS,
     REGIONS,
     UNITS,
     VEHICLE_TYPES,
@@ -28,6 +32,14 @@ from .const import (
 from .helpers import is_valid_config_entry
 
 
+def default_options(hass: HomeAssistant) -> dict[str, str | bool]:
+    """Get the default options."""
+    defaults = DEFAULT_OPTIONS.copy()
+    if hass.config.units is IMPERIAL_SYSTEM:
+        defaults[CONF_UNITS] = IMPERIAL_UNITS
+    return defaults
+
+
 class WazeOptionsFlow(config_entries.OptionsFlow):
     """Handle an options flow for Waze Travel Time."""
 
@@ -35,7 +47,7 @@ class WazeOptionsFlow(config_entries.OptionsFlow):
         """Initialize waze options flow."""
         self.config_entry = config_entry
 
-    async def async_step_init(self, user_input=None):
+    async def async_step_init(self, user_input=None) -> FlowResult:
         """Handle the initial step."""
         if user_input is not None:
             return self.async_create_entry(
@@ -99,7 +111,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
         """Get the options flow for this handler."""
         return WazeOptionsFlow(config_entry)
 
-    async def async_step_user(self, user_input=None):
+    async def async_step_user(self, user_input=None) -> FlowResult:
         """Handle the initial step."""
         errors = {}
         user_input = user_input or {}
@@ -115,6 +127,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
                 return self.async_create_entry(
                     title=user_input.get(CONF_NAME, DEFAULT_NAME),
                     data=user_input,
+                    options=default_options(self.hass),
                 )
 
             # If we get here, it's because we couldn't connect
diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py
index 50384e79d92..1121519f8cd 100644
--- a/homeassistant/components/waze_travel_time/const.py
+++ b/homeassistant/components/waze_travel_time/const.py
@@ -1,4 +1,6 @@
 """Constants for waze_travel_time."""
+from __future__ import annotations
+
 DOMAIN = "waze_travel_time"
 
 CONF_DESTINATION = "destination"
@@ -25,3 +27,12 @@ UNITS = [METRIC_UNITS, IMPERIAL_UNITS]
 
 REGIONS = ["US", "NA", "EU", "IL", "AU"]
 VEHICLE_TYPES = ["car", "taxi", "motorcycle"]
+
+DEFAULT_OPTIONS: dict[str, str | bool] = {
+    CONF_REALTIME: DEFAULT_REALTIME,
+    CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE,
+    CONF_UNITS: METRIC_UNITS,
+    CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES,
+    CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS,
+    CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS,
+}
diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py
index d9916ae7df1..942c1bccb36 100644
--- a/homeassistant/components/waze_travel_time/sensor.py
+++ b/homeassistant/components/waze_travel_time/sensor.py
@@ -26,7 +26,6 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.location import find_coordinates
 from homeassistant.util.unit_conversion import DistanceConverter
-from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     CONF_AVOID_FERRIES,
@@ -39,15 +38,9 @@ from .const import (
     CONF_REALTIME,
     CONF_UNITS,
     CONF_VEHICLE_TYPE,
-    DEFAULT_AVOID_FERRIES,
-    DEFAULT_AVOID_SUBSCRIPTION_ROADS,
-    DEFAULT_AVOID_TOLL_ROADS,
     DEFAULT_NAME,
-    DEFAULT_REALTIME,
-    DEFAULT_VEHICLE_TYPE,
     DOMAIN,
     IMPERIAL_UNITS,
-    METRIC_UNITS,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -61,39 +54,6 @@ async def async_setup_entry(
     async_add_entities: AddEntitiesCallback,
 ) -> None:
     """Set up a Waze travel time sensor entry."""
-    defaults = {
-        CONF_REALTIME: DEFAULT_REALTIME,
-        CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE,
-        CONF_UNITS: METRIC_UNITS,
-        CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES,
-        CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS,
-        CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS,
-    }
-    if hass.config.units is US_CUSTOMARY_SYSTEM:
-        defaults[CONF_UNITS] = IMPERIAL_UNITS
-
-    if not config_entry.options:
-        new_data = config_entry.data.copy()
-        options = {}
-        for key in (
-            CONF_INCL_FILTER,
-            CONF_EXCL_FILTER,
-            CONF_REALTIME,
-            CONF_VEHICLE_TYPE,
-            CONF_AVOID_TOLL_ROADS,
-            CONF_AVOID_SUBSCRIPTION_ROADS,
-            CONF_AVOID_FERRIES,
-            CONF_UNITS,
-        ):
-            if key in new_data:
-                options[key] = new_data.pop(key)
-            elif key in defaults:
-                options[key] = defaults[key]
-
-        hass.config_entries.async_update_entry(
-            config_entry, data=new_data, options=options
-        )
-
     destination = config_entry.data[CONF_DESTINATION]
     origin = config_entry.data[CONF_ORIGIN]
     region = config_entry.data[CONF_REGION]
diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py
index 34ea985888d..51bf1ae8319 100644
--- a/tests/components/waze_travel_time/test_config_flow.py
+++ b/tests/components/waze_travel_time/test_config_flow.py
@@ -14,9 +14,11 @@ from homeassistant.components.waze_travel_time.const import (
     CONF_UNITS,
     CONF_VEHICLE_TYPE,
     DEFAULT_NAME,
+    DEFAULT_OPTIONS,
     DOMAIN,
+    IMPERIAL_UNITS,
 )
-from homeassistant.const import CONF_NAME, CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL
+from homeassistant.const import CONF_NAME, CONF_REGION
 
 from .const import MOCK_CONFIG
 
@@ -53,6 +55,7 @@ async def test_options(hass):
     entry = MockConfigEntry(
         domain=DOMAIN,
         data=MOCK_CONFIG,
+        options=DEFAULT_OPTIONS,
     )
     entry.add_to_hass(hass)
     await hass.config_entries.async_setup(entry.entry_id)
@@ -72,7 +75,7 @@ async def test_options(hass):
             CONF_EXCL_FILTER: "exclude",
             CONF_INCL_FILTER: "include",
             CONF_REALTIME: False,
-            CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+            CONF_UNITS: IMPERIAL_UNITS,
             CONF_VEHICLE_TYPE: "taxi",
         },
     )
@@ -85,7 +88,7 @@ async def test_options(hass):
         CONF_EXCL_FILTER: "exclude",
         CONF_INCL_FILTER: "include",
         CONF_REALTIME: False,
-        CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+        CONF_UNITS: IMPERIAL_UNITS,
         CONF_VEHICLE_TYPE: "taxi",
     }
 
@@ -96,7 +99,7 @@ async def test_options(hass):
         CONF_EXCL_FILTER: "exclude",
         CONF_INCL_FILTER: "include",
         CONF_REALTIME: False,
-        CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+        CONF_UNITS: IMPERIAL_UNITS,
         CONF_VEHICLE_TYPE: "taxi",
     }
 
diff --git a/tests/components/waze_travel_time/test_sensor.py b/tests/components/waze_travel_time/test_sensor.py
index 67ba7c6e311..569335f9e6c 100644
--- a/tests/components/waze_travel_time/test_sensor.py
+++ b/tests/components/waze_travel_time/test_sensor.py
@@ -10,9 +10,10 @@ from homeassistant.components.waze_travel_time.const import (
     CONF_REALTIME,
     CONF_UNITS,
     CONF_VEHICLE_TYPE,
+    DEFAULT_OPTIONS,
     DOMAIN,
+    IMPERIAL_UNITS,
 )
-from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL
 
 from .const import MOCK_CONFIG
 
@@ -51,7 +52,7 @@ def mock_update_keyerror_fixture(mock_wrc):
 
 @pytest.mark.parametrize(
     "data,options",
-    [(MOCK_CONFIG, {})],
+    [(MOCK_CONFIG, DEFAULT_OPTIONS)],
 )
 @pytest.mark.usefixtures("mock_update", "mock_config")
 async def test_sensor(hass):
@@ -84,7 +85,7 @@ async def test_sensor(hass):
         (
             MOCK_CONFIG,
             {
-                CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL,
+                CONF_UNITS: IMPERIAL_UNITS,
                 CONF_REALTIME: True,
                 CONF_VEHICLE_TYPE: "car",
                 CONF_AVOID_TOLL_ROADS: True,
@@ -105,7 +106,9 @@ async def test_imperial(hass):
 @pytest.mark.usefixtures("mock_update_wrcerror")
 async def test_sensor_failed_wrcerror(hass, caplog):
     """Test that sensor update fails with log message."""
-    config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
+    config_entry = MockConfigEntry(
+        domain=DOMAIN, data=MOCK_CONFIG, options=DEFAULT_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()
@@ -117,7 +120,9 @@ async def test_sensor_failed_wrcerror(hass, caplog):
 @pytest.mark.usefixtures("mock_update_keyerror")
 async def test_sensor_failed_keyerror(hass, caplog):
     """Test that sensor update fails with log message."""
-    config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
+    config_entry = MockConfigEntry(
+        domain=DOMAIN, data=MOCK_CONFIG, options=DEFAULT_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()
-- 
GitLab


From 3aa24afad860845542e3a657ae2bec50e64f9477 Mon Sep 17 00:00:00 2001
From: Rami Mosleh <engrbm87@gmail.com>
Date: Fri, 21 Oct 2022 18:52:02 +0300
Subject: [PATCH 669/985] Retry setup in case of empty response from Pushover
 api (#80602)

Retry setup in case of empty response
---
 homeassistant/components/pushover/__init__.py |  2 +-
 tests/components/pushover/test_init.py        | 27 ++++++++++++++++---
 2 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/pushover/__init__.py b/homeassistant/components/pushover/__init__.py
index 3c0c92db044..551e374fbb6 100644
--- a/homeassistant/components/pushover/__init__.py
+++ b/homeassistant/components/pushover/__init__.py
@@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
             pushover_api.validate, entry.data[CONF_USER_KEY]
         )
 
-    except BadAPIRequestError as err:
+    except (BadAPIRequestError, ValueError) as err:
         if "application token is invalid" in str(err):
             raise ConfigEntryAuthFailed(err) from err
         raise ConfigEntryNotReady(err) from err
diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py
index 7a8b02c93a0..ff85e88f07e 100644
--- a/tests/components/pushover/test_init.py
+++ b/tests/components/pushover/test_init.py
@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
 import aiohttp
 from pushover_complete import BadAPIRequestError
 import pytest
+from requests_mock import Mocker
 
 from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
 from homeassistant.components.pushover.const import DOMAIN
@@ -19,7 +20,7 @@ from tests.common import MockConfigEntry
 from tests.components.repairs import get_repairs
 
 
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=False)
 def mock_pushover():
     """Mock pushover."""
     with patch(
@@ -33,6 +34,7 @@ async def test_setup(
     hass_ws_client: Callable[
         [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
     ],
+    mock_pushover: MagicMock,
 ) -> None:
     """Test integration failed due to an error."""
     assert await async_setup_component(
@@ -56,7 +58,9 @@ async def test_setup(
     assert issues[0]["issue_id"] == "deprecated_yaml"
 
 
-async def test_async_setup_entry_success(hass: HomeAssistant) -> None:
+async def test_async_setup_entry_success(
+    hass: HomeAssistant, mock_pushover: MagicMock
+) -> None:
     """Test pushover successful setup."""
     entry = MockConfigEntry(
         domain=DOMAIN,
@@ -68,7 +72,7 @@ async def test_async_setup_entry_success(hass: HomeAssistant) -> None:
     assert entry.state == ConfigEntryState.LOADED
 
 
-async def test_unique_id_updated(hass: HomeAssistant) -> None:
+async def test_unique_id_updated(hass: HomeAssistant, mock_pushover: MagicMock) -> None:
     """Test updating unique_id to new format."""
     entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, unique_id="MYUSERKEY")
     entry.add_to_hass(hass)
@@ -106,3 +110,20 @@ async def test_async_setup_entry_failed_conn_error(
     await hass.config_entries.async_setup(entry.entry_id)
     await hass.async_block_till_done()
     assert entry.state == ConfigEntryState.SETUP_RETRY
+
+
+async def test_async_setup_entry_failed_json_error(
+    hass: HomeAssistant, requests_mock: Mocker
+) -> None:
+    """Test pushover failed setup due to bad json response from library."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data=MOCK_CONFIG,
+    )
+    entry.add_to_hass(hass)
+    requests_mock.post(
+        "https://api.pushover.net/1/users/validate.json", status_code=204
+    )
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+    assert entry.state == ConfigEntryState.SETUP_RETRY
-- 
GitLab


From a2080492dec0f35e8091922eef93f4a8baac64da Mon Sep 17 00:00:00 2001
From: starkillerOG <starkiller.og@gmail.com>
Date: Fri, 21 Oct 2022 18:07:49 +0200
Subject: [PATCH 670/985] Remove Xiaomi Miio YAML import (#78995)

* Deprecate YAML import

* Add logging for unexpected errors

* remove unused import

* fix tests

* unused import

* fix tests

* fix snake_case

* Do not add to standard key string
---
 .../components/xiaomi_miio/__init__.py        |  4 -
 .../components/xiaomi_miio/config_flow.py     | 34 ++++----
 .../components/xiaomi_miio/strings.json       |  3 +-
 .../xiaomi_miio/translations/en.json          |  3 +-
 .../xiaomi_miio/test_config_flow.py           | 84 +++++++++++--------
 5 files changed, 70 insertions(+), 58 deletions(-)

diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py
index 8719319aec8..d3a407d529e 100644
--- a/homeassistant/components/xiaomi_miio/__init__.py
+++ b/homeassistant/components/xiaomi_miio/__init__.py
@@ -383,10 +383,6 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) ->
 
     assert gateway_id
 
-    # For backwards compat
-    if gateway_id.endswith("-gateway"):
-        hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"])
-
     # Connect to gateway
     gateway = ConnectXiaomiGateway(hass, entry)
     try:
diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py
index 4e2ba24bc05..70e6fb5c0b6 100644
--- a/homeassistant/components/xiaomi_miio/config_flow.py
+++ b/homeassistant/components/xiaomi_miio/config_flow.py
@@ -13,7 +13,7 @@ import voluptuous as vol
 from homeassistant import config_entries
 from homeassistant.components import zeroconf
 from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN
+from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN
 from homeassistant.core import callback
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers.device_registry import format_mac
@@ -145,18 +145,6 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
             return await self.async_step_cloud()
         return self.async_show_form(step_id="reauth_confirm")
 
-    async def async_step_import(self, conf: dict[str, Any]) -> FlowResult:
-        """Import a configuration from config.yaml."""
-        self.host = conf[CONF_HOST]
-        self.token = conf[CONF_TOKEN]
-        self.name = conf.get(CONF_NAME)
-        self.model = conf.get(CONF_MODEL)
-
-        self.context.update(
-            {"title_placeholders": {"name": f"YAML import {self.host}"}}
-        )
-        return await self.async_step_connect()
-
     async def async_step_user(
         self, user_input: dict[str, Any] | None = None
     ) -> FlowResult:
@@ -250,15 +238,22 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
                     errors["base"] = "cloud_login_error"
             except MiCloudAccessDenied:
                 errors["base"] = "cloud_login_error"
+            except Exception:  # pylint: disable=broad-except
+                _LOGGER.exception("Unexpected exception in Miio cloud login")
+                return self.async_abort(reason="unknown")
 
             if errors:
                 return self.async_show_form(
                     step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
                 )
 
-            devices_raw = await self.hass.async_add_executor_job(
-                miio_cloud.get_devices, cloud_country
-            )
+            try:
+                devices_raw = await self.hass.async_add_executor_job(
+                    miio_cloud.get_devices, cloud_country
+                )
+            except Exception:  # pylint: disable=broad-except
+                _LOGGER.exception("Unexpected exception in Miio cloud get devices")
+                return self.async_abort(reason="unknown")
 
             if not devices_raw:
                 errors["base"] = "cloud_no_devices"
@@ -353,6 +348,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         except SetupException:
             if self.model is None:
                 errors["base"] = "cannot_connect"
+        except Exception:  # pylint: disable=broad-except
+            _LOGGER.exception("Unexpected exception in connect Xiaomi device")
+            return self.async_abort(reason="unknown")
 
         device_info = connect_device_class.device_info
 
@@ -386,8 +384,8 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
                 data[CONF_CLOUD_USERNAME] = self.cloud_username
                 data[CONF_CLOUD_PASSWORD] = self.cloud_password
                 data[CONF_CLOUD_COUNTRY] = self.cloud_country
-            self.hass.config_entries.async_update_entry(existing_entry, data=data)
-            await self.hass.config_entries.async_reload(existing_entry.entry_id)
+            if self.hass.config_entries.async_update_entry(existing_entry, data=data):
+                await self.hass.config_entries.async_reload(existing_entry.entry_id)
             return self.async_abort(reason="reauth_successful")
 
         if self.name is None:
diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json
index e359f54cc5a..ea9e1712697 100644
--- a/homeassistant/components/xiaomi_miio/strings.json
+++ b/homeassistant/components/xiaomi_miio/strings.json
@@ -5,7 +5,8 @@
       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
       "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
       "incomplete_info": "Incomplete information to setup device, no host or token supplied.",
-      "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio."
+      "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio.",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
     },
     "error": {
       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json
index c37be0a7f74..d24509e0e25 100644
--- a/homeassistant/components/xiaomi_miio/translations/en.json
+++ b/homeassistant/components/xiaomi_miio/translations/en.json
@@ -5,7 +5,8 @@
             "already_in_progress": "Configuration flow is already in progress",
             "incomplete_info": "Incomplete information to setup device, no host or token supplied.",
             "not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio.",
-            "reauth_successful": "Re-authentication was successful"
+            "reauth_successful": "Re-authentication was successful",
+            "unknown": "Unexpected error"
         },
         "error": {
             "cannot_connect": "Failed to connect",
diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py
index e47a1a1ace5..9d8a8b39167 100644
--- a/tests/components/xiaomi_miio/test_config_flow.py
+++ b/tests/components/xiaomi_miio/test_config_flow.py
@@ -9,7 +9,7 @@ import pytest
 from homeassistant import config_entries, data_entry_flow
 from homeassistant.components import zeroconf
 from homeassistant.components.xiaomi_miio import const
-from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN
+from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN
 
 from . import TEST_MAC
 
@@ -67,7 +67,7 @@ TEST_CLOUD_DEVICES_2 = [
 
 @pytest.fixture(name="xiaomi_miio_connect", autouse=True)
 def xiaomi_miio_connect_fixture():
-    """Mock denonavr connection and entry setup."""
+    """Mock miio connection and entry setup."""
     mock_info = get_mock_info()
 
     with patch(
@@ -320,6 +320,22 @@ async def test_config_flow_gateway_cloud_login_error(hass):
     assert result["step_id"] == "cloud"
     assert result["errors"] == {"base": "cloud_login_error"}
 
+    with patch(
+        "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login",
+        side_effect=Exception({}),
+    ):
+        result = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {
+                const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+                const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+                const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+            },
+        )
+
+    assert result["type"] == "abort"
+    assert result["reason"] == "unknown"
+
 
 async def test_config_flow_gateway_cloud_no_devices(hass):
     """Test a failed config flow using cloud with no devices."""
@@ -348,6 +364,22 @@ async def test_config_flow_gateway_cloud_no_devices(hass):
     assert result["step_id"] == "cloud"
     assert result["errors"] == {"base": "cloud_no_devices"}
 
+    with patch(
+        "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
+        side_effect=Exception({}),
+    ):
+        result = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {
+                const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
+                const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
+                const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
+            },
+        )
+
+    assert result["type"] == "abort"
+    assert result["reason"] == "unknown"
+
 
 async def test_config_flow_gateway_cloud_missing_token(hass):
     """Test a failed config flow using cloud with a missing token."""
@@ -558,34 +590,6 @@ async def test_config_flow_step_unknown_device(hass):
     assert result["errors"] == {"base": "unknown_device"}
 
 
-async def test_import_flow_success(hass):
-    """Test a successful import form yaml for a device."""
-    mock_info = get_mock_info(model=const.MODELS_SWITCH[0])
-
-    with patch(
-        "homeassistant.components.xiaomi_miio.device.Device.info",
-        return_value=mock_info,
-    ):
-        result = await hass.config_entries.flow.async_init(
-            const.DOMAIN,
-            context={"source": config_entries.SOURCE_IMPORT},
-            data={CONF_NAME: TEST_NAME, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
-        )
-
-    assert result["type"] == "create_entry"
-    assert result["title"] == TEST_NAME
-    assert result["data"] == {
-        const.CONF_FLOW_TYPE: const.CONF_DEVICE,
-        const.CONF_CLOUD_USERNAME: None,
-        const.CONF_CLOUD_PASSWORD: None,
-        const.CONF_CLOUD_COUNTRY: None,
-        CONF_HOST: TEST_HOST,
-        CONF_TOKEN: TEST_TOKEN,
-        CONF_MODEL: const.MODELS_SWITCH[0],
-        const.CONF_MAC: TEST_MAC,
-    }
-
-
 async def test_config_flow_step_device_manual_model_error(hass):
     """Test config flow, device connection error, model None."""
     result = await hass.config_entries.flow.async_init(
@@ -618,6 +622,18 @@ async def test_config_flow_step_device_manual_model_error(hass):
     assert result["step_id"] == "connect"
     assert result["errors"] == {"base": "cannot_connect"}
 
+    with patch(
+        "homeassistant.components.xiaomi_miio.device.Device.info",
+        side_effect=Exception({}),
+    ):
+        result = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {CONF_MODEL: TEST_MODEL},
+        )
+
+    assert result["type"] == "abort"
+    assert result["reason"] == "unknown"
+
 
 async def test_config_flow_step_device_manual_model_succes(hass):
     """Test config flow, device connection error, manual model."""
@@ -724,7 +740,7 @@ async def config_flow_device_success(hass, model_to_test):
 
 async def config_flow_generic_roborock(hass):
     """Test a successful config flow for a generic roborock vacuum."""
-    DUMMY_MODEL = "roborock.vacuum.dummy"
+    dummy_model = "roborock.vacuum.dummy"
 
     result = await hass.config_entries.flow.async_init(
         const.DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -743,7 +759,7 @@ async def config_flow_generic_roborock(hass):
     assert result["step_id"] == "manual"
     assert result["errors"] == {}
 
-    mock_info = get_mock_info(model=DUMMY_MODEL)
+    mock_info = get_mock_info(model=dummy_model)
 
     with patch(
         "homeassistant.components.xiaomi_miio.device.Device.info",
@@ -755,7 +771,7 @@ async def config_flow_generic_roborock(hass):
         )
 
     assert result["type"] == "create_entry"
-    assert result["title"] == DUMMY_MODEL
+    assert result["title"] == dummy_model
     assert result["data"] == {
         const.CONF_FLOW_TYPE: const.CONF_DEVICE,
         const.CONF_CLOUD_USERNAME: None,
@@ -763,7 +779,7 @@ async def config_flow_generic_roborock(hass):
         const.CONF_CLOUD_COUNTRY: None,
         CONF_HOST: TEST_HOST,
         CONF_TOKEN: TEST_TOKEN,
-        CONF_MODEL: DUMMY_MODEL,
+        CONF_MODEL: dummy_model,
         const.CONF_MAC: TEST_MAC,
     }
 
-- 
GitLab


From 611194ddd9a8e29843f2bfa3d496d08c855526a7 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 21 Oct 2022 12:17:21 -0500
Subject: [PATCH 671/985] Fix zha LogEntry call (#80737)

The signature for LogEntry changed in #80645 to drop the
unused argument
---
 homeassistant/components/zha/core/gateway.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index 5261396c794..17adc5fc848 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -754,7 +754,7 @@ class LogRelayHandler(logging.Handler):
                 "|".join([re.escape(x) for x in (hass_path, config_dir)])
             )
         )
-        entry = LogEntry(record, stack, _figure_out_source(record, stack, paths_re))
+        entry = LogEntry(record, _figure_out_source(record, stack, paths_re))
         async_dispatcher_send(
             self.hass,
             ZHA_GW_MSG,
-- 
GitLab


From 9c8a919e3f4ecf8f11998290374ebe86e23b470e Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 21 Oct 2022 13:49:39 -0500
Subject: [PATCH 672/985] Remove system_log missing format arg test (#80739)

---
 tests/components/system_log/test_init.py | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py
index 18693aee448..53cb6531aaf 100644
--- a/tests/components/system_log/test_init.py
+++ b/tests/components/system_log/test_init.py
@@ -217,17 +217,6 @@ async def test_critical(hass, hass_ws_client):
     assert_log(log, "", "critical message", "CRITICAL")
 
 
-async def test_critical_with_missing_format_args(hass, hass_ws_client):
-    """Test that critical messages with missing format args are logged and retrieved correctly."""
-    await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
-    await hass.async_block_till_done()
-
-    try:
-        _LOGGER.critical("critical message %s = %s", "one_but_needs_two")
-    except TypeError:
-        pass
-
-
 async def test_remove_older_logs(hass, hass_ws_client):
     """Test that older logs are rotated out."""
     await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
-- 
GitLab


From b8c574e9abf9943b471716c1b5b297bfe8c3b178 Mon Sep 17 00:00:00 2001
From: javicalle <31999997+javicalle@users.noreply.github.com>
Date: Fri, 21 Oct 2022 20:53:03 +0200
Subject: [PATCH 673/985] Add `_TZE200_kds0pmmv` to ZHA `ZONNSMARTThermostat`
 (#80746)

Add `_TZE200_kds0pmmv` to `ZONNSMARTThermostat`

Related to: https://github.com/zigpy/zha-device-handlers/pull/1843
---
 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 a4e1be78c08..155a254217e 100644
--- a/homeassistant/components/zha/climate.py
+++ b/homeassistant/components/zha/climate.py
@@ -765,6 +765,7 @@ class StelproFanHeater(Thermostat):
         "_TZE200_e9ba97vf",  # TV01-ZG
         "_TZE200_hue3yfsn",  # TV02-ZG
         "_TZE200_husqqvux",  # TSL-TRV-TV01ZG
+        "_TZE200_kds0pmmv",  # MOES TRV TV02
         "_TZE200_kly8gjlz",  # TV05-ZG
         "_TZE200_lnbfnyxd",
         "_TZE200_mudxchsu",
-- 
GitLab


From 91c5aac8ba22b0f7df51e50e2be4eabc1f82b8b2 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 21 Oct 2022 13:54:02 -0500
Subject: [PATCH 674/985] Add homekit notifications to the list of allowed
 apple bluetooth start bytes (#80733)

---
 homeassistant/components/bluetooth/manager.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index b38f6005b50..ba294e8e64a 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -61,9 +61,11 @@ APPLE_MFR_ID: Final = 76
 APPLE_IBEACON_START_BYTE: Final = 0x02  # iBeacon (tilt_ble)
 APPLE_HOMEKIT_START_BYTE: Final = 0x06  # homekit_controller
 APPLE_DEVICE_ID_START_BYTE: Final = 0x10  # bluetooth_le_tracker
+APPLE_HOMEKIT_NOTIFY_START_BYTE: Final = 0x11  # homekit_controller
 APPLE_START_BYTES_WANTED: Final = {
     APPLE_IBEACON_START_BYTE,
     APPLE_HOMEKIT_START_BYTE,
+    APPLE_HOMEKIT_NOTIFY_START_BYTE,
     APPLE_DEVICE_ID_START_BYTE,
 }
 
-- 
GitLab


From e40db797c5fb0b16d69dda9b87dd13760c7ed0d0 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Fri, 21 Oct 2022 14:08:37 -0600
Subject: [PATCH 675/985] Remove redundant `async_update` for OpenUV entities
 (#80735)

---
 homeassistant/components/openuv/__init__.py | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py
index 1e85e70100f..fb649bce759 100644
--- a/homeassistant/components/openuv/__init__.py
+++ b/homeassistant/components/openuv/__init__.py
@@ -310,10 +310,3 @@ class OpenUvEntity(CoordinatorEntity):
         """Handle entity which will be added."""
         await super().async_added_to_hass()
         self._update_from_latest_data()
-
-    async def async_update(self) -> None:
-        """Update the entity.
-
-        Only used by the generic entity update service.
-        """
-        await self.coordinator.async_request_refresh()
-- 
GitLab


From 8dc0846d985fd76f8408c9047538ca55514e983f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Fri, 21 Oct 2022 15:31:45 -0500
Subject: [PATCH 676/985] Bump qingping-ble to 0.8.2 to handle door left open
 (#80748)

fixes #78439
---
 homeassistant/components/qingping/binary_sensor.py | 4 ++++
 homeassistant/components/qingping/manifest.json    | 2 +-
 requirements_all.txt                               | 2 +-
 requirements_test_all.txt                          | 2 +-
 4 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py
index cdc75af0b09..046792a2ff2 100644
--- a/homeassistant/components/qingping/binary_sensor.py
+++ b/homeassistant/components/qingping/binary_sensor.py
@@ -39,6 +39,10 @@ BINARY_SENSOR_DESCRIPTIONS = {
         key=QingpingBinarySensorDeviceClass.DOOR,
         device_class=BinarySensorDeviceClass.DOOR,
     ),
+    QingpingBinarySensorDeviceClass.PROBLEM: BinarySensorEntityDescription(
+        key=QingpingBinarySensorDeviceClass.PROBLEM,
+        device_class=BinarySensorDeviceClass.PROBLEM,
+    ),
 }
 
 
diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json
index a30795fb42d..31657280b19 100644
--- a/homeassistant/components/qingping/manifest.json
+++ b/homeassistant/components/qingping/manifest.json
@@ -11,7 +11,7 @@
       "connectable": false
     }
   ],
-  "requirements": ["qingping-ble==0.8.1"],
+  "requirements": ["qingping-ble==0.8.2"],
   "dependencies": ["bluetooth"],
   "codeowners": ["@bdraco", "@skgsergio"],
   "iot_class": "local_push"
diff --git a/requirements_all.txt b/requirements_all.txt
index d82ce4cb9bc..63c5e9064fd 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2123,7 +2123,7 @@ pyzbar==0.1.7
 pyzerproc==0.4.8
 
 # homeassistant.components.qingping
-qingping-ble==0.8.1
+qingping-ble==0.8.2
 
 # homeassistant.components.qnap
 qnapstats==0.4.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 105592a6995..b4281a9d605 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1471,7 +1471,7 @@ pyws66i==1.1
 pyzerproc==0.4.8
 
 # homeassistant.components.qingping
-qingping-ble==0.8.1
+qingping-ble==0.8.2
 
 # homeassistant.components.rachio
 rachiopy==1.0.3
-- 
GitLab


From 8974657ba98318c550d3906ebbfb5ab9033ba8b1 Mon Sep 17 00:00:00 2001
From: starkillerOG <starkiller.og@gmail.com>
Date: Fri, 21 Oct 2022 22:50:00 +0200
Subject: [PATCH 677/985] Fix Goodwe spinlock (#80624)

* fix spinlock

* Add debug log

* fix styling
---
 homeassistant/components/goodwe/sensor.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py
index bf01f449724..22439a05491 100644
--- a/homeassistant/components/goodwe/sensor.py
+++ b/homeassistant/components/goodwe/sensor.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 from collections.abc import Callable
 from dataclasses import dataclass
 from datetime import timedelta
+import logging
 from typing import Any, cast
 
 from goodwe import Inverter, Sensor, SensorKind
@@ -36,6 +37,8 @@ import homeassistant.util.dt as dt_util
 
 from .const import DOMAIN, KEY_COORDINATOR, KEY_DEVICE_INFO, KEY_INVERTER
 
+_LOGGER = logging.getLogger(__name__)
+
 # Sensor name of battery SoC
 BATTERY_SOC = "battery_soc"
 
@@ -209,7 +212,10 @@ class InverterSensor(CoordinatorEntity, SensorEntity):
             self._previous_value = 0
             self.coordinator.data[self._sensor.id_] = 0
             self.async_write_ha_state()
-        next_midnight = dt_util.start_of_local_day(dt_util.utcnow() + timedelta(days=1))
+            _LOGGER.debug("Goodwe reset %s to 0", self.name)
+        next_midnight = dt_util.start_of_local_day(
+            dt_util.now() + timedelta(days=1, minutes=1)
+        )
         self._stop_reset = async_track_point_in_time(
             self.hass, self.async_reset, next_midnight
         )
@@ -218,7 +224,7 @@ class InverterSensor(CoordinatorEntity, SensorEntity):
         """Schedule reset task at midnight."""
         if self._sensor.id_ in DAILY_RESET:
             next_midnight = dt_util.start_of_local_day(
-                dt_util.utcnow() + timedelta(days=1)
+                dt_util.now() + timedelta(days=1)
             )
             self._stop_reset = async_track_point_in_time(
                 self.hass, self.async_reset, next_midnight
-- 
GitLab


From f21fabba171dc4e2d3f248b5a8287f7ae71e82dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Klomp?= <rene@klomp.ws>
Date: Fri, 21 Oct 2022 22:54:42 +0200
Subject: [PATCH 678/985] Bump pysma to 0.7.1 (#80601)

* Bump pysma to 0.7.0

* Bump pysma to 0.7.1

* Fix test
---
 homeassistant/components/sma/manifest.json | 2 +-
 requirements_all.txt                       | 2 +-
 requirements_test_all.txt                  | 2 +-
 tests/components/sma/conftest.py           | 4 ++--
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json
index 8183e7a97c0..c65f3b81d3b 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.6.12"],
+  "requirements": ["pysma==0.7.1"],
   "codeowners": ["@kellerza", "@rklomp"],
   "iot_class": "local_polling",
   "loggers": ["pysma"]
diff --git a/requirements_all.txt b/requirements_all.txt
index 63c5e9064fd..700813104f4 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1887,7 +1887,7 @@ pysignalclirestapi==0.3.18
 pyskyqhub==0.1.4
 
 # homeassistant.components.sma
-pysma==0.6.12
+pysma==0.7.1
 
 # homeassistant.components.smappee
 pysmappee==0.2.29
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b4281a9d605..c5ef4f3c0f8 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1328,7 +1328,7 @@ pysiaalarm==3.0.2
 pysignalclirestapi==0.3.18
 
 # homeassistant.components.sma
-pysma==0.6.12
+pysma==0.7.1
 
 # homeassistant.components.smappee
 pysmappee==0.2.29
diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py
index b953d8692a8..2ce5db5e0ca 100644
--- a/tests/components/sma/conftest.py
+++ b/tests/components/sma/conftest.py
@@ -1,7 +1,7 @@
 """Fixtures for sma tests."""
 from unittest.mock import patch
 
-from pysma.const import DEVCLASS_INVERTER
+from pysma.const import GENERIC_SENSORS
 from pysma.definitions import sensor_map
 from pysma.sensor import Sensors
 import pytest
@@ -32,7 +32,7 @@ async def init_integration(hass, mock_config_entry):
     mock_config_entry.add_to_hass(hass)
 
     with patch("pysma.SMA.read"), patch(
-        "pysma.SMA.get_sensors", return_value=Sensors(sensor_map[DEVCLASS_INVERTER])
+        "pysma.SMA.get_sensors", return_value=Sensors(sensor_map[GENERIC_SENSORS])
     ):
         await hass.config_entries.async_setup(mock_config_entry.entry_id)
     await hass.async_block_till_done()
-- 
GitLab


From da099532fe837604383d5e195be4a0320941a87c Mon Sep 17 00:00:00 2001
From: kevdliu <1766838+kevdliu@users.noreply.github.com>
Date: Fri, 21 Oct 2022 17:19:26 -0400
Subject: [PATCH 679/985] Load ecobee notify platform via discovery (#78558)

* Fix ecobee notify platform KeyError

* set up notify platform via discovery

* address comments

* fix isort

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 homeassistant/components/ecobee/__init__.py | 26 +++++++++++++++---
 homeassistant/components/ecobee/const.py    |  3 ++-
 homeassistant/components/ecobee/notify.py   | 30 +++++++++++----------
 3 files changed, 41 insertions(+), 18 deletions(-)

diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py
index 7204dbf8de2..31a1e753fc6 100644
--- a/homeassistant/components/ecobee/__init__.py
+++ b/homeassistant/components/ecobee/__init__.py
@@ -5,13 +5,21 @@ from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenE
 import voluptuous as vol
 
 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import CONF_API_KEY
+from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, discovery
 from homeassistant.helpers.typing import ConfigType
 from homeassistant.util import Throttle
 
-from .const import _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, PLATFORMS
+from .const import (
+    _LOGGER,
+    ATTR_CONFIG_ENTRY_ID,
+    CONF_REFRESH_TOKEN,
+    DATA_ECOBEE_CONFIG,
+    DATA_HASS_CONFIG,
+    DOMAIN,
+    PLATFORMS,
+)
 
 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
 
@@ -30,7 +38,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     migrating from the old ecobee integration. Otherwise, the user will have to
     continue setting up the integration via the config flow.
     """
+
     hass.data[DATA_ECOBEE_CONFIG] = config.get(DOMAIN, {})
+    hass.data[DATA_HASS_CONFIG] = config
 
     if not hass.config_entries.async_entries(DOMAIN) and hass.data[DATA_ECOBEE_CONFIG]:
         # No config entry exists and configuration.yaml config exists, trigger the import flow.
@@ -63,6 +73,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
 
+    hass.async_create_task(
+        discovery.async_load_platform(
+            hass,
+            Platform.NOTIFY,
+            DOMAIN,
+            {CONF_NAME: entry.title, ATTR_CONFIG_ENTRY_ID: entry.entry_id},
+            hass.data[DATA_HASS_CONFIG],
+        )
+    )
+
     return True
 
 
diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py
index b4e0c485e45..4a318f2be3c 100644
--- a/homeassistant/components/ecobee/const.py
+++ b/homeassistant/components/ecobee/const.py
@@ -20,8 +20,9 @@ _LOGGER = logging.getLogger(__package__)
 
 DOMAIN = "ecobee"
 DATA_ECOBEE_CONFIG = "ecobee_config"
+DATA_HASS_CONFIG = "ecobee_hass_config"
+ATTR_CONFIG_ENTRY_ID = "entry_id"
 
-CONF_INDEX = "index"
 CONF_REFRESH_TOKEN = "refresh_token"
 
 ECOBEE_MODEL_TO_NAME = {
diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py
index a8f53a027b3..75d1316f0e3 100644
--- a/homeassistant/components/ecobee/notify.py
+++ b/homeassistant/components/ecobee/notify.py
@@ -1,31 +1,33 @@
 """Support for Ecobee Send Message service."""
-import voluptuous as vol
 
-from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
-import homeassistant.helpers.config_validation as cv
+from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
 
-from .const import CONF_INDEX, DOMAIN
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
-    {vol.Optional(CONF_INDEX, default=0): cv.positive_int}
-)
+from .const import DOMAIN
 
 
 def get_service(hass, config, discovery_info=None):
     """Get the Ecobee notification service."""
+    if discovery_info is None:
+        return None
+
     data = hass.data[DOMAIN]
-    index = config.get(CONF_INDEX)
-    return EcobeeNotificationService(data, index)
+    return EcobeeNotificationService(data.ecobee)
 
 
 class EcobeeNotificationService(BaseNotificationService):
     """Implement the notification service for the Ecobee thermostat."""
 
-    def __init__(self, data, thermostat_index):
+    def __init__(self, ecobee):
         """Initialize the service."""
-        self.data = data
-        self.thermostat_index = thermostat_index
+        self.ecobee = ecobee
 
     def send_message(self, message="", **kwargs):
         """Send a message."""
-        self.data.ecobee.send_message(self.thermostat_index, message)
+        targets = kwargs.get(ATTR_TARGET)
+
+        if not targets:
+            raise ValueError("Missing required argument: target")
+
+        for target in targets:
+            thermostat_index = int(target)
+            self.ecobee.send_message(thermostat_index, message)
-- 
GitLab


From 7714ce7235c2555b4216b00a6a4f0640c1d395f5 Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Sat, 22 Oct 2022 00:07:23 +0200
Subject: [PATCH 680/985] Update pylint to 2.15.5 (#80759)

---
 requirements_test.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements_test.txt b/requirements_test.txt
index 2cbe45ace49..b15ceb3b002 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -14,7 +14,7 @@ freezegun==1.2.2
 mock-open==1.4.0
 mypy==0.982
 pre-commit==2.20.0
-pylint==2.15.4
+pylint==2.15.5
 pipdeptree==2.3.1
 pytest-aiohttp==0.3.0
 pytest-cov==3.0.0
-- 
GitLab


From 76dbd017a767be03b14794bf7dbb5d273fce12b1 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
Date: Fri, 21 Oct 2022 20:27:59 -0400
Subject: [PATCH 681/985] Conditionally include config flow and iot_class when
 relevant (#80756)

---
 homeassistant/generated/integrations.json | 400 +++++++++-------------
 script/hassfest/config_flow.py            |  11 +-
 2 files changed, 165 insertions(+), 246 deletions(-)

diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index e9323fe8c5d..162431052be 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -93,15 +93,15 @@
       "name": "Airthings",
       "integrations": {
         "airthings": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Airthings"
         },
         "airthings_ble": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Airthings BLE"
         }
       }
@@ -158,27 +158,23 @@
       "name": "Amazon",
       "integrations": {
         "alexa": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Amazon Alexa"
         },
         "amazon_polly": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Amazon Polly"
         },
         "aws": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Amazon Web Services (AWS)"
         },
         "route53": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "AWS Route53"
         }
       }
@@ -258,38 +254,37 @@
       "name": "Apple",
       "integrations": {
         "apple_tv": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Apple TV"
         },
         "homekit_controller": {
+          "integration_type": "hub",
           "config_flow": true,
-          "iot_class": "local_push",
-          "integration_type": "hub"
+          "iot_class": "local_push"
         },
         "homekit": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "HomeKit"
         },
         "ibeacon": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "iBeacon Tracker"
         },
         "icloud": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Apple iCloud"
         },
         "itunes": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Apple iTunes"
         }
       }
@@ -340,15 +335,13 @@
       "name": "Aruba",
       "integrations": {
         "aruba": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Aruba"
         },
         "cppm_tracker": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Aruba ClearPass"
         }
       }
@@ -369,15 +362,13 @@
       "name": "Asterisk",
       "integrations": {
         "asterisk_cdr": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Asterisk Call Detail Records"
         },
         "asterisk_mbox": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "Asterisk Voicemail"
         }
       }
@@ -410,15 +401,15 @@
       "name": "August Home",
       "integrations": {
         "august": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "August"
         },
         "yalexs_ble": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Yale Access Bluetooth"
         }
       }
@@ -718,21 +709,18 @@
       "name": "Cisco",
       "integrations": {
         "cisco_ios": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Cisco IOS"
         },
         "cisco_mobility_express": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Cisco Mobility Express"
         },
         "cisco_webex_teams": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Cisco Webex Teams"
         }
       }
@@ -759,15 +747,13 @@
       "name": "ClickSend",
       "integrations": {
         "clicksend": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "ClickSend SMS"
         },
         "clicksend_tts": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "ClickSend TTS"
         }
       }
@@ -963,21 +949,20 @@
       "name": "Denon",
       "integrations": {
         "denon": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Denon Network Receivers"
         },
         "denonavr": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Denon AVR Network Receivers"
         },
         "heos": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Denon HEOS"
         }
       }
@@ -998,15 +983,15 @@
       "name": "devolo",
       "integrations": {
         "devolo_home_control": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "devolo Home Control"
         },
         "devolo_home_network": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "devolo Home Network"
         }
       },
@@ -1076,15 +1061,15 @@
       "name": "DLNA",
       "integrations": {
         "dlna_dmr": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "DLNA Digital Media Renderer"
         },
         "dlna_dms": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "DLNA Digital Media Server"
         }
       }
@@ -1265,15 +1250,14 @@
       "name": "Elgato",
       "integrations": {
         "avea": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Elgato Avea"
         },
         "elgato": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Elgato Light"
         }
       }
@@ -1312,15 +1296,13 @@
       "name": "emoncms",
       "integrations": {
         "emoncms": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Emoncms"
         },
         "emoncms_history": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Emoncms History"
         }
       }
@@ -1394,15 +1376,14 @@
       "name": "Epson",
       "integrations": {
         "epson": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Epson"
         },
         "epsonworkforce": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Epson Workforce"
         }
       }
@@ -1411,15 +1392,13 @@
       "name": "eQ-3",
       "integrations": {
         "eq3btsmart": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "eQ-3 Bluetooth Smart Thermostats"
         },
         "maxcube": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "eQ-3 MAX!"
         }
       }
@@ -1506,21 +1485,17 @@
       "name": "FFmpeg",
       "integrations": {
         "ffmpeg": {
-          "config_flow": false,
-          "iot_class": null,
           "integration_type": "hub",
           "name": "FFmpeg"
         },
         "ffmpeg_motion": {
-          "config_flow": false,
-          "iot_class": "calculated",
           "integration_type": "hub",
+          "iot_class": "calculated",
           "name": "FFmpeg Motion"
         },
         "ffmpeg_noise": {
-          "config_flow": false,
-          "iot_class": "calculated",
           "integration_type": "hub",
+          "iot_class": "calculated",
           "name": "FFmpeg Noise"
         }
       }
@@ -1737,21 +1712,21 @@
       "name": "FRITZ!Box",
       "integrations": {
         "fritz": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "AVM FRITZ!Box Tools"
         },
         "fritzbox": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "AVM FRITZ!SmartHome"
         },
         "fritzbox_callmonitor": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "AVM FRITZ!Box Call Monitor"
         }
       }
@@ -1854,15 +1829,15 @@
       "name": "GeoNet",
       "integrations": {
         "geonetnz_quakes": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "GeoNet NZ Quakes"
         },
         "geonetnz_volcano": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "GeoNet NZ Volcano"
         }
       }
@@ -1901,15 +1876,13 @@
       "name": "Global Cach\u00e9",
       "integrations": {
         "gc100": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Global Cach\u00e9 GC-100"
         },
         "itach": {
-          "config_flow": false,
-          "iot_class": "assumed_state",
           "integration_type": "hub",
+          "iot_class": "assumed_state",
           "name": "Global Cach\u00e9 iTach TCP/IP to IR"
         }
       }
@@ -1942,86 +1915,79 @@
       "name": "Google",
       "integrations": {
         "google_assistant": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Google Assistant"
         },
         "google_cloud": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Google Cloud Platform"
         },
         "google_domains": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Google Domains"
         },
         "google_maps": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Google Maps"
         },
         "google_pubsub": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Google Pub/Sub"
         },
         "google_sheets": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Google Sheets"
         },
         "google_translate": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Google Translate Text-to-Speech"
         },
         "google_travel_time": {
+          "integration_type": "hub",
           "config_flow": true,
-          "iot_class": "cloud_polling",
-          "integration_type": "hub"
+          "iot_class": "cloud_polling"
         },
         "google_wifi": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Google Wifi"
         },
         "google": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Google Calendar"
         },
         "nest": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "Google Nest"
         },
         "cast": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Google Cast"
         },
         "hangouts": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "Google Chat"
         },
         "dialogflow": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "Dialogflow"
         }
       }
@@ -2163,15 +2129,13 @@
       "name": "Hikvision",
       "integrations": {
         "hikvision": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "Hikvision"
         },
         "hikvisioncam": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Hikvision"
         }
       }
@@ -2237,15 +2201,14 @@
       "name": "Homematic",
       "integrations": {
         "homematic": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "Homematic"
         },
         "homematicip_cloud": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "HomematicIP Cloud"
         }
       }
@@ -2260,21 +2223,20 @@
       "name": "Honeywell",
       "integrations": {
         "lyric": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Honeywell Lyric"
         },
         "evohome": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Honeywell Total Connect Comfort (Europe)"
         },
         "honeywell": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Honeywell Total Connect Comfort (US)"
         }
       }
@@ -2360,15 +2322,13 @@
       "name": "IBM",
       "integrations": {
         "watson_iot": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "IBM Watson IoT Platform"
         },
         "watson_tts": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "IBM Watson TTS"
         }
       }
@@ -2769,21 +2729,20 @@
       "name": "LG",
       "integrations": {
         "lg_netcast": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "LG Netcast"
         },
         "lg_soundbar": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "LG Soundbars"
         },
         "webostv": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "LG webOS Smart TV"
         }
       }
@@ -2899,21 +2858,20 @@
       "name": "Logitech",
       "integrations": {
         "harmony": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Logitech Harmony Hub"
         },
         "ue_smart_radio": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Logitech UE Smart Radio"
         },
         "squeezebox": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Squeezebox (Logitech Media Server)"
         }
       }
@@ -2952,21 +2910,19 @@
       "name": "Lutron",
       "integrations": {
         "lutron": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Lutron"
         },
         "lutron_caseta": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Lutron Cas\u00e9ta"
         },
         "homeworks": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "Lutron Homeworks"
         }
       }
@@ -3073,15 +3029,14 @@
       "name": "Melnor",
       "integrations": {
         "melnor": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Melnor Bluetooth"
         },
         "raincloud": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Melnor RainCloud"
         }
       }
@@ -3144,63 +3099,56 @@
       "name": "Microsoft",
       "integrations": {
         "azure_devops": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Azure DevOps"
         },
         "azure_event_hub": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "Azure Event Hub"
         },
         "azure_service_bus": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Azure Service Bus"
         },
         "microsoft_face_detect": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Microsoft Face Detect"
         },
         "microsoft_face_identify": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Microsoft Face Identify"
         },
         "microsoft_face": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Microsoft Face"
         },
         "microsoft": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Microsoft Text-to-Speech (TTS)"
         },
         "msteams": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Microsoft Teams"
         },
         "xbox": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Xbox"
         },
         "xbox_live": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Xbox Live"
         }
       }
@@ -3326,39 +3274,34 @@
       "name": "MQTT",
       "integrations": {
         "manual_mqtt": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "Manual MQTT Alarm Control Panel"
         },
         "mqtt": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "MQTT"
         },
         "mqtt_eventstream": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "MQTT Eventstream"
         },
         "mqtt_json": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "MQTT JSON"
         },
         "mqtt_room": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "MQTT Room Presence"
         },
         "mqtt_statestream": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "MQTT Statestream"
         }
       }
@@ -3469,15 +3412,14 @@
       "name": "NETGEAR",
       "integrations": {
         "netgear": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "NETGEAR"
         },
         "netgear_lte": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "NETGEAR LTE"
         }
       }
@@ -3837,15 +3779,13 @@
       "name": "OpenWrt",
       "integrations": {
         "luci": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "OpenWrt (luci)"
         },
         "ubus": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "OpenWrt (ubus)"
         }
       }
@@ -3914,15 +3854,14 @@
       "name": "Panasonic",
       "integrations": {
         "panasonic_bluray": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Panasonic Blu-Ray Player"
         },
         "panasonic_viera": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Panasonic Viera"
         }
       }
@@ -3970,21 +3909,21 @@
       "name": "Philips",
       "integrations": {
         "dynalite": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Philips Dynalite"
         },
         "hue": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Philips Hue"
         },
         "philips_js": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Philips TV"
         }
       }
@@ -4215,15 +4154,14 @@
       "name": "QNAP",
       "integrations": {
         "qnap": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "QNAP"
         },
         "qnap_qsw": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "QNAP QSW"
         }
       }
@@ -4304,20 +4242,18 @@
       "name": "Raspberry Pi",
       "integrations": {
         "rpi_camera": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Raspberry Pi Camera"
         },
         "rpi_power": {
+          "integration_type": "hub",
           "config_flow": true,
-          "iot_class": "local_polling",
-          "integration_type": "hub"
+          "iot_class": "local_polling"
         },
         "remote_rpi_gpio": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "remote_rpi_gpio"
         }
       }
@@ -4520,15 +4456,13 @@
       "name": "Russound",
       "integrations": {
         "russound_rio": {
-          "config_flow": false,
-          "iot_class": "local_push",
           "integration_type": "hub",
+          "iot_class": "local_push",
           "name": "Russound RIO"
         },
         "russound_rnet": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Russound RNET"
         }
       }
@@ -4549,21 +4483,20 @@
       "name": "Samsung",
       "integrations": {
         "familyhub": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Samsung Family Hub"
         },
         "samsungtv": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Samsung Smart TV"
         },
         "syncthru": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Samsung SyncThru Printer"
         }
       }
@@ -4925,15 +4858,14 @@
       "name": "SolarEdge",
       "integrations": {
         "solaredge": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "SolarEdge"
         },
         "solaredge_local": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "SolarEdge Local"
         }
       }
@@ -4983,27 +4915,26 @@
       "name": "Sony",
       "integrations": {
         "braviatv": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Sony Bravia TV"
         },
         "ps4": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Sony PlayStation 4"
         },
         "sony_projector": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Sony Projector"
         },
         "songpal": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Sony Songpal"
         }
       }
@@ -5215,21 +5146,19 @@
       "name": "Synology",
       "integrations": {
         "synology_chat": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Synology Chat"
         },
         "synology_dsm": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Synology DSM"
         },
         "synology_srm": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Synology SRM"
         }
       }
@@ -5319,15 +5248,13 @@
       "name": "Telegram",
       "integrations": {
         "telegram": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Telegram"
         },
         "telegram_bot": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Telegram bot"
         }
       }
@@ -5336,15 +5263,14 @@
       "name": "Telldus",
       "integrations": {
         "tellduslive": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Telldus Live"
         },
         "tellstick": {
-          "config_flow": false,
-          "iot_class": "assumed_state",
           "integration_type": "hub",
+          "iot_class": "assumed_state",
           "name": "TellStick"
         }
       }
@@ -5377,15 +5303,15 @@
       "name": "Tesla",
       "integrations": {
         "powerwall": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Tesla Powerwall"
         },
         "tesla_wall_connector": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Tesla Wall Connector"
         }
       }
@@ -5567,21 +5493,21 @@
       "name": "Trafikverket",
       "integrations": {
         "trafikverket_ferry": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Trafikverket Ferry"
         },
         "trafikverket_train": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Trafikverket Train"
         },
         "trafikverket_weatherstation": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Trafikverket Weather Station"
         }
       }
@@ -5626,21 +5552,19 @@
       "name": "Twilio",
       "integrations": {
         "twilio": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "Twilio"
         },
         "twilio_call": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Twilio Call"
         },
         "twilio_sms": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Twilio SMS"
         }
       }
@@ -5673,27 +5597,25 @@
       "name": "Ubiquiti",
       "integrations": {
         "unifi": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "UniFi Network"
         },
         "unifi_direct": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "UniFi AP"
         },
         "unifiled": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "UniFi LED"
         },
         "unifiprotect": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "UniFi Protect"
         }
       }
@@ -5862,15 +5784,14 @@
       "name": "VideoLAN",
       "integrations": {
         "vlc": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "VLC media player"
         },
         "vlc_telnet": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "VLC media player via Telnet"
         }
       }
@@ -6069,33 +5990,31 @@
       "name": "Xiaomi",
       "integrations": {
         "xiaomi_aqara": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Xiaomi Gateway (Aqara)"
         },
         "xiaomi_ble": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Xiaomi BLE"
         },
         "xiaomi_miio": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_polling",
-          "integration_type": "hub",
           "name": "Xiaomi Miio"
         },
         "xiaomi_tv": {
-          "config_flow": false,
-          "iot_class": "assumed_state",
           "integration_type": "hub",
+          "iot_class": "assumed_state",
           "name": "Xiaomi TV"
         },
         "xiaomi": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Xiaomi"
         }
       }
@@ -6116,21 +6035,21 @@
       "name": "Yale",
       "integrations": {
         "august": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_push",
-          "integration_type": "hub",
           "name": "August"
         },
         "yale_smart_alarm": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "cloud_polling",
-          "integration_type": "hub",
           "name": "Yale Smart Living"
         },
         "yalexs_ble": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Yale Access Bluetooth"
         }
       }
@@ -6151,15 +6070,13 @@
       "name": "Yandex",
       "integrations": {
         "yandex_transport": {
-          "config_flow": false,
-          "iot_class": "cloud_polling",
           "integration_type": "hub",
+          "iot_class": "cloud_polling",
           "name": "Yandex Transport"
         },
         "yandextts": {
-          "config_flow": false,
-          "iot_class": "cloud_push",
           "integration_type": "hub",
+          "iot_class": "cloud_push",
           "name": "Yandex TTS"
         }
       }
@@ -6168,15 +6085,14 @@
       "name": "Yeelight",
       "integrations": {
         "yeelight": {
+          "integration_type": "hub",
           "config_flow": true,
           "iot_class": "local_push",
-          "integration_type": "hub",
           "name": "Yeelight"
         },
         "yeelightsunflower": {
-          "config_flow": false,
-          "iot_class": "local_polling",
           "integration_type": "hub",
+          "iot_class": "local_polling",
           "name": "Yeelight Sunflower"
         }
       }
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index 3fcf946bc32..fdf8ac0474d 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -106,10 +106,13 @@ def _populate_brand_integrations(
         integration = integrations.get(domain)
         if not integration or integration.integration_type in ("entity", "system"):
             continue
-        metadata = {}
-        metadata["config_flow"] = integration.config_flow
-        metadata["iot_class"] = integration.iot_class
-        metadata["integration_type"] = integration.integration_type
+        metadata = {
+            "integration_type": integration.integration_type,
+        }
+        if integration.config_flow:
+            metadata["config_flow"] = integration.config_flow
+        if integration.iot_class:
+            metadata["iot_class"] = integration.iot_class
         if integration.translated_name:
             integration_data["translated_name"].add(domain)
         else:
-- 
GitLab


From 43d43689f7066bd97eeb28dc1d0bd50a4e912fa1 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
Date: Fri, 21 Oct 2022 20:29:01 -0400
Subject: [PATCH 682/985] Add ultraloq virtual integration (#80755)

---
 homeassistant/brands/u_tec.json                 |  2 +-
 homeassistant/components/ultraloq/manifest.json |  6 ++++++
 homeassistant/generated/integrations.json       | 11 ++++++++---
 3 files changed, 15 insertions(+), 4 deletions(-)
 create mode 100644 homeassistant/components/ultraloq/manifest.json

diff --git a/homeassistant/brands/u_tec.json b/homeassistant/brands/u_tec.json
index 2ce4be9a7d9..f0c2cf8a691 100644
--- a/homeassistant/brands/u_tec.json
+++ b/homeassistant/brands/u_tec.json
@@ -1,5 +1,5 @@
 {
   "domain": "u_tec",
   "name": "U-tec",
-  "iot_standards": ["zwave"]
+  "integrations": ["ultraloq"]
 }
diff --git a/homeassistant/components/ultraloq/manifest.json b/homeassistant/components/ultraloq/manifest.json
new file mode 100644
index 00000000000..53e4efc99da
--- /dev/null
+++ b/homeassistant/components/ultraloq/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "ultraloq",
+  "name": "Ultraloq",
+  "integration_type": "virtual",
+  "iot_standard": "zwave"
+}
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 162431052be..5505fa7de3f 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -5589,9 +5589,14 @@
     },
     "u_tec": {
       "name": "U-tec",
-      "iot_standards": [
-        "zwave"
-      ]
+      "integrations": {
+        "ultraloq": {
+          "config_flow": false,
+          "iot_class": null,
+          "integration_type": "virtual",
+          "name": "Ultraloq"
+        }
+      }
     },
     "ubiquiti": {
       "name": "Ubiquiti",
-- 
GitLab


From 1ac2d4ae74bc3390ab14496b5664183ad9b99bd2 Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
Date: Sat, 22 Oct 2022 02:29:28 +0200
Subject: [PATCH 683/985] Update frontend to 20221021.0 (#80751)

---
 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 1c8a6465681..0deb091b4c4 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==20221020.0"],
+  "requirements": ["home-assistant-frontend==20221021.0"],
   "dependencies": [
     "api",
     "auth",
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index f65c14d3c17..28d9cf02d9e 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -21,7 +21,7 @@ dbus-fast==1.47.0
 fnvhash==0.1.0
 hass-nabucasa==0.56.0
 home-assistant-bluetooth==1.6.0
-home-assistant-frontend==20221020.0
+home-assistant-frontend==20221021.0
 httpx==0.23.0
 ifaddr==0.1.7
 jinja2==3.1.2
diff --git a/requirements_all.txt b/requirements_all.txt
index 700813104f4..0302b1b60de 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==20221020.0
+home-assistant-frontend==20221021.0
 
 # homeassistant.components.home_connect
 homeconnect==0.7.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index c5ef4f3c0f8..cd530443971 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==20221020.0
+home-assistant-frontend==20221021.0
 
 # homeassistant.components.home_connect
 homeconnect==0.7.2
-- 
GitLab


From 5a77c8f96f310b42fd92e4b7449015dd42a3a829 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Sat, 22 Oct 2022 00:35:23 +0000
Subject: [PATCH 684/985] [ci skip] Translation update

---
 .../airthings_ble/translations/pt-BR.json      |  8 ++++----
 .../components/apcupsd/translations/pt-BR.json |  6 +++---
 .../components/coinbase/translations/nl.json   |  5 +++++
 .../dsmr_reader/translations/pt-BR.json        |  2 +-
 .../google_sheets/translations/pt-BR.json      | 10 +++++-----
 .../google_travel_time/translations/pt.json    |  3 +++
 .../huawei_lte/translations/pt-BR.json         |  2 +-
 .../components/ibeacon/translations/pt-BR.json |  2 +-
 .../components/kegtron/translations/pt-BR.json |  4 ++--
 .../keymitt_ble/translations/pt-BR.json        |  4 ++--
 .../components/lametric/translations/pt.json   |  3 ++-
 .../components/lidarr/translations/pt-BR.json  |  8 ++++----
 .../components/mikrotik/translations/nl.json   |  1 +
 .../nibe_heatpump/translations/pt-BR.json      |  2 +-
 .../octoprint/translations/pt-BR.json          |  2 +-
 .../plugwise/translations/select.ca.json       |  6 ++++++
 .../plugwise/translations/select.hu.json       |  6 ++++++
 .../plugwise/translations/select.it.json       |  5 +++++
 .../plugwise/translations/select.pl.json       |  2 +-
 .../plugwise/translations/select.pt-BR.json    |  6 ++++++
 .../components/radarr/translations/nl.json     | 11 ++++++++++-
 .../components/radarr/translations/pt-BR.json  |  6 +++---
 .../components/roomba/translations/pt.json     |  8 +++++---
 .../components/shelly/translations/pt-BR.json  |  2 +-
 .../components/snooz/translations/pt-BR.json   |  4 ++--
 .../components/somfy/translations/bg.json      | 17 +++++++++++++++++
 .../components/somfy/translations/ca.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/cs.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/da.json      | 16 ++++++++++++++++
 .../components/somfy/translations/de.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/el.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/en.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/en_GB.json   |  7 +++++++
 .../components/somfy/translations/es-419.json  | 16 ++++++++++++++++
 .../components/somfy/translations/es.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/et.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/fr.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/he.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/hr.json      |  7 +++++++
 .../components/somfy/translations/hu.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/id.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/it.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/ja.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/ko.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/lb.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/nl.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/no.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/pl.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/pt-BR.json   | 18 ++++++++++++++++++
 .../components/somfy/translations/pt.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/ru.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/sk.json      |  7 +++++++
 .../components/somfy/translations/sl.json      | 16 ++++++++++++++++
 .../components/somfy/translations/sv.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/tr.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/uk.json      | 18 ++++++++++++++++++
 .../components/somfy/translations/zh-Hant.json | 18 ++++++++++++++++++
 .../switchbee/translations/pt-BR.json          |  6 +++---
 .../components/uptime/translations/nl.json     |  5 +++++
 .../xiaomi_miio/translations/de.json           |  3 ++-
 .../xiaomi_miio/translations/es.json           |  3 ++-
 .../xiaomi_miio/translations/hu.json           |  3 ++-
 .../xiaomi_miio/translations/it.json           |  3 ++-
 .../xiaomi_miio/translations/pl.json           |  3 ++-
 .../xiaomi_miio/translations/pt-BR.json        |  3 ++-
 65 files changed, 637 insertions(+), 46 deletions(-)
 create mode 100644 homeassistant/components/somfy/translations/bg.json
 create mode 100644 homeassistant/components/somfy/translations/ca.json
 create mode 100644 homeassistant/components/somfy/translations/cs.json
 create mode 100644 homeassistant/components/somfy/translations/da.json
 create mode 100644 homeassistant/components/somfy/translations/de.json
 create mode 100644 homeassistant/components/somfy/translations/el.json
 create mode 100644 homeassistant/components/somfy/translations/en.json
 create mode 100644 homeassistant/components/somfy/translations/en_GB.json
 create mode 100644 homeassistant/components/somfy/translations/es-419.json
 create mode 100644 homeassistant/components/somfy/translations/es.json
 create mode 100644 homeassistant/components/somfy/translations/et.json
 create mode 100644 homeassistant/components/somfy/translations/fr.json
 create mode 100644 homeassistant/components/somfy/translations/he.json
 create mode 100644 homeassistant/components/somfy/translations/hr.json
 create mode 100644 homeassistant/components/somfy/translations/hu.json
 create mode 100644 homeassistant/components/somfy/translations/id.json
 create mode 100644 homeassistant/components/somfy/translations/it.json
 create mode 100644 homeassistant/components/somfy/translations/ja.json
 create mode 100644 homeassistant/components/somfy/translations/ko.json
 create mode 100644 homeassistant/components/somfy/translations/lb.json
 create mode 100644 homeassistant/components/somfy/translations/nl.json
 create mode 100644 homeassistant/components/somfy/translations/no.json
 create mode 100644 homeassistant/components/somfy/translations/pl.json
 create mode 100644 homeassistant/components/somfy/translations/pt-BR.json
 create mode 100644 homeassistant/components/somfy/translations/pt.json
 create mode 100644 homeassistant/components/somfy/translations/ru.json
 create mode 100644 homeassistant/components/somfy/translations/sk.json
 create mode 100644 homeassistant/components/somfy/translations/sl.json
 create mode 100644 homeassistant/components/somfy/translations/sv.json
 create mode 100644 homeassistant/components/somfy/translations/tr.json
 create mode 100644 homeassistant/components/somfy/translations/uk.json
 create mode 100644 homeassistant/components/somfy/translations/zh-Hant.json

diff --git a/homeassistant/components/airthings_ble/translations/pt-BR.json b/homeassistant/components/airthings_ble/translations/pt-BR.json
index a5093f346c1..bbce7b1219a 100644
--- a/homeassistant/components/airthings_ble/translations/pt-BR.json
+++ b/homeassistant/components/airthings_ble/translations/pt-BR.json
@@ -1,9 +1,9 @@
 {
     "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",
+            "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado",
+            "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
+            "cannot_connect": "Falha ao conectar",
             "no_devices_found": "Nenhum dispositivo encontrado na rede",
             "unknown": "Erro inesperado"
         },
@@ -16,7 +16,7 @@
                 "data": {
                     "address": "Dispositivo"
                 },
-                "description": "Saiba como funciona"
+                "description": "Escolha um dispositivo para configurar"
             }
         }
     }
diff --git a/homeassistant/components/apcupsd/translations/pt-BR.json b/homeassistant/components/apcupsd/translations/pt-BR.json
index d8792506772..44c7673489c 100644
--- a/homeassistant/components/apcupsd/translations/pt-BR.json
+++ b/homeassistant/components/apcupsd/translations/pt-BR.json
@@ -1,16 +1,16 @@
 {
     "config": {
         "abort": {
-            "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
+            "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado",
             "no_status": "Nenhum status \u00e9 relatado de Nome do host"
         },
         "error": {
-            "cannot_connect": "Falhou ao conectar"
+            "cannot_connect": "Falha ao conectar"
         },
         "step": {
             "user": {
                 "data": {
-                    "host": "Host",
+                    "host": "Nome do host",
                     "port": "Porta"
                 },
                 "description": "Insira o host e a porta em que o NIS apcupsd est\u00e1 sendo servido."
diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json
index 472a15659c0..5c47abfebbd 100644
--- a/homeassistant/components/coinbase/translations/nl.json
+++ b/homeassistant/components/coinbase/translations/nl.json
@@ -21,6 +21,11 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "title": "De Coinbase YAML-configuratie is verwijderd"
+        }
+    },
     "options": {
         "error": {
             "currency_unavailable": "Een of meer van de gevraagde valutabalansen wordt niet geleverd door uw Coinbase API.",
diff --git a/homeassistant/components/dsmr_reader/translations/pt-BR.json b/homeassistant/components/dsmr_reader/translations/pt-BR.json
index 292ef5b59fa..5980921027a 100644
--- a/homeassistant/components/dsmr_reader/translations/pt-BR.json
+++ b/homeassistant/components/dsmr_reader/translations/pt-BR.json
@@ -1,7 +1,7 @@
 {
     "config": {
         "abort": {
-            "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel."
+            "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel."
         },
         "step": {
             "confirm": {
diff --git a/homeassistant/components/google_sheets/translations/pt-BR.json b/homeassistant/components/google_sheets/translations/pt-BR.json
index 870a7ef267e..acc438b37d5 100644
--- a/homeassistant/components/google_sheets/translations/pt-BR.json
+++ b/homeassistant/components/google_sheets/translations/pt-BR.json
@@ -4,16 +4,16 @@
     },
     "config": {
         "abort": {
-            "already_configured": "A conta j\u00e1 est\u00e1 configurada",
-            "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
-            "cannot_connect": "Falhou ao conectar",
+            "already_configured": "A conta j\u00e1 foi configurada",
+            "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
+            "cannot_connect": "Falha ao conectar",
             "create_spreadsheet_failure": "Erro ao criar planilha, veja o log de erros para detalhes",
             "invalid_access_token": "Token de acesso inv\u00e1lido",
             "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.",
-            "oauth_error": "Dados de token inv\u00e1lidos recebidos.",
+            "oauth_error": "Dados de token recebidos inv\u00e1lidos.",
             "open_spreadsheet_failure": "Erro ao abrir a planilha, veja o log de erros para detalhes",
             "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida",
-            "timeout_connect": "Tempo limite estabelecendo conex\u00e3o",
+            "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido",
             "unknown": "Erro inesperado"
         },
         "create_entry": {
diff --git a/homeassistant/components/google_travel_time/translations/pt.json b/homeassistant/components/google_travel_time/translations/pt.json
index 286cd58dd89..c61bf4d3ca4 100644
--- a/homeassistant/components/google_travel_time/translations/pt.json
+++ b/homeassistant/components/google_travel_time/translations/pt.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "error": {
+            "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida"
+        },
         "step": {
             "user": {
                 "data": {
diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json
index c9c453d69f2..d10fb60a013 100644
--- a/homeassistant/components/huawei_lte/translations/pt-BR.json
+++ b/homeassistant/components/huawei_lte/translations/pt-BR.json
@@ -19,7 +19,7 @@
             "reauth_confirm": {
                 "data": {
                     "password": "Senha",
-                    "username": "Nome de usu\u00e1rio"
+                    "username": "Usu\u00e1rio"
                 },
                 "description": "Insira as credenciais de acesso ao dispositivo.",
                 "title": "Reautenticar Integra\u00e7\u00e3o"
diff --git a/homeassistant/components/ibeacon/translations/pt-BR.json b/homeassistant/components/ibeacon/translations/pt-BR.json
index 0dfe8a4d8cd..71667967746 100644
--- a/homeassistant/components/ibeacon/translations/pt-BR.json
+++ b/homeassistant/components/ibeacon/translations/pt-BR.json
@@ -2,7 +2,7 @@
     "config": {
         "abort": {
             "bluetooth_not_available": "Pelo menos um adaptador ou controle remoto Bluetooth deve ser configurado para usar o iBeacon Tracker.",
-            "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel."
+            "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel."
         },
         "step": {
             "user": {
diff --git a/homeassistant/components/kegtron/translations/pt-BR.json b/homeassistant/components/kegtron/translations/pt-BR.json
index 0da7639fa2a..5b654163201 100644
--- a/homeassistant/components/kegtron/translations/pt-BR.json
+++ b/homeassistant/components/kegtron/translations/pt-BR.json
@@ -1,8 +1,8 @@
 {
     "config": {
         "abort": {
-            "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
-            "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
+            "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado",
+            "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
             "no_devices_found": "Nenhum dispositivo encontrado na rede",
             "not_supported": "Dispositivo n\u00e3o suportado"
         },
diff --git a/homeassistant/components/keymitt_ble/translations/pt-BR.json b/homeassistant/components/keymitt_ble/translations/pt-BR.json
index 66e44612afe..a04c7d7f90f 100644
--- a/homeassistant/components/keymitt_ble/translations/pt-BR.json
+++ b/homeassistant/components/keymitt_ble/translations/pt-BR.json
@@ -1,8 +1,8 @@
 {
     "config": {
         "abort": {
-            "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado",
-            "cannot_connect": "Falhou ao conectar",
+            "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado",
+            "cannot_connect": "Falha ao conectar",
             "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.",
             "unknown": "Erro inesperado"
         },
diff --git a/homeassistant/components/lametric/translations/pt.json b/homeassistant/components/lametric/translations/pt.json
index ca715b2e6e2..74a501a1ca8 100644
--- a/homeassistant/components/lametric/translations/pt.json
+++ b/homeassistant/components/lametric/translations/pt.json
@@ -4,7 +4,8 @@
             "invalid_discovery_info": "Informa\u00e7\u00f5es de descoberta inv\u00e1lidas recebidas",
             "link_local_address": "Endere\u00e7os locais de link n\u00e3o s\u00e3o suportados",
             "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.",
-            "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric"
+            "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric",
+            "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida"
         },
         "step": {
             "choice_enter_manual_or_fetch_cloud": {
diff --git a/homeassistant/components/lidarr/translations/pt-BR.json b/homeassistant/components/lidarr/translations/pt-BR.json
index 9390e86b497..5d9b99704c4 100644
--- a/homeassistant/components/lidarr/translations/pt-BR.json
+++ b/homeassistant/components/lidarr/translations/pt-BR.json
@@ -5,7 +5,7 @@
             "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida"
         },
         "error": {
-            "cannot_connect": "Falhou ao conectar",
+            "cannot_connect": "Falha ao conectar",
             "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
             "unknown": "Erro inesperado",
             "wrong_app": "Aplica\u00e7\u00e3o incorreta alcan\u00e7ada. Por favor, tente novamente",
@@ -14,16 +14,16 @@
         "step": {
             "reauth_confirm": {
                 "data": {
-                    "api_key": "Chave de API"
+                    "api_key": "Chave da API"
                 },
                 "description": "A integra\u00e7\u00e3o do Lidarr precisa ser autenticada manualmente com a API do Lidarr",
                 "title": "Reautenticar Integra\u00e7\u00e3o"
             },
             "user": {
                 "data": {
-                    "api_key": "Chave de API",
+                    "api_key": "Chave da API",
                     "url": "URL",
-                    "verify_ssl": "Verificar certificado SSL"
+                    "verify_ssl": "Verifique o certificado SSL"
                 },
                 "description": "A chave de API pode ser recuperada automaticamente se as credenciais de login n\u00e3o tiverem sido definidas no aplicativo.\n Sua chave de API pode ser encontrada em Configura\u00e7\u00f5es > Geral na IU da Web do Lidarr."
             }
diff --git a/homeassistant/components/mikrotik/translations/nl.json b/homeassistant/components/mikrotik/translations/nl.json
index daa70c9e3a1..a85a272b457 100644
--- a/homeassistant/components/mikrotik/translations/nl.json
+++ b/homeassistant/components/mikrotik/translations/nl.json
@@ -14,6 +14,7 @@
                 "data": {
                     "password": "Wachtwoord"
                 },
+                "description": "Het wachtwoord voor {username} is onjuist.",
                 "title": "Integratie herauthenticeren"
             },
             "user": {
diff --git a/homeassistant/components/nibe_heatpump/translations/pt-BR.json b/homeassistant/components/nibe_heatpump/translations/pt-BR.json
index 127f6c6010b..b20ab8acbf8 100644
--- a/homeassistant/components/nibe_heatpump/translations/pt-BR.json
+++ b/homeassistant/components/nibe_heatpump/translations/pt-BR.json
@@ -1,7 +1,7 @@
 {
     "config": {
         "abort": {
-            "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado"
+            "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado"
         },
         "error": {
             "address": "Endere\u00e7o IP remoto inv\u00e1lido especificado. O endere\u00e7o deve ser um endere\u00e7o IPV4.",
diff --git a/homeassistant/components/octoprint/translations/pt-BR.json b/homeassistant/components/octoprint/translations/pt-BR.json
index 39cc45bb8a6..577e96495fb 100644
--- a/homeassistant/components/octoprint/translations/pt-BR.json
+++ b/homeassistant/components/octoprint/translations/pt-BR.json
@@ -18,7 +18,7 @@
         "step": {
             "reauth_confirm": {
                 "data": {
-                    "username": "Nome de usu\u00e1rio"
+                    "username": "Usu\u00e1rio"
                 }
             },
             "user": {
diff --git a/homeassistant/components/plugwise/translations/select.ca.json b/homeassistant/components/plugwise/translations/select.ca.json
index e7caf47404c..a5b699c36f1 100644
--- a/homeassistant/components/plugwise/translations/select.ca.json
+++ b/homeassistant/components/plugwise/translations/select.ca.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Autom\u00e0tic",
+            "boost": "Incrementat",
+            "comfort": "Confort",
+            "off": "OFF"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "Molt fred",
             "bleeding_hot": "Molt calent",
diff --git a/homeassistant/components/plugwise/translations/select.hu.json b/homeassistant/components/plugwise/translations/select.hu.json
index 2d614743f16..18480d95f63 100644
--- a/homeassistant/components/plugwise/translations/select.hu.json
+++ b/homeassistant/components/plugwise/translations/select.hu.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Automatikus",
+            "boost": "Turb\u00f3",
+            "comfort": "Komfort",
+            "off": "Ki"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "J\u00e9ghideg",
             "bleeding_hot": "Forr\u00f3",
diff --git a/homeassistant/components/plugwise/translations/select.it.json b/homeassistant/components/plugwise/translations/select.it.json
index 64f15b3915b..9cbe0d8fdb9 100644
--- a/homeassistant/components/plugwise/translations/select.it.json
+++ b/homeassistant/components/plugwise/translations/select.it.json
@@ -1,5 +1,10 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Automatico",
+            "comfort": "Comfort",
+            "off": "Spento"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "Sfiatamento freddo",
             "bleeding_hot": "Sfiatamento caldo",
diff --git a/homeassistant/components/plugwise/translations/select.pl.json b/homeassistant/components/plugwise/translations/select.pl.json
index 92159b0fab2..d17e5788de0 100644
--- a/homeassistant/components/plugwise/translations/select.pl.json
+++ b/homeassistant/components/plugwise/translations/select.pl.json
@@ -1,7 +1,7 @@
 {
     "state": {
         "plugwise__dhw_mode": {
-            "auto": "automatycznie",
+            "auto": "auto",
             "boost": "dogrzewanie",
             "comfort": "komfortowo",
             "off": "wy\u0142\u0105czone"
diff --git a/homeassistant/components/plugwise/translations/select.pt-BR.json b/homeassistant/components/plugwise/translations/select.pt-BR.json
index 578a7c7d488..4804fca07e2 100644
--- a/homeassistant/components/plugwise/translations/select.pt-BR.json
+++ b/homeassistant/components/plugwise/translations/select.pt-BR.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "Autom\u00e1tico",
+            "boost": "Impulso",
+            "comfort": "Conforto",
+            "off": "Desligado"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "Frio congelante",
             "bleeding_hot": "Queimando quente",
diff --git a/homeassistant/components/radarr/translations/nl.json b/homeassistant/components/radarr/translations/nl.json
index 436d0998a9e..b4f1fcd7106 100644
--- a/homeassistant/components/radarr/translations/nl.json
+++ b/homeassistant/components/radarr/translations/nl.json
@@ -7,7 +7,8 @@
         "error": {
             "cannot_connect": "Kan geen verbinding maken",
             "invalid_auth": "Ongeldige authenticatie",
-            "unknown": "Onverwachte fout"
+            "unknown": "Onverwachte fout",
+            "zeroconf_failed": "API-sleutel niet gevonden. Voer het handmatig in"
         },
         "step": {
             "reauth_confirm": {
@@ -21,5 +22,13 @@
                 }
             }
         }
+    },
+    "issues": {
+        "deprecated_yaml": {
+            "title": "De Radarr YAML-configuratie wordt verwijderd"
+        },
+        "removed_attributes": {
+            "title": "Wijzigingen in de Radarr-integratie"
+        }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/radarr/translations/pt-BR.json b/homeassistant/components/radarr/translations/pt-BR.json
index 74d33fa6136..78b9078cb9f 100644
--- a/homeassistant/components/radarr/translations/pt-BR.json
+++ b/homeassistant/components/radarr/translations/pt-BR.json
@@ -5,7 +5,7 @@
             "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida"
         },
         "error": {
-            "cannot_connect": "Falhou ao conectar",
+            "cannot_connect": "Falha ao conectar",
             "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
             "unknown": "Erro inesperado",
             "wrong_app": "Aplica\u00e7\u00e3o incorreta alcan\u00e7ada. Por favor, tente novamente",
@@ -18,9 +18,9 @@
             },
             "user": {
                 "data": {
-                    "api_key": "Chave de API",
+                    "api_key": "Chave da API",
                     "url": "URL",
-                    "verify_ssl": "Verificar certificado SSL"
+                    "verify_ssl": "Verifique o certificado SSL"
                 },
                 "description": "A chave de API pode ser recuperada automaticamente se as credenciais de login n\u00e3o tiverem sido definidas no aplicativo.\n Sua chave de API pode ser encontrada em Configura\u00e7\u00f5es > Geral na interface da Web do Radarr."
             }
diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json
index 5e40221cec6..fc6cc956c6d 100644
--- a/homeassistant/components/roomba/translations/pt.json
+++ b/homeassistant/components/roomba/translations/pt.json
@@ -1,20 +1,22 @@
 {
     "config": {
         "abort": {
-            "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot"
+            "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um iRobot"
         },
         "error": {
             "cannot_connect": "Falha na liga\u00e7\u00e3o"
         },
-        "flow_title": "iRobot {name} ({host})",
+        "flow_title": "{name} ({host})",
         "step": {
             "link": {
+                "description": "Por favor, garanta que a app iRobot Home n\u00e3o est\u00e1 aberta em nenhum dispositivo enquanto tenta obter a palavra-passe. Prima e mantenha o bot\u00e3o Home em {name} at\u00e9 o dispositivo emitir um som (cerca de 2 segundos), e clique em Enviar nos 30 segundos seguintes.",
                 "title": "Recuperar Palavra-passe"
             },
             "link_manual": {
                 "data": {
                     "password": "Palavra-passe"
-                }
+                },
+                "description": "A palavra-passe n\u00e3o p\u00f4de ser obtida automaticamente a partir do dispositivo. Por favor, garanta que a app iRobot Home n\u00e3o est\u00e1 aberta em nenhum dispositivo enquanto tenta obter a palavra-passe. Siga os passos descritos na documenta\u00e7\u00e3o em: {auth_help_url}."
             },
             "manual": {
                 "data": {
diff --git a/homeassistant/components/shelly/translations/pt-BR.json b/homeassistant/components/shelly/translations/pt-BR.json
index 6fff6c1ec3c..0a546c9807d 100644
--- a/homeassistant/components/shelly/translations/pt-BR.json
+++ b/homeassistant/components/shelly/translations/pt-BR.json
@@ -26,7 +26,7 @@
             "reauth_confirm": {
                 "data": {
                     "password": "Senha",
-                    "username": "Nome de usu\u00e1rio"
+                    "username": "Usu\u00e1rio"
                 }
             },
             "user": {
diff --git a/homeassistant/components/snooz/translations/pt-BR.json b/homeassistant/components/snooz/translations/pt-BR.json
index 953e074c5aa..a9bf79dad4e 100644
--- a/homeassistant/components/snooz/translations/pt-BR.json
+++ b/homeassistant/components/snooz/translations/pt-BR.json
@@ -1,8 +1,8 @@
 {
     "config": {
         "abort": {
-            "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
-            "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
+            "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado",
+            "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
             "no_devices_found": "Nenhum dispositivo encontrado na rede"
         },
         "flow_title": "{name}",
diff --git a/homeassistant/components/somfy/translations/bg.json b/homeassistant/components/somfy/translations/bg.json
new file mode 100644
index 00000000000..62905ef389e
--- /dev/null
+++ b/homeassistant/components/somfy/translations/bg.json
@@ -0,0 +1,17 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.",
+            "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 Somfy \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 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441\u044a\u0441 Somfy."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/ca.json b/homeassistant/components/somfy/translations/ca.json
new file mode 100644
index 00000000000..bc34c57c939
--- /dev/null
+++ b/homeassistant/components/somfy/translations/ca.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
+            "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
+            "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})",
+            "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+        },
+        "create_entry": {
+            "default": "Autenticaci\u00f3 exitosa"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/cs.json b/homeassistant/components/somfy/translations/cs.json
new file mode 100644
index 00000000000..acc7d260cad
--- /dev/null
+++ b/homeassistant/components/somfy/translations/cs.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
+            "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.",
+            "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})",
+            "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
+        },
+        "create_entry": {
+            "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Vyberte metodu ov\u011b\u0159en\u00ed"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/da.json b/homeassistant/components/somfy/translations/da.json
new file mode 100644
index 00000000000..3b7a79ef008
--- /dev/null
+++ b/homeassistant/components/somfy/translations/da.json
@@ -0,0 +1,16 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Timeout ved generering af autoriseret url.",
+            "missing_configuration": "Komponenten Somfy er ikke konfigureret. F\u00f8lg venligst dokumentationen."
+        },
+        "create_entry": {
+            "default": "Godkendt med Somfy."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "V\u00e6lg godkendelsesmetode"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/de.json b/homeassistant/components/somfy/translations/de.json
new file mode 100644
index 00000000000..29a959f48ce
--- /dev/null
+++ b/homeassistant/components/somfy/translations/de.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.",
+            "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.",
+            "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).",
+            "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
+        },
+        "create_entry": {
+            "default": "Erfolgreich authentifiziert"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "W\u00e4hle die Authentifizierungsmethode"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/el.json b/homeassistant/components/somfy/translations/el.json
new file mode 100644
index 00000000000..8d1f457ae10
--- /dev/null
+++ b/homeassistant/components/somfy/translations/el.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.",
+            "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.",
+            "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )",
+            "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae."
+        },
+        "create_entry": {
+            "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2"
+        },
+        "step": {
+            "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"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/en.json b/homeassistant/components/somfy/translations/en.json
new file mode 100644
index 00000000000..e0072d1da4d
--- /dev/null
+++ b/homeassistant/components/somfy/translations/en.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Timeout generating authorize URL.",
+            "missing_configuration": "The component is not configured. Please follow the documentation.",
+            "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
+            "single_instance_allowed": "Already configured. Only a single configuration possible."
+        },
+        "create_entry": {
+            "default": "Successfully authenticated"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Pick Authentication Method"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/en_GB.json b/homeassistant/components/somfy/translations/en_GB.json
new file mode 100644
index 00000000000..ddf7ee6d5dd
--- /dev/null
+++ b/homeassistant/components/somfy/translations/en_GB.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Timeout generating authorise URL."
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/es-419.json b/homeassistant/components/somfy/translations/es-419.json
new file mode 100644
index 00000000000..6acd9bb6bb8
--- /dev/null
+++ b/homeassistant/components/somfy/translations/es-419.json
@@ -0,0 +1,16 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.",
+            "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n."
+        },
+        "create_entry": {
+            "default": "Autenticado con \u00e9xito con Somfy."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/es.json b/homeassistant/components/somfy/translations/es.json
new file mode 100644
index 00000000000..db3edbd35fd
--- /dev/null
+++ b/homeassistant/components/somfy/translations/es.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.",
+            "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.",
+            "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})",
+            "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
+        },
+        "create_entry": {
+            "default": "Autenticado correctamente"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/et.json b/homeassistant/components/somfy/translations/et.json
new file mode 100644
index 00000000000..9239f7df0ef
--- /dev/null
+++ b/homeassistant/components/somfy/translations/et.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.",
+            "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.",
+            "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})",
+            "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+        },
+        "create_entry": {
+            "default": "Edukalt tuvastatud"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Vali tuvastusmeetod"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/fr.json b/homeassistant/components/somfy/translations/fr.json
new file mode 100644
index 00000000000..0c7a25831bc
--- /dev/null
+++ b/homeassistant/components/somfy/translations/fr.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.",
+            "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.",
+            "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})",
+            "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
+        },
+        "create_entry": {
+            "default": "Authentification r\u00e9ussie"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "S\u00e9lectionner une m\u00e9thode d'authentification"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/he.json b/homeassistant/components/somfy/translations/he.json
new file mode 100644
index 00000000000..c68d7f74d85
--- /dev/null
+++ b/homeassistant/components/somfy/translations/he.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "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.",
+            "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.",
+            "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})",
+            "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."
+        },
+        "create_entry": {
+            "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/hr.json b/homeassistant/components/somfy/translations/hr.json
new file mode 100644
index 00000000000..a601eb2b9bf
--- /dev/null
+++ b/homeassistant/components/somfy/translations/hr.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "create_entry": {
+            "default": "Uspje\u0161no autentificirano sa Somfy."
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json
new file mode 100644
index 00000000000..96b873b2c42
--- /dev/null
+++ b/homeassistant/components/somfy/translations/hu.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.",
+            "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.",
+            "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.",
+            "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+        },
+        "create_entry": {
+            "default": "Sikeres hiteles\u00edt\u00e9s"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/id.json b/homeassistant/components/somfy/translations/id.json
new file mode 100644
index 00000000000..2d229de00d5
--- /dev/null
+++ b/homeassistant/components/somfy/translations/id.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.",
+            "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.",
+            "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})",
+            "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan."
+        },
+        "create_entry": {
+            "default": "Berhasil diautentikasi"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Pilih Metode Autentikasi"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/it.json b/homeassistant/components/somfy/translations/it.json
new file mode 100644
index 00000000000..0201e1e2569
--- /dev/null
+++ b/homeassistant/components/somfy/translations/it.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
+            "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.",
+            "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
+            "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+        },
+        "create_entry": {
+            "default": "Autenticazione riuscita"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Scegli il metodo di autenticazione"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/ja.json b/homeassistant/components/somfy/translations/ja.json
new file mode 100644
index 00000000000..365d3e4b0db
--- /dev/null
+++ b/homeassistant/components/somfy/translations/ja.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002",
+            "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002",
+            "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})",
+            "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"
+        },
+        "create_entry": {
+            "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json
new file mode 100644
index 00000000000..568c8d05116
--- /dev/null
+++ b/homeassistant/components/somfy/translations/ko.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+            "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.",
+            "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
+            "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+        },
+        "create_entry": {
+            "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/lb.json b/homeassistant/components/somfy/translations/lb.json
new file mode 100644
index 00000000000..a463473c2e1
--- /dev/null
+++ b/homeassistant/components/somfy/translations/lb.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.",
+            "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.",
+            "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})",
+            "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+        },
+        "create_entry": {
+            "default": "Erfollegr\u00e4ich authentifiz\u00e9iert."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Wiel Authentifikatiouns Method aus"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/nl.json b/homeassistant/components/somfy/translations/nl.json
new file mode 100644
index 00000000000..efd07952467
--- /dev/null
+++ b/homeassistant/components/somfy/translations/nl.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.",
+            "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.",
+            "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})",
+            "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk."
+        },
+        "create_entry": {
+            "default": "Authenticatie geslaagd"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Kies een authenticatie methode"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/no.json b/homeassistant/components/somfy/translations/no.json
new file mode 100644
index 00000000000..57bc6e68436
--- /dev/null
+++ b/homeassistant/components/somfy/translations/no.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "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})",
+            "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+        },
+        "create_entry": {
+            "default": "Vellykket godkjenning"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Velg godkjenningsmetode"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/pl.json b/homeassistant/components/somfy/translations/pl.json
new file mode 100644
index 00000000000..baeb38e755e
--- /dev/null
+++ b/homeassistant/components/somfy/translations/pl.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
+            "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
+            "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})",
+            "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+        },
+        "create_entry": {
+            "default": "Pomy\u015blnie uwierzytelniono"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Wybierz metod\u0119 uwierzytelniania"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/pt-BR.json b/homeassistant/components/somfy/translations/pt-BR.json
new file mode 100644
index 00000000000..8ad5fac9044
--- /dev/null
+++ b/homeassistant/components/somfy/translations/pt-BR.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.",
+            "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.",
+            "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})",
+            "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel."
+        },
+        "create_entry": {
+            "default": "Autenticado com sucesso"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/pt.json b/homeassistant/components/somfy/translations/pt.json
new file mode 100644
index 00000000000..592ccd85589
--- /dev/null
+++ b/homeassistant/components/somfy/translations/pt.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o",
+            "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.",
+            "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})",
+            "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel."
+        },
+        "create_entry": {
+            "default": "Autenticado com sucesso"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/ru.json b/homeassistant/components/somfy/translations/ru.json
new file mode 100644
index 00000000000..38ac0dda412
--- /dev/null
+++ b/homeassistant/components/somfy/translations/ru.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
+            "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.",
+            "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.",
+            "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
+        },
+        "create_entry": {
+            "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/sk.json b/homeassistant/components/somfy/translations/sk.json
new file mode 100644
index 00000000000..c19b1a0b70c
--- /dev/null
+++ b/homeassistant/components/somfy/translations/sk.json
@@ -0,0 +1,7 @@
+{
+    "config": {
+        "create_entry": {
+            "default": "\u00daspe\u0161ne overen\u00e9"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/sl.json b/homeassistant/components/somfy/translations/sl.json
new file mode 100644
index 00000000000..3b9bc038fe6
--- /dev/null
+++ b/homeassistant/components/somfy/translations/sl.json
@@ -0,0 +1,16 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.",
+            "missing_configuration": "Komponenta Somfy ni konfigurirana. Upo\u0161tevajte dokumentacijo."
+        },
+        "create_entry": {
+            "default": "Uspe\u0161no overjen s Somfy-jem."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Izberite na\u010din preverjanja pristnosti"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/sv.json b/homeassistant/components/somfy/translations/sv.json
new file mode 100644
index 00000000000..d011a16d90b
--- /dev/null
+++ b/homeassistant/components/somfy/translations/sv.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Timeout vid skapandet av en auktoriseringsadress.",
+            "missing_configuration": "Somfy-komponenten \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.",
+            "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})",
+            "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig."
+        },
+        "create_entry": {
+            "default": "Lyckad autentisering med Somfy."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "V\u00e4lj autentiseringsmetod"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/tr.json b/homeassistant/components/somfy/translations/tr.json
new file mode 100644
index 00000000000..b3b645cd52d
--- /dev/null
+++ b/homeassistant/components/somfy/translations/tr.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.",
+            "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.",
+            "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})",
+            "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr."
+        },
+        "create_entry": {
+            "default": "Ba\u015far\u0131yla do\u011fruland\u0131"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/uk.json b/homeassistant/components/somfy/translations/uk.json
new file mode 100644
index 00000000000..207169ad6b0
--- /dev/null
+++ b/homeassistant/components/somfy/translations/uk.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.",
+            "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.",
+            "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.",
+            "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f."
+        },
+        "create_entry": {
+            "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e."
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json
new file mode 100644
index 00000000000..8dccd6771cb
--- /dev/null
+++ b/homeassistant/components/somfy/translations/zh-Hant.json
@@ -0,0 +1,18 @@
+{
+    "config": {
+        "abort": {
+            "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
+            "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+            "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
+            "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
+        },
+        "create_entry": {
+            "default": "\u5df2\u6210\u529f\u8a8d\u8b49"
+        },
+        "step": {
+            "pick_implementation": {
+                "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switchbee/translations/pt-BR.json b/homeassistant/components/switchbee/translations/pt-BR.json
index 2b93ff92ac3..a99ffe41150 100644
--- a/homeassistant/components/switchbee/translations/pt-BR.json
+++ b/homeassistant/components/switchbee/translations/pt-BR.json
@@ -1,17 +1,17 @@
 {
     "config": {
         "abort": {
-            "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado"
+            "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado"
         },
         "error": {
-            "cannot_connect": "Falhou ao se conectar",
+            "cannot_connect": "Falha ao conectar",
             "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida",
             "unknown": "Erro inesperado"
         },
         "step": {
             "user": {
                 "data": {
-                    "host": "Host",
+                    "host": "Nome do host",
                     "password": "Senha",
                     "switch_as_light": "Inicializar switches como entidades de luz",
                     "username": "Nome de usu\u00e1rio"
diff --git a/homeassistant/components/uptime/translations/nl.json b/homeassistant/components/uptime/translations/nl.json
index b4ed0a1db36..bd054cfefc7 100644
--- a/homeassistant/components/uptime/translations/nl.json
+++ b/homeassistant/components/uptime/translations/nl.json
@@ -9,5 +9,10 @@
             }
         }
     },
+    "issues": {
+        "removed_yaml": {
+            "title": "De Uptime YAML-configuratie is verwijderd"
+        }
+    },
     "title": "Uptime"
 }
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json
index 70a630f90f3..aa5ad47dd4c 100644
--- a/homeassistant/components/xiaomi_miio/translations/de.json
+++ b/homeassistant/components/xiaomi_miio/translations/de.json
@@ -5,7 +5,8 @@
             "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
             "incomplete_info": "Unvollst\u00e4ndige Informationen zur Einrichtung des Ger\u00e4ts, kein Host oder Token geliefert.",
             "not_xiaomi_miio": "Ger\u00e4t wird (noch) nicht von Xiaomi Miio unterst\u00fctzt.",
-            "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
+            "reauth_successful": "Die erneute Authentifizierung war erfolgreich",
+            "unknown": "Unerwarteter Fehler"
         },
         "error": {
             "cannot_connect": "Verbindung fehlgeschlagen",
diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json
index 68bdda15a22..3173e888df6 100644
--- a/homeassistant/components/xiaomi_miio/translations/es.json
+++ b/homeassistant/components/xiaomi_miio/translations/es.json
@@ -5,7 +5,8 @@
             "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso",
             "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se proporcion\u00f3 host ni token.",
             "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio.",
-            "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente"
+            "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente",
+            "unknown": "Error inesperado"
         },
         "error": {
             "cannot_connect": "No se pudo conectar",
diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json
index a7536283240..874483fffb3 100644
--- a/homeassistant/components/xiaomi_miio/translations/hu.json
+++ b/homeassistant/components/xiaomi_miio/translations/hu.json
@@ -5,7 +5,8 @@
             "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve",
             "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva \u00e1llom\u00e1s vagy token.",
             "not_xiaomi_miio": "Az eszk\u00f6zt (m\u00e9g) nem t\u00e1mogatja a Xiaomi Miio integr\u00e1ci\u00f3.",
-            "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt."
+            "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.",
+            "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt"
         },
         "error": {
             "cannot_connect": "Sikertelen csatlakoz\u00e1s",
diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json
index 51b5f108751..239620dfdc6 100644
--- a/homeassistant/components/xiaomi_miio/translations/it.json
+++ b/homeassistant/components/xiaomi_miio/translations/it.json
@@ -5,7 +5,8 @@
             "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
             "incomplete_info": "Informazioni incomplete per configurare il dispositivo, nessun host o token fornito.",
             "not_xiaomi_miio": "Il dispositivo non \u00e8 (ancora) supportato da Xiaomi Miio.",
-            "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente"
+            "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente",
+            "unknown": "Errore imprevisto"
         },
         "error": {
             "cannot_connect": "Impossibile connettersi",
diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json
index d0f3cc9a4a8..c360f1ab730 100644
--- a/homeassistant/components/xiaomi_miio/translations/pl.json
+++ b/homeassistant/components/xiaomi_miio/translations/pl.json
@@ -5,7 +5,8 @@
             "already_in_progress": "Konfiguracja jest ju\u017c w toku",
             "incomplete_info": "Niepe\u0142ne informacje do skonfigurowania urz\u0105dzenia, brak nazwy hosta, IP lub tokena.",
             "not_xiaomi_miio": "Urz\u0105dzenie nie jest (jeszcze) obs\u0142ugiwane przez Xiaomi Miio.",
-            "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+            "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
+            "unknown": "B\u0142\u0105d nieznany"
         },
         "error": {
             "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
diff --git a/homeassistant/components/xiaomi_miio/translations/pt-BR.json b/homeassistant/components/xiaomi_miio/translations/pt-BR.json
index ce67173f9b2..f12c3637b7e 100644
--- a/homeassistant/components/xiaomi_miio/translations/pt-BR.json
+++ b/homeassistant/components/xiaomi_miio/translations/pt-BR.json
@@ -5,7 +5,8 @@
             "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento",
             "incomplete_info": "Informa\u00e7\u00f5es incompletas para configurar o dispositivo, nenhum host ou token fornecido.",
             "not_xiaomi_miio": "O dispositivo (ainda) n\u00e3o \u00e9 suportado pelo Xiaomi Miio.",
-            "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida"
+            "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida",
+            "unknown": "Erro inesperado"
         },
         "error": {
             "cannot_connect": "Falha ao conectar",
-- 
GitLab


From e38d93ee938ca4fd1ba1755cee268d0b300b7df1 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
Date: Fri, 21 Oct 2022 20:56:20 -0400
Subject: [PATCH 685/985] Remove unused keys from ultraloq (#80762)

---
 homeassistant/generated/integrations.json | 2 --
 1 file changed, 2 deletions(-)

diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 5505fa7de3f..5285d7c46d4 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -5591,8 +5591,6 @@
       "name": "U-tec",
       "integrations": {
         "ultraloq": {
-          "config_flow": false,
-          "iot_class": null,
           "integration_type": "virtual",
           "name": "Ultraloq"
         }
-- 
GitLab


From f4f7122c6687b5061f895c3d05e144f8180c7ba3 Mon Sep 17 00:00:00 2001
From: TheJulianJES <TheJulianJES@users.noreply.github.com>
Date: Sat, 22 Oct 2022 02:56:49 +0200
Subject: [PATCH 686/985] Add "power on state" config entity for Tuya plugs to
 ZHA (#80486)

* Add "power on state" config entity for Tuya plugs to ZHA

* Remove TS004F from models, as it doesn't support power-on-state
---
 .../components/zha/core/channels/general.py   | 13 ++++++++++++
 homeassistant/components/zha/select.py        | 20 +++++++++++++++++++
 2 files changed, 33 insertions(+)

diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py
index d310157327b..f5dbab9e38e 100644
--- a/homeassistant/components/zha/core/channels/general.py
+++ b/homeassistant/components/zha/core/channels/general.py
@@ -331,6 +331,19 @@ class OnOffChannel(ZigbeeChannel):
         super().__init__(cluster, ch_pool)
         self._off_listener = None
 
+        if self.cluster.endpoint.model in (
+            "TS011F",
+            "TS0121",
+            "TS0001",
+            "TS0002",
+            "TS0003",
+            "TS0004",
+        ):
+            self.ZCL_INIT_ATTRS = (  # pylint: disable=invalid-name
+                self.ZCL_INIT_ATTRS.copy()
+            )
+            self.ZCL_INIT_ATTRS["power_on_state"] = True
+
     @property
     def on_off(self) -> bool | None:
         """Return cached value of on/off attribute."""
diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py
index c2f315cd217..35746951b47 100644
--- a/homeassistant/components/zha/select.py
+++ b/homeassistant/components/zha/select.py
@@ -227,6 +227,26 @@ class ZHAStartupOnOffSelectEntity(
     _attr_name = "Start-up behavior"
 
 
+class TuyaPowerOnState(types.enum8):
+    """Tuya power on state enum."""
+
+    Off = 0x00
+    On = 0x01
+    LastState = 0x02
+
+
+@CONFIG_DIAGNOSTIC_MATCH(
+    channel_names=CHANNEL_ON_OFF,
+    models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"},
+)
+class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_state"):
+    """Representation of a ZHA power on state select entity."""
+
+    _select_attr = "power_on_state"
+    _enum = TuyaPowerOnState
+    _attr_name = "Power on state"
+
+
 class AqaraMotionSensitivities(types.enum8):
     """Aqara motion sensitivities."""
 
-- 
GitLab


From 5eb69f38aa9b8bbc6e439cd08e401db9a56da64f Mon Sep 17 00:00:00 2001
From: TheJulianJES <TheJulianJES@users.noreply.github.com>
Date: Sat, 22 Oct 2022 02:57:50 +0200
Subject: [PATCH 687/985] Add "power outage memory" config entity to Xiaomi EU
 plugs to ZHA (#80444)

---
 .../zha/core/channels/manufacturerspecific.py        |  6 +++++-
 homeassistant/components/zha/switch.py               | 12 ++++++++++++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py
index febe589dffa..5139854d66a 100644
--- a/homeassistant/components/zha/core/channels/manufacturerspecific.py
+++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py
@@ -62,7 +62,7 @@ class PhillipsRemote(ZigbeeChannel):
 @registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0)
 @registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0)
 class OppleRemote(ZigbeeChannel):
-    """Opple button channel."""
+    """Opple channel."""
 
     REPORT_CONFIG = ()
 
@@ -82,6 +82,10 @@ class OppleRemote(ZigbeeChannel):
                 "motion_sensitivity": True,
                 "approach_distance": True,
             }
+        elif self.cluster.endpoint.model in ("lumi.plug.mmeu01", "lumi.plug.maeu01"):
+            self.ZCL_INIT_ATTRS = {
+                "power_outage_memory": True,
+            }
 
     async def async_initialize_channel_specific(self, from_cache: bool) -> None:
         """Initialize channel specific."""
diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py
index 3db142694fb..e2c956e0722 100644
--- a/homeassistant/components/zha/switch.py
+++ b/homeassistant/components/zha/switch.py
@@ -293,6 +293,18 @@ class P1MotionTriggerIndicatorSwitch(
     _attr_name = "LED trigger indicator"
 
 
+@CONFIG_DIAGNOSTIC_MATCH(
+    channel_names="opple_cluster", models={"lumi.plug.mmeu01", "lumi.plug.maeu01"}
+)
+class XiaomiPlugPowerOutageMemorySwitch(
+    ZHASwitchConfigurationEntity, id_suffix="power_outage_memory"
+):
+    """Representation of a ZHA power outage memory configuration entity."""
+
+    _zcl_attribute: str = "power_outage_memory"
+    _attr_name = "Power outage memory"
+
+
 @CONFIG_DIAGNOSTIC_MATCH(
     channel_names="ikea_airpurifier",
     models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
-- 
GitLab


From 00b0fa6cdfb7066b5b08317f566c68552ce7ac2a Mon Sep 17 00:00:00 2001
From: TheJulianJES <TheJulianJES@users.noreply.github.com>
Date: Sat, 22 Oct 2022 02:59:51 +0200
Subject: [PATCH 688/985] Add Philips Hue motion sensor config entities to ZHA
 (#79923)

* Add Hue trigger LED config switch

* Add Hue sensitivity config select

* Use existing consts for channel names

* Add friendly names to config entities

* Follow HA capitalization conventions

* Move Hue motion sensor check to a helper method

* Move helper method to a new helpers file in channels folder
---
 .../components/zha/core/channels/general.py   | 10 ++++
 .../components/zha/core/channels/helpers.py   | 15 ++++++
 .../zha/core/channels/measurement.py          | 18 +++++++
 homeassistant/components/zha/select.py        | 47 ++++++++++++++++++-
 homeassistant/components/zha/switch.py        | 15 ++++++
 5 files changed, 104 insertions(+), 1 deletion(-)
 create mode 100644 homeassistant/components/zha/core/channels/helpers.py

diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py
index f5dbab9e38e..ded51455af8 100644
--- a/homeassistant/components/zha/core/channels/general.py
+++ b/homeassistant/components/zha/core/channels/general.py
@@ -28,6 +28,7 @@ from ..const import (
     SIGNAL_UPDATE_DEVICE,
 )
 from .base import AttrReportConfig, ClientChannel, ZigbeeChannel, parse_and_log_command
+from .helpers import is_hue_motion_sensor
 
 if TYPE_CHECKING:
     from . import ChannelPool
@@ -152,6 +153,15 @@ class BasicChannel(ZigbeeChannel):
         6: "Emergency mains and transfer switch",
     }
 
+    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):
+            self.ZCL_INIT_ATTRS = (  # pylint: disable=invalid-name
+                self.ZCL_INIT_ATTRS.copy()
+            )
+            self.ZCL_INIT_ATTRS["trigger_indicator"] = True
+
 
 @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id)
 class BinaryInput(ZigbeeChannel):
diff --git a/homeassistant/components/zha/core/channels/helpers.py b/homeassistant/components/zha/core/channels/helpers.py
new file mode 100644
index 00000000000..2297af312eb
--- /dev/null
+++ b/homeassistant/components/zha/core/channels/helpers.py
@@ -0,0 +1,15 @@
+"""Helpers for use with ZHA Zigbee channels."""
+from .base import ZigbeeChannel
+
+
+def is_hue_motion_sensor(channel: ZigbeeChannel) -> bool:
+    """Return true if the manufacturer and model match known Hue motion sensor models."""
+    return channel.cluster.endpoint.manufacturer in (
+        "Philips",
+        "Signify Netherlands B.V.",
+    ) and channel.cluster.endpoint.model in (
+        "SML001",
+        "SML002",
+        "SML003",
+        "SML004",
+    )
diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py
index fa6f9c07dee..be61a75962e 100644
--- a/homeassistant/components/zha/core/channels/measurement.py
+++ b/homeassistant/components/zha/core/channels/measurement.py
@@ -1,4 +1,9 @@
 """Measurement channels module for Zigbee Home Automation."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import zigpy.zcl
 from zigpy.zcl.clusters import measurement
 
 from .. import registries
@@ -9,6 +14,10 @@ from ..const import (
     REPORT_CONFIG_MIN_INT,
 )
 from .base import AttrReportConfig, ZigbeeChannel
+from .helpers import is_hue_motion_sensor
+
+if TYPE_CHECKING:
+    from . import ChannelPool
 
 
 @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id)
@@ -50,6 +59,15 @@ class OccupancySensing(ZigbeeChannel):
         AttrReportConfig(attr="occupancy", config=REPORT_CONFIG_IMMEDIATE),
     )
 
+    def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
+        """Initialize Occupancy channel."""
+        super().__init__(cluster, ch_pool)
+        if is_hue_motion_sensor(self):
+            self.ZCL_INIT_ATTRS = (  # pylint: disable=invalid-name
+                self.ZCL_INIT_ATTRS.copy()
+            )
+            self.ZCL_INIT_ATTRS["sensitivity"] = True
+
 
 @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id)
 class PressureMeasurement(ZigbeeChannel):
diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py
index 35746951b47..38f2f417643 100644
--- a/homeassistant/components/zha/select.py
+++ b/homeassistant/components/zha/select.py
@@ -22,6 +22,7 @@ from .core import discovery
 from .core.const import (
     CHANNEL_IAS_WD,
     CHANNEL_INOVELLI,
+    CHANNEL_OCCUPANCY,
     CHANNEL_ON_OFF,
     DATA_ZHA,
     SIGNAL_ADD_ENTITIES,
@@ -259,13 +260,57 @@ class AqaraMotionSensitivities(types.enum8):
     channel_names="opple_cluster", models={"lumi.motion.ac01", "lumi.motion.ac02"}
 )
 class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"):
-    """Representation of a ZHA on off transition time configuration entity."""
+    """Representation of a ZHA motion sensitivity configuration entity."""
 
     _select_attr = "motion_sensitivity"
     _enum = AqaraMotionSensitivities
     _attr_name = "Motion sensitivity"
 
 
+class HueV1MotionSensitivities(types.enum8):
+    """Hue v1 motion sensitivities."""
+
+    Low = 0x00
+    Medium = 0x01
+    High = 0x02
+
+
+@CONFIG_DIAGNOSTIC_MATCH(
+    channel_names=CHANNEL_OCCUPANCY,
+    manufacturers={"Philips", "Signify Netherlands B.V."},
+    models={"SML001"},
+)
+class HueV1MotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"):
+    """Representation of a ZHA motion sensitivity configuration entity."""
+
+    _select_attr = "sensitivity"
+    _attr_name = "Hue motion sensitivity"
+    _enum = HueV1MotionSensitivities
+
+
+class HueV2MotionSensitivities(types.enum8):
+    """Hue v2 motion sensitivities."""
+
+    Lowest = 0x00
+    Low = 0x01
+    Medium = 0x02
+    High = 0x03
+    Highest = 0x04
+
+
+@CONFIG_DIAGNOSTIC_MATCH(
+    channel_names=CHANNEL_OCCUPANCY,
+    manufacturers={"Philips", "Signify Netherlands B.V."},
+    models={"SML002", "SML003", "SML004"},
+)
+class HueV2MotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"):
+    """Representation of a ZHA motion sensitivity configuration entity."""
+
+    _select_attr = "sensitivity"
+    _attr_name = "Hue motion sensitivity"
+    _enum = HueV2MotionSensitivities
+
+
 class AqaraMonitoringModess(types.enum8):
     """Aqara monitoring modes."""
 
diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py
index e2c956e0722..0bd55cdbe68 100644
--- a/homeassistant/components/zha/switch.py
+++ b/homeassistant/components/zha/switch.py
@@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .core import discovery
 from .core.const import (
+    CHANNEL_BASIC,
     CHANNEL_INOVELLI,
     CHANNEL_ON_OFF,
     DATA_ZHA,
@@ -305,6 +306,20 @@ class XiaomiPlugPowerOutageMemorySwitch(
     _attr_name = "Power outage memory"
 
 
+@CONFIG_DIAGNOSTIC_MATCH(
+    channel_names=CHANNEL_BASIC,
+    manufacturers={"Philips", "Signify Netherlands B.V."},
+    models={"SML001", "SML002", "SML003", "SML004"},
+)
+class HueMotionTriggerIndicatorSwitch(
+    ZHASwitchConfigurationEntity, id_suffix="trigger_indicator"
+):
+    """Representation of a ZHA motion triggering configuration entity."""
+
+    _zcl_attribute: str = "trigger_indicator"
+    _attr_name = "LED trigger indicator"
+
+
 @CONFIG_DIAGNOSTIC_MATCH(
     channel_names="ikea_airpurifier",
     models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
-- 
GitLab


From 2ef14d60f3d665f5cbac51d081c9bb3bfc046052 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
Date: Fri, 21 Oct 2022 22:47:25 -0400
Subject: [PATCH 689/985] Update pending message error (#80763)

---
 homeassistant/components/websocket_api/http.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py
index 7336fa1c0d2..23c8fddd56c 100644
--- a/homeassistant/components/websocket_api/http.py
+++ b/homeassistant/components/websocket_api/http.py
@@ -155,7 +155,8 @@ class WebSocketHandler:
             return
 
         self._logger.error(
-            "Client unable to keep up with pending messages. Stayed over %s for %s seconds",
+            "Client unable to keep up with pending messages. Stayed over %s for %s seconds. "
+            "The system's load is too high or an integration is misbehaving",
             PENDING_MSG_PEAK,
             PENDING_MSG_PEAK_TIME,
         )
-- 
GitLab


From f35af09429ae6028b15a863f7712140e37afb40f Mon Sep 17 00:00:00 2001
From: kpine <keith.pine@gmail.com>
Date: Fri, 21 Oct 2022 23:27:36 -0700
Subject: [PATCH 690/985] Update zwave_js.refresh_value service example
 (#80764)

---
 homeassistant/components/zwave_js/services.yaml | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml
index 687d486888c..de9d4842ff7 100644
--- a/homeassistant/components/zwave_js/services.yaml
+++ b/homeassistant/components/zwave_js/services.yaml
@@ -98,10 +98,12 @@ refresh_value:
   description: Force update value(s) for a Z-Wave entity
   fields:
     entity_id:
-      name: Entity
-      description: Entity whose value(s) should be refreshed
+      name: Entities
+      description: Entities to refresh values for.
       required: true
-      example: sensor.family_room_motion
+      example: |
+        - sensor.family_room_motion
+        - switch.kitchen
       selector:
         entity:
           integration: zwave_js
-- 
GitLab


From 228d491216e6041302ea5624e60cbe55defa4cc6 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Sat, 22 Oct 2022 10:00:35 +0300
Subject: [PATCH 691/985] Fix Shelly entry unload and add tests for init
 (#80760)

---
 .coveragerc                                 |   1 -
 homeassistant/components/shelly/__init__.py |  12 +-
 tests/components/shelly/__init__.py         |   8 +-
 tests/components/shelly/conftest.py         |  35 ++--
 tests/components/shelly/test_init.py        | 184 ++++++++++++++++++++
 tests/components/shelly/test_update.py      |   6 +-
 6 files changed, 218 insertions(+), 28 deletions(-)
 create mode 100644 tests/components/shelly/test_init.py

diff --git a/.coveragerc b/.coveragerc
index ee420ca0f3b..62083486c03 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1106,7 +1106,6 @@ omit =
     homeassistant/components/sesame/lock.py
     homeassistant/components/seven_segments/image_processing.py
     homeassistant/components/seventeentrack/sensor.py
-    homeassistant/components/shelly/__init__.py
     homeassistant/components/shelly/binary_sensor.py
     homeassistant/components/shelly/climate.py
     homeassistant/components/shelly/coordinator.py
diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index 49a67b1a6a0..921ffb352d5 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -143,6 +143,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
                 )
             },
         )
+    # https://github.com/home-assistant/core/pull/48076
     if device_entry and entry.entry_id not in device_entry.config_entries:
         device_entry = None
 
@@ -231,6 +232,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo
                 )
             },
         )
+    # https://github.com/home-assistant/core/pull/48076
     if device_entry and entry.entry_id not in device_entry.config_entries:
         device_entry = None
 
@@ -295,9 +297,13 @@ 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 shelly_entry_data.device is not None:
-        # If device is present, block/rpc coordinator is not setup yet
-        shelly_entry_data.device.shutdown()
+    # If device is present, block/rpc coordinator is not setup yet
+    device = shelly_entry_data.device
+    if isinstance(device, RpcDevice):
+        await device.shutdown()
+        return True
+    if isinstance(device, BlockDevice):
+        device.shutdown()
         return True
 
     platforms = RPC_SLEEPING_PLATFORMS
diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py
index 326e62432d3..a3c571d7177 100644
--- a/tests/components/shelly/__init__.py
+++ b/tests/components/shelly/__init__.py
@@ -5,19 +5,21 @@ from homeassistant.core import HomeAssistant
 
 from tests.common import MockConfigEntry
 
+MOCK_MAC = "123456789ABC"
+
 
 async def init_integration(
-    hass: HomeAssistant, gen: int, model="SHSW-25"
+    hass: HomeAssistant, gen: int, model="SHSW-25", sleep_period=0
 ) -> MockConfigEntry:
     """Set up the Shelly integration in Home Assistant."""
     data = {
         CONF_HOST: "192.168.1.37",
-        CONF_SLEEP_PERIOD: 0,
+        CONF_SLEEP_PERIOD: sleep_period,
         "model": model,
         "gen": gen,
     }
 
-    entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN)
+    entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC)
     entry.add_to_hass(hass)
 
     await hass.config_entries.async_setup(entry.entry_id)
diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py
index 33bad4b1fc0..cd23cc240c5 100644
--- a/tests/components/shelly/conftest.py
+++ b/tests/components/shelly/conftest.py
@@ -1,6 +1,8 @@
 """Test configuration for Shelly."""
 from unittest.mock import AsyncMock, Mock, patch
 
+from aioshelly.block_device import BlockDevice
+from aioshelly.rpc_device import RpcDevice
 import pytest
 
 from homeassistant.components.shelly.const import (
@@ -8,13 +10,15 @@ from homeassistant.components.shelly.const import (
     REST_SENSORS_UPDATE_INTERVAL,
 )
 
+from . import MOCK_MAC
+
 from tests.common import async_capture_events, async_mock_service, mock_device_registry
 
 MOCK_SETTINGS = {
     "name": "Test name",
     "mode": "relay",
     "device": {
-        "mac": "test-mac",
+        "mac": MOCK_MAC,
         "hostname": "test-host",
         "type": "SHSW-25",
         "num_outputs": 2,
@@ -95,7 +99,7 @@ MOCK_CONFIG = {
 }
 
 MOCK_SHELLY_COAP = {
-    "mac": "test-mac",
+    "mac": MOCK_MAC,
     "auth": False,
     "fw": "20201124-092854/v1.9.0@57ac4ad8",
     "num_outputs": 2,
@@ -104,7 +108,7 @@ MOCK_SHELLY_COAP = {
 MOCK_SHELLY_RPC = {
     "name": "Test Gen2",
     "id": "shellyplus2pm-123456789abc",
-    "mac": "123456789ABC",
+    "mac": MOCK_MAC,
     "model": "SNSW-002P16EU",
     "gen": 2,
     "fw_id": "20220830-130540/0.11.0-gfa1bc37",
@@ -142,7 +146,13 @@ MOCK_STATUS_RPC = {
 @pytest.fixture(autouse=True)
 def mock_coap():
     """Mock out coap."""
-    with patch("homeassistant.components.shelly.utils.get_coap_context"):
+    with patch(
+        "homeassistant.components.shelly.utils.COAP",
+        return_value=Mock(
+            initialize=AsyncMock(),
+            close=Mock(),
+        ),
+    ):
         yield
 
 
@@ -174,24 +184,18 @@ def events(hass):
 @pytest.fixture
 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:
+    with patch("aioshelly.block_device.BlockDevice.create") as block_device_mock:
 
         def update():
             block_device_mock.return_value.subscribe_updates.call_args[0][0]({})
 
         device = Mock(
+            spec=BlockDevice,
             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
@@ -209,18 +213,13 @@ async def mock_rpc_device():
             rpc_device_mock.return_value.subscribe_updates.call_args[0][0]({})
 
         device = Mock(
-            call_rpc=AsyncMock(),
+            spec=RpcDevice,
             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(),
-            initialize=AsyncMock(),
             initialized=True,
-            shutdown=AsyncMock(),
         )
 
         rpc_device_mock.return_value = device
diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py
new file mode 100644
index 00000000000..f795b79132f
--- /dev/null
+++ b/tests/components/shelly/test_init.py
@@ -0,0 +1,184 @@
+"""Test cases for the Shelly component."""
+
+from unittest.mock import AsyncMock
+
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
+import pytest
+
+from homeassistant.components.shelly.const import DOMAIN
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
+from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
+from homeassistant.helpers import device_registry
+from homeassistant.setup import async_setup_component
+
+from . import MOCK_MAC, init_integration
+
+from tests.common import MockConfigEntry
+
+
+async def test_custom_coap_port(hass, mock_block_device, caplog):
+    """Test custom coap port."""
+    assert await async_setup_component(
+        hass,
+        DOMAIN,
+        {DOMAIN: {"coap_port": 7632}},
+    )
+    await hass.async_block_till_done()
+
+    await init_integration(hass, 1)
+    assert "Starting CoAP context with UDP port 7632" in caplog.text
+
+
+@pytest.mark.parametrize("gen", [1, 2])
+async def test_shared_device_mac(
+    hass, gen, mock_block_device, mock_rpc_device, device_reg, caplog
+):
+    """Test first time shared device with another domain."""
+    config_entry = MockConfigEntry(domain="test", data={}, unique_id="some_id")
+    config_entry.add_to_hass(hass)
+    device_reg.async_get_or_create(
+        config_entry_id=config_entry.entry_id,
+        connections={
+            (
+                device_registry.CONNECTION_NETWORK_MAC,
+                device_registry.format_mac(MOCK_MAC),
+            )
+        },
+    )
+    await init_integration(hass, gen, sleep_period=1000)
+    assert "will resume when device is online" in caplog.text
+
+
+async def test_setup_entry_not_shelly(hass, caplog):
+    """Test not Shelly entry."""
+    entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
+    entry.add_to_hass(hass)
+
+    assert await hass.config_entries.async_setup(entry.entry_id) is False
+    await hass.async_block_till_done()
+
+    assert "probably comes from a custom integration" in caplog.text
+
+
+@pytest.mark.parametrize("gen", [1, 2])
+async def test_device_connection_error(
+    hass, gen, mock_block_device, mock_rpc_device, monkeypatch
+):
+    """Test device connection error."""
+    monkeypatch.setattr(
+        mock_block_device, "initialize", AsyncMock(side_effect=DeviceConnectionError)
+    )
+    monkeypatch.setattr(
+        mock_rpc_device, "initialize", AsyncMock(side_effect=DeviceConnectionError)
+    )
+
+    entry = await init_integration(hass, gen)
+    assert entry.state == ConfigEntryState.SETUP_RETRY
+
+
+@pytest.mark.parametrize("gen", [1, 2])
+async def test_device_auth_error(
+    hass, gen, mock_block_device, mock_rpc_device, monkeypatch
+):
+    """Test device authentication error."""
+    monkeypatch.setattr(
+        mock_block_device, "initialize", AsyncMock(side_effect=InvalidAuthError)
+    )
+    monkeypatch.setattr(
+        mock_rpc_device, "initialize", AsyncMock(side_effect=InvalidAuthError)
+    )
+
+    entry = await init_integration(hass, gen)
+    assert entry.state == ConfigEntryState.SETUP_ERROR
+
+    flows = hass.config_entries.flow.async_progress()
+    assert len(flows) == 1
+
+    flow = flows[0]
+    assert flow.get("step_id") == "reauth_confirm"
+    assert flow.get("handler") == DOMAIN
+
+    assert "context" in flow
+    assert flow["context"].get("source") == SOURCE_REAUTH
+    assert flow["context"].get("entry_id") == entry.entry_id
+
+
+@pytest.mark.parametrize("entry_sleep, device_sleep", [(None, 0), (1000, 1000)])
+async def test_sleeping_block_device_online(
+    hass, entry_sleep, device_sleep, mock_block_device, device_reg, caplog
+):
+    """Test sleeping block device online."""
+    config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id="shelly")
+    config_entry.add_to_hass(hass)
+    device_reg.async_get_or_create(
+        config_entry_id=config_entry.entry_id,
+        connections={
+            (
+                device_registry.CONNECTION_NETWORK_MAC,
+                device_registry.format_mac(MOCK_MAC),
+            )
+        },
+    )
+
+    entry = await init_integration(hass, 1, sleep_period=entry_sleep)
+    assert "will resume when device is online" in caplog.text
+
+    mock_block_device.mock_update()
+    assert "online, resuming setup" in caplog.text
+    assert entry.data["sleep_period"] == device_sleep
+
+
+@pytest.mark.parametrize("entry_sleep, device_sleep", [(None, 0), (1000, 1000)])
+async def test_sleeping_rpc_device_online(
+    hass, entry_sleep, device_sleep, mock_rpc_device, caplog
+):
+    """Test sleeping RPC device online."""
+    entry = await init_integration(hass, 2, sleep_period=entry_sleep)
+    assert "will resume when device is online" in caplog.text
+
+    mock_rpc_device.mock_update()
+    assert "online, resuming setup" in caplog.text
+    assert entry.data["sleep_period"] == device_sleep
+
+
+@pytest.mark.parametrize(
+    "gen, entity_id",
+    [
+        (1, "switch.test_name_channel_1"),
+        (2, "switch.test_switch_0"),
+    ],
+)
+async def test_entry_unload(hass, gen, entity_id, mock_block_device, mock_rpc_device):
+    """Test entry unload."""
+    entry = await init_integration(hass, gen)
+
+    assert entry.state is ConfigEntryState.LOADED
+    assert hass.states.get(entity_id).state is STATE_ON
+
+    await hass.config_entries.async_unload(entry.entry_id)
+    await hass.async_block_till_done()
+
+    assert entry.state is ConfigEntryState.NOT_LOADED
+    assert hass.states.get(entity_id).state is STATE_UNAVAILABLE
+
+
+@pytest.mark.parametrize(
+    "gen, entity_id",
+    [
+        (1, "switch.test_name_channel_1"),
+        (2, "switch.test_switch_0"),
+    ],
+)
+async def test_entry_unload_device_not_ready(
+    hass, gen, entity_id, mock_block_device, mock_rpc_device
+):
+    """Test entry unload when device is not ready."""
+    entry = await init_integration(hass, gen, sleep_period=1000)
+
+    assert entry.state is ConfigEntryState.LOADED
+    assert hass.states.get(entity_id) is None
+
+    await hass.config_entries.async_unload(entry.entry_id)
+    await hass.async_block_till_done()
+
+    assert entry.state is ConfigEntryState.NOT_LOADED
diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py
index 7cff529f48a..f5f713eb81e 100644
--- a/tests/components/shelly/test_update.py
+++ b/tests/components/shelly/test_update.py
@@ -6,7 +6,7 @@ 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
+from . import MOCK_MAC, init_integration
 
 
 async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch):
@@ -15,7 +15,7 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch)
     entity_registry.async_get_or_create(
         UPDATE_DOMAIN,
         DOMAIN,
-        "test-mac-fwupdate",
+        f"{MOCK_MAC}-fwupdate",
         suggested_object_id="test_name_firmware_update",
         disabled_by=None,
     )
@@ -46,7 +46,7 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch):
     entity_registry.async_get_or_create(
         UPDATE_DOMAIN,
         DOMAIN,
-        "shelly-sys-fwupdate",
+        f"{MOCK_MAC}-sys-fwupdate",
         suggested_object_id="test_name_firmware_update",
         disabled_by=None,
     )
-- 
GitLab


From 9fa5c5c576ac442d133561a858b60fe4bd598401 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
Date: Sat, 22 Oct 2022 12:36:52 +0200
Subject: [PATCH 692/985] Remove myself from Xiaomi Miio code owner list
 (#80768)

---
 CODEOWNERS                                         | 4 ++--
 homeassistant/components/xiaomi_miio/manifest.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index c9e42a8becf..eaf1ba3662c 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1295,8 +1295,8 @@ build.json @home-assistant/supervisor
 /tests/components/xiaomi_aqara/ @danielhiversen @syssi
 /homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79
 /tests/components/xiaomi_ble/ @Jc2k @Ernst79
-/homeassistant/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu
-/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu
+/homeassistant/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
+/tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG
 /homeassistant/components/xiaomi_tv/ @simse
 /homeassistant/components/xmpp/ @fabaff @flowolf
 /homeassistant/components/yale_smart_alarm/ @gjohansson-ST
diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json
index 0f1a9dd92aa..4f806c3ed58 100644
--- a/homeassistant/components/xiaomi_miio/manifest.json
+++ b/homeassistant/components/xiaomi_miio/manifest.json
@@ -4,7 +4,7 @@
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
   "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.12"],
-  "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
+  "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"],
   "zeroconf": ["_miio._udp.local."],
   "iot_class": "local_polling",
   "loggers": ["micloud", "miio"]
-- 
GitLab


From b6b36861d9acac3b5f7fcf2a6e40fd1086df9aad Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
Date: Sat, 22 Oct 2022 14:38:08 +0200
Subject: [PATCH 693/985] Add `integration_type` field for a few integrations
 (#80767)

---
 .../components/accuweather/manifest.json       |  3 ++-
 homeassistant/components/airly/manifest.json   |  3 ++-
 .../components/braviatv/manifest.json          |  3 ++-
 homeassistant/components/brother/manifest.json |  3 ++-
 homeassistant/components/gios/manifest.json    |  3 ++-
 homeassistant/components/nam/manifest.json     |  3 ++-
 homeassistant/components/nextdns/manifest.json |  3 ++-
 homeassistant/components/shelly/manifest.json  |  3 ++-
 .../components/tractive/manifest.json          |  3 ++-
 homeassistant/generated/integrations.json      | 18 +++++++++---------
 10 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json
index f92dca9dfee..5bda281ff3c 100644
--- a/homeassistant/components/accuweather/manifest.json
+++ b/homeassistant/components/accuweather/manifest.json
@@ -7,5 +7,6 @@
   "config_flow": true,
   "quality_scale": "platinum",
   "iot_class": "cloud_polling",
-  "loggers": ["accuweather"]
+  "loggers": ["accuweather"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json
index 56dd205de68..91d2e829741 100644
--- a/homeassistant/components/airly/manifest.json
+++ b/homeassistant/components/airly/manifest.json
@@ -7,5 +7,6 @@
   "config_flow": true,
   "quality_scale": "platinum",
   "iot_class": "cloud_polling",
-  "loggers": ["airly"]
+  "loggers": ["airly"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json
index ffb92a2348f..fa009bf05ef 100644
--- a/homeassistant/components/braviatv/manifest.json
+++ b/homeassistant/components/braviatv/manifest.json
@@ -12,5 +12,6 @@
   ],
   "config_flow": true,
   "iot_class": "local_polling",
-  "loggers": ["pybravia"]
+  "loggers": ["pybravia"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json
index 61b1d8bcdc9..68922ecaeb3 100644
--- a/homeassistant/components/brother/manifest.json
+++ b/homeassistant/components/brother/manifest.json
@@ -13,5 +13,6 @@
   "config_flow": true,
   "quality_scale": "platinum",
   "iot_class": "local_polling",
-  "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"]
+  "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json
index 20ad912d40c..d5f01d0f1c3 100644
--- a/homeassistant/components/gios/manifest.json
+++ b/homeassistant/components/gios/manifest.json
@@ -7,5 +7,6 @@
   "config_flow": true,
   "quality_scale": "platinum",
   "iot_class": "cloud_polling",
-  "loggers": ["dacite", "gios"]
+  "loggers": ["dacite", "gios"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json
index b70c2054808..43c217e2a4d 100644
--- a/homeassistant/components/nam/manifest.json
+++ b/homeassistant/components/nam/manifest.json
@@ -17,5 +17,6 @@
   "config_flow": true,
   "quality_scale": "platinum",
   "iot_class": "local_polling",
-  "loggers": ["nettigo_air_monitor"]
+  "loggers": ["nettigo_air_monitor"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json
index 04c2e3575f1..2a68107079e 100644
--- a/homeassistant/components/nextdns/manifest.json
+++ b/homeassistant/components/nextdns/manifest.json
@@ -7,5 +7,6 @@
   "config_flow": true,
   "iot_class": "cloud_polling",
   "loggers": ["nextdns"],
-  "quality_scale": "platinum"
+  "quality_scale": "platinum",
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json
index b3bd329b868..3e12835bf0f 100644
--- a/homeassistant/components/shelly/manifest.json
+++ b/homeassistant/components/shelly/manifest.json
@@ -13,5 +13,6 @@
   ],
   "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"],
   "iot_class": "local_push",
-  "loggers": ["aioshelly"]
+  "loggers": ["aioshelly"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json
index a672c862959..308f190a063 100644
--- a/homeassistant/components/tractive/manifest.json
+++ b/homeassistant/components/tractive/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["aiotractive==0.5.4"],
   "codeowners": ["@Danielhiversen", "@zhulik", "@bieniu"],
   "iot_class": "cloud_push",
-  "loggers": ["aiotractive"]
+  "loggers": ["aiotractive"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 5285d7c46d4..fad8266872b 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -13,7 +13,7 @@
     },
     "accuweather": {
       "name": "AccuWeather",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -79,7 +79,7 @@
     },
     "airly": {
       "name": "Airly",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -609,7 +609,7 @@
     },
     "brother": {
       "name": "Brother Printer",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -1844,7 +1844,7 @@
     },
     "gios": {
       "name": "GIO\u015a",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -3362,7 +3362,7 @@
     },
     "nam": {
       "name": "Nettigo Air Monitor",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -3461,7 +3461,7 @@
     },
     "nextdns": {
       "name": "NextDNS",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4639,7 +4639,7 @@
     },
     "shelly": {
       "name": "Shelly",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_push"
     },
@@ -4915,7 +4915,7 @@
       "name": "Sony",
       "integrations": {
         "braviatv": {
-          "integration_type": "hub",
+          "integration_type": "device",
           "config_flow": true,
           "iot_class": "local_polling",
           "name": "Sony Bravia TV"
@@ -5479,7 +5479,7 @@
     },
     "tractive": {
       "name": "Tractive",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "cloud_push"
     },
-- 
GitLab


From 10dbef80ae03860b90a955657d75a631629b61f4 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Sat, 22 Oct 2022 15:07:43 +0200
Subject: [PATCH 694/985] Add myself as code owner to util and const (#80664)

---
 CODEOWNERS                    | 2 ++
 script/hassfest/codeowners.py | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/CODEOWNERS b/CODEOWNERS
index eaf1ba3662c..e1e1d7282d3 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -19,6 +19,8 @@ build.json @home-assistant/supervisor
 
 # Other code
 /homeassistant/scripts/check_config.py @kellerza
+/homeassistant/const.py @epenet
+/homeassistant/util/ @epenet
 
 # Integrations
 /homeassistant/components/abode/ @shred86
diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py
index 43695a39ad4..0cc58012162 100644
--- a/script/hassfest/codeowners.py
+++ b/script/hassfest/codeowners.py
@@ -25,6 +25,8 @@ build.json @home-assistant/supervisor
 
 # Other code
 /homeassistant/scripts/check_config.py @kellerza
+/homeassistant/const.py @epenet
+/homeassistant/util/ @epenet
 
 # Integrations
 """.strip()
-- 
GitLab


From 85dda685241df1e2319aca553026b01977040369 Mon Sep 17 00:00:00 2001
From: Guido Schmitz <Shutgun@users.noreply.github.com>
Date: Sat, 22 Oct 2022 15:55:23 +0200
Subject: [PATCH 695/985] Add integration_type to devolo integrations (#80695)

---
 homeassistant/components/devolo_home_control/manifest.json | 1 +
 homeassistant/components/devolo_home_network/manifest.json | 1 +
 homeassistant/generated/integrations.json                  | 2 +-
 3 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json
index 0de3cd7f07d..c6a25420333 100644
--- a/homeassistant/components/devolo_home_control/manifest.json
+++ b/homeassistant/components/devolo_home_control/manifest.json
@@ -1,6 +1,7 @@
 {
   "domain": "devolo_home_control",
   "name": "devolo Home Control",
+  "integration_type": "hub",
   "documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
   "requirements": ["devolo-home-control-api==0.18.2"],
   "after_dependencies": ["zeroconf"],
diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json
index 94f26c8a615..945c314a196 100644
--- a/homeassistant/components/devolo_home_network/manifest.json
+++ b/homeassistant/components/devolo_home_network/manifest.json
@@ -1,6 +1,7 @@
 {
   "domain": "devolo_home_network",
   "name": "devolo Home Network",
+  "integration_type": "device",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/devolo_home_network",
   "requirements": ["devolo-plc-api==0.8.0"],
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index fad8266872b..677436c7f81 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -989,7 +989,7 @@
           "name": "devolo Home Control"
         },
         "devolo_home_network": {
-          "integration_type": "hub",
+          "integration_type": "device",
           "config_flow": true,
           "iot_class": "local_polling",
           "name": "devolo Home Network"
-- 
GitLab


From 81f2f134f45d2303977f7e25fdc339245c754ca6 Mon Sep 17 00:00:00 2001
From: uvjustin <46082645+uvjustin@users.noreply.github.com>
Date: Sat, 22 Oct 2022 07:09:13 -0700
Subject: [PATCH 696/985] Bump owntone requirements (#80552)

---
 homeassistant/components/forked_daapd/manifest.json | 2 +-
 requirements_all.txt                                | 4 ++--
 requirements_test_all.txt                           | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json
index 8cd2822156f..e6793498c35 100644
--- a/homeassistant/components/forked_daapd/manifest.json
+++ b/homeassistant/components/forked_daapd/manifest.json
@@ -3,7 +3,7 @@
   "name": "Owntone",
   "documentation": "https://www.home-assistant.io/integrations/forked_daapd",
   "codeowners": ["@uvjustin"],
-  "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"],
+  "requirements": ["pyforked-daapd==0.1.14", "pylibrespot-java==0.1.1"],
   "after_dependencies": ["spotify"],
   "config_flow": true,
   "zeroconf": ["_daap._tcp.local."],
diff --git a/requirements_all.txt b/requirements_all.txt
index 0302b1b60de..129bb442ba9 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1571,7 +1571,7 @@ pyflume==0.6.5
 pyfnip==0.2
 
 # homeassistant.components.forked_daapd
-pyforked-daapd==0.1.11
+pyforked-daapd==0.1.14
 
 # homeassistant.components.freedompro
 pyfreedompro==1.1.0
@@ -1679,7 +1679,7 @@ pylaunches==1.3.0
 pylgnetcast==0.3.7
 
 # homeassistant.components.forked_daapd
-pylibrespot-java==0.1.0
+pylibrespot-java==0.1.1
 
 # homeassistant.components.litejet
 pylitejet==0.3.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index cd530443971..1f89d575b57 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1102,7 +1102,7 @@ pyflic==2.0.3
 pyflume==0.6.5
 
 # homeassistant.components.forked_daapd
-pyforked-daapd==0.1.11
+pyforked-daapd==0.1.14
 
 # homeassistant.components.freedompro
 pyfreedompro==1.1.0
@@ -1180,7 +1180,7 @@ pylast==4.2.1
 pylaunches==1.3.0
 
 # homeassistant.components.forked_daapd
-pylibrespot-java==0.1.0
+pylibrespot-java==0.1.1
 
 # homeassistant.components.litejet
 pylitejet==0.3.0
-- 
GitLab


From c4831333fae3b3200b6d0d4f031c878002b9c2b4 Mon Sep 17 00:00:00 2001
From: Steven Hosking <steve@Hosking.com.au>
Date: Sun, 23 Oct 2022 02:44:25 +1100
Subject: [PATCH 697/985] Extend roomba mac range in manifest (#80714)

Co-authored-by: Franck Nijhof <git@frenck.dev>
---
 homeassistant/components/roomba/manifest.json | 4 ++++
 homeassistant/generated/dhcp.py               | 1 +
 2 files changed, 5 insertions(+)

diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json
index af60705f73c..34b31058f5f 100644
--- a/homeassistant/components/roomba/manifest.json
+++ b/homeassistant/components/roomba/manifest.json
@@ -17,6 +17,10 @@
     {
       "hostname": "roomba-*",
       "macaddress": "DCF505*"
+    },
+    {
+      "hostname": "roomba-*",
+      "macaddress": "204EF6*"
     }
   ],
   "iot_class": "local_push",
diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py
index ae1b7a76a88..7b2eb4eacf6 100644
--- a/homeassistant/generated/dhcp.py
+++ b/homeassistant/generated/dhcp.py
@@ -93,6 +93,7 @@ DHCP: list[dict[str, str | bool]] = [
     {"domain": "roomba", "hostname": "irobot-*", "macaddress": "501479*"},
     {"domain": "roomba", "hostname": "roomba-*", "macaddress": "80A589*"},
     {"domain": "roomba", "hostname": "roomba-*", "macaddress": "DCF505*"},
+    {"domain": "roomba", "hostname": "roomba-*", "macaddress": "204EF6*"},
     {"domain": "samsungtv", "registered_devices": True},
     {"domain": "samsungtv", "hostname": "tizen*"},
     {"domain": "samsungtv", "macaddress": "4844F7*"},
-- 
GitLab


From b9527972987521123342658ef9579ac71a485d13 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sat, 22 Oct 2022 16:14:11 -0500
Subject: [PATCH 698/985] Bump bleak-retry-connector to 2.3.2 (#80790)

---
 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 6c4b6f7dc6c..a2e2322b4db 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -7,7 +7,7 @@
   "quality_scale": "internal",
   "requirements": [
     "bleak==0.19.0",
-    "bleak-retry-connector==2.3.1",
+    "bleak-retry-connector==2.3.2",
     "bluetooth-adapters==0.6.0",
     "bluetooth-auto-recovery==0.3.6",
     "dbus-fast==1.47.0"
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 28d9cf02d9e..e27ad8e79e5 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.3.1
+bleak-retry-connector==2.3.2
 bleak==0.19.0
 bluetooth-adapters==0.6.0
 bluetooth-auto-recovery==0.3.6
diff --git a/requirements_all.txt b/requirements_all.txt
index 129bb442ba9..8c94d7c0f6e 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.3.1
+bleak-retry-connector==2.3.2
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 1f89d575b57..9856c164f40 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.3.1
+bleak-retry-connector==2.3.2
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
-- 
GitLab


From a4b8124a10c98324dbc09db7544c257a9ebecd85 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Sun, 23 Oct 2022 00:36:20 +0000
Subject: [PATCH 699/985] [ci skip] Translation update

---
 homeassistant/components/braviatv/translations/ca.json        | 2 +-
 homeassistant/components/lidarr/translations/ca.json          | 1 +
 homeassistant/components/nest/translations/ca.json            | 2 +-
 .../components/openexchangerates/translations/ca.json         | 4 ++--
 homeassistant/components/roomba/translations/ca.json          | 4 ++--
 homeassistant/components/xiaomi_miio/translations/ca.json     | 3 ++-
 homeassistant/components/xiaomi_miio/translations/et.json     | 3 ++-
 homeassistant/components/xiaomi_miio/translations/nl.json     | 3 ++-
 homeassistant/components/xiaomi_miio/translations/pl.json     | 2 +-
 homeassistant/components/xiaomi_miio/translations/ru.json     | 3 ++-
 10 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json
index f686c076bb8..e6f8ebc8e92 100644
--- a/homeassistant/components/braviatv/translations/ca.json
+++ b/homeassistant/components/braviatv/translations/ca.json
@@ -19,7 +19,7 @@
                     "pin": "Codi PIN",
                     "use_psk": "Utilitza autenticaci\u00f3 PSK"
                 },
-                "description": "Introdueix el codi PIN que es mostra a la pantalla del televisor.\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.",
+                "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.",
                 "title": "Autoritzaci\u00f3 del televisor Sony Bravia"
             },
             "confirm": {
diff --git a/homeassistant/components/lidarr/translations/ca.json b/homeassistant/components/lidarr/translations/ca.json
index 78d0904b50a..9cc30d6f893 100644
--- a/homeassistant/components/lidarr/translations/ca.json
+++ b/homeassistant/components/lidarr/translations/ca.json
@@ -33,6 +33,7 @@
         "step": {
             "init": {
                 "data": {
+                    "max_records": "Nombre m\u00e0xim de registres a mostrar a la cua i a desitjats",
                     "upcoming_days": "Nombre dies propers a mostrar al calendari"
                 }
             }
diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json
index 20fc09c1af3..e3d55a7c84d 100644
--- a/homeassistant/components/nest/translations/ca.json
+++ b/homeassistant/components/nest/translations/ca.json
@@ -52,7 +52,7 @@
                 "data": {
                     "project_id": "ID de projecte Device Access"
                 },
-                "description": "Crea un projecte d'acc\u00e9s a dispositius Nest que **requereix una tarifa de 5 $** per configurar-lo.\n1. V\u00e9s a [Consola d'acc\u00e9s al dispositiu]({device_access_console_url}) i a trav\u00e9s del flux de pagament.\n2. Fes clic a **Crea projecte**.\n3. D\u00f3na-li un nom al projecte d'acc\u00e9s a dispositius i feu clic a **Seg\u00fcent**.\n4. Introdueix el teu ID de client OAuth.\n5. Activa els esdeveniments fent clic a **Activa** i **Crea projecte**. \n\nIntrodueix el teu ID de projecte d'acc\u00e9s a dispositiu a continuaci\u00f3 ([m\u00e9s informaci\u00f3]({more_info_url})).\n",
+                "description": "Crea un projecte d'acc\u00e9s a dispositius Nest que **implica el pagament a Google de 5$** per configurar-lo.\n1. V\u00e9s a [Consola d'acc\u00e9s al dispositiu]({device_access_console_url}) i a trav\u00e9s del flux de pagament.\n2. Fes clic a **Crea projecte**.\n3. D\u00f3na-li un nom al projecte d'acc\u00e9s a dispositius i feu clic a **Seg\u00fcent**.\n4. Introdueix el teu ID de client OAuth.\n5. Activa els esdeveniments fent clic a **Activa** i **Crea projecte**. \n\nIntrodueix el teu ID de projecte d'acc\u00e9s a dispositiu a continuaci\u00f3 ([m\u00e9s informaci\u00f3]({more_info_url})).\n",
                 "title": "Nest: crea un projecte Device Access"
             },
             "device_project_upgrade": {
diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json
index 634ea7578e7..81d359599ae 100644
--- a/homeassistant/components/openexchangerates/translations/ca.json
+++ b/homeassistant/components/openexchangerates/translations/ca.json
@@ -26,8 +26,8 @@
     },
     "issues": {
         "deprecated_yaml": {
-            "description": "La configuraci\u00f3 d'Open Exchange Rates mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Open Exchange Rates del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.",
-            "title": "La configuraci\u00f3 YAML d'Open Exchange Rates est\u00e0 sent eliminada"
+            "description": "La configuraci\u00f3 d'Open Exchange Rates mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nElimina la configuraci\u00f3 YAML d'Open Exchange Rates del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.",
+            "title": "La configuraci\u00f3 YAML d'Open Exchange Rates s'ha eliminat"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json
index de0f673e971..e480c06d07d 100644
--- a/homeassistant/components/roomba/translations/ca.json
+++ b/homeassistant/components/roomba/translations/ca.json
@@ -12,14 +12,14 @@
         "flow_title": "{name} ({host})",
         "step": {
             "link": {
-                "description": "Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons) despr\u00e9s, envia en els seg\u00fcents 30 segons.",
+                "description": "Assegura't que l'aplicaci\u00f3 d'iRobot no est\u00e0 funcionant en cap dispositiu. Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons) despr\u00e9s, envia en els seg\u00fcents 30 segons.",
                 "title": "Recupera la contrasenya"
             },
             "link_manual": {
                 "data": {
                     "password": "Contrasenya"
                 },
-                "description": "No s'ha pogut obtenir la contrasenya del dispositiu autom\u00e0ticament. Segueix els passos de la seg\u00fcent documentaci\u00f3: {auth_help_url}",
+                "description": "No s'ha pogut obtenir la contrasenya del dispositiu autom\u00e0ticament. Assegura't que l'aplicaci\u00f3 d'iRobot est\u00e0 tancada de tots els dispositius mentre s'intenti obtenir la contrasenya. Segueix els passos de la documentaci\u00f3 seg\u00fcent: {auth_help_url}",
                 "title": "Introdueix contrasenya"
             },
             "manual": {
diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json
index 614238a54b4..ff1297080c3 100644
--- a/homeassistant/components/xiaomi_miio/translations/ca.json
+++ b/homeassistant/components/xiaomi_miio/translations/ca.json
@@ -5,7 +5,8 @@
             "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
             "incomplete_info": "Informaci\u00f3 incompleta per configurar el dispositiu, no s'ha proporcionat cap amfitri\u00f3 o token.",
             "not_xiaomi_miio": "Xiaomi Miio encara no \u00e9s compatible amb el dispositiu.",
-            "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+            "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament",
+            "unknown": "Error inesperat"
         },
         "error": {
             "cannot_connect": "Ha fallat la connexi\u00f3",
diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json
index fef6a73622a..bfb9de5077e 100644
--- a/homeassistant/components/xiaomi_miio/translations/et.json
+++ b/homeassistant/components/xiaomi_miio/translations/et.json
@@ -5,7 +5,8 @@
             "already_in_progress": "Seadistamine on juba k\u00e4imas",
             "incomplete_info": "Puudulik seadistusteave, hosti v\u00f5i p\u00e4\u00e4suluba pole esitatud.",
             "not_xiaomi_miio": "Seade ei ole (veel) Xiaomi Miio poolt toetatud.",
-            "reauth_successful": "Taastuvastamine \u00f5nnestus"
+            "reauth_successful": "Taastuvastamine \u00f5nnestus",
+            "unknown": "Ootamatu t\u00f5rge"
         },
         "error": {
             "cannot_connect": "\u00dchendus nurjus",
diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json
index cc077839808..07671e1802e 100644
--- a/homeassistant/components/xiaomi_miio/translations/nl.json
+++ b/homeassistant/components/xiaomi_miio/translations/nl.json
@@ -5,7 +5,8 @@
             "already_in_progress": "De configuratie is momenteel al bezig",
             "incomplete_info": "Onvolledige informatie voor het instellen van het apparaat, geen host of token opgegeven.",
             "not_xiaomi_miio": "Apparaat wordt (nog) niet ondersteund door Xiaomi Miio.",
-            "reauth_successful": "Herauthenticatie geslaagd"
+            "reauth_successful": "Herauthenticatie geslaagd",
+            "unknown": "Onverwachte fout"
         },
         "error": {
             "cannot_connect": "Kan geen verbinding maken",
diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json
index c360f1ab730..72b031bd606 100644
--- a/homeassistant/components/xiaomi_miio/translations/pl.json
+++ b/homeassistant/components/xiaomi_miio/translations/pl.json
@@ -6,7 +6,7 @@
             "incomplete_info": "Niepe\u0142ne informacje do skonfigurowania urz\u0105dzenia, brak nazwy hosta, IP lub tokena.",
             "not_xiaomi_miio": "Urz\u0105dzenie nie jest (jeszcze) obs\u0142ugiwane przez Xiaomi Miio.",
             "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
-            "unknown": "B\u0142\u0105d nieznany"
+            "unknown": "Nieoczekiwany b\u0142\u0105d"
         },
         "error": {
             "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json
index 1432666cb44..256cf75bbd2 100644
--- a/homeassistant/components/xiaomi_miio/translations/ru.json
+++ b/homeassistant/components/xiaomi_miio/translations/ru.json
@@ -5,7 +5,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.",
             "incomplete_info": "\u041d\u0435\u043f\u043e\u043b\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 \u0442\u043e\u043a\u0435\u043d.",
             "not_xiaomi_miio": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u043f\u043e\u043a\u0430) \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f Xiaomi Miio.",
-            "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_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": {
             "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
-- 
GitLab


From 4837e22262b97784891ba218f3b607f732f9d148 Mon Sep 17 00:00:00 2001
From: jjlawren <jjlawren@users.noreply.github.com>
Date: Sat, 22 Oct 2022 19:59:44 -0500
Subject: [PATCH 700/985] Bump soco to 0.28.1 (#80792)

---
 homeassistant/components/sonos/manifest.json | 2 +-
 requirements_all.txt                         | 2 +-
 requirements_test_all.txt                    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index b8506cd2783..57438d1864a 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -3,7 +3,7 @@
   "name": "Sonos",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/sonos",
-  "requirements": ["soco==0.28.0"],
+  "requirements": ["soco==0.28.1"],
   "dependencies": ["ssdp"],
   "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
   "zeroconf": ["_sonos._tcp.local."],
diff --git a/requirements_all.txt b/requirements_all.txt
index 8c94d7c0f6e..edfaa7c543e 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2277,7 +2277,7 @@ smhi-pkg==1.0.16
 snapcast==2.3.0
 
 # homeassistant.components.sonos
-soco==0.28.0
+soco==0.28.1
 
 # homeassistant.components.solaredge_local
 solaredge-local==0.2.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 9856c164f40..d834a7c6585 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1565,7 +1565,7 @@ smart-meter-texas==0.4.7
 smhi-pkg==1.0.16
 
 # homeassistant.components.sonos
-soco==0.28.0
+soco==0.28.1
 
 # homeassistant.components.solaredge
 solaredge==0.0.2
-- 
GitLab


From 8fa64a7a89d9ea414652731d831a1edd2f247a40 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Sun, 23 Oct 2022 07:57:25 +0300
Subject: [PATCH 701/985] Bump aioshelly to 4.1.0 (#80795)

---
 homeassistant/components/shelly/coordinator.py | 10 +++++++---
 homeassistant/components/shelly/entity.py      |  8 ++++++--
 homeassistant/components/shelly/manifest.json  |  2 +-
 requirements_all.txt                           |  2 +-
 requirements_test_all.txt                      |  2 +-
 5 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py
index f309fc99358..fdb94b3bdb0 100644
--- a/homeassistant/components/shelly/coordinator.py
+++ b/homeassistant/components/shelly/coordinator.py
@@ -8,7 +8,7 @@ from typing import Any, cast
 
 import aioshelly
 from aioshelly.block_device import BlockDevice
-from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
 from aioshelly.rpc_device import RpcDevice
 
 from homeassistant.config_entries import ConfigEntry
@@ -510,7 +510,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
         try:
             await self.device.trigger_ota_update(beta=beta)
         except DeviceConnectionError as err:
-            raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err
+            raise HomeAssistantError(
+                f"OTA update connection error: {repr(err)}"
+            ) from err
+        except RpcCallError as err:
+            raise HomeAssistantError(f"OTA update request error: {repr(err)}") from err
         except InvalidAuthError:
             self.entry.async_start_reauth(self.hass)
         else:
@@ -553,7 +557,7 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator):
         LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
         try:
             await self.device.update_status()
-        except DeviceConnectionError as err:
+        except (DeviceConnectionError, RpcCallError) as err:
             raise UpdateFailed(f"Device disconnected: {repr(err)}") from err
         except InvalidAuthError:
             self.entry.async_start_reauth(self.hass)
diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py
index 72794be60cc..fd92ea41408 100644
--- a/homeassistant/components/shelly/entity.py
+++ b/homeassistant/components/shelly/entity.py
@@ -6,7 +6,7 @@ from dataclasses import dataclass
 from typing import Any, cast
 
 from aioshelly.block_device import Block
-from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
@@ -426,7 +426,11 @@ class ShellyRpcEntity(entity.Entity):
         except DeviceConnectionError as err:
             self.coordinator.last_update_success = False
             raise HomeAssistantError(
-                f"Call RPC for entity {self.name} failed, method: {method}, params: {params}, error: {repr(err)}"
+                f"Call RPC for {self.name} connection error, method: {method}, params: {params}, error: {repr(err)}"
+            ) from err
+        except RpcCallError as err:
+            raise HomeAssistantError(
+                f"Call RPC for {self.name} request error, method: {method}, params: {params}, error: {repr(err)}"
             ) from err
         except InvalidAuthError:
             self.coordinator.entry.async_start_reauth(self.hass)
diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json
index 3e12835bf0f..54396d7171e 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.0.0"],
+  "requirements": ["aioshelly==4.1.0"],
   "dependencies": ["http"],
   "zeroconf": [
     {
diff --git a/requirements_all.txt b/requirements_all.txt
index edfaa7c543e..2c436f65568 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.0.0
+aioshelly==4.1.0
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index d834a7c6585..a84a2eb13f7 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.0.0
+aioshelly==4.1.0
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
-- 
GitLab


From dd39ddca2f3dd3f9d28e160a9b4904bcd58331c7 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 23 Oct 2022 01:13:44 -0500
Subject: [PATCH 702/985] Bump aiohomekit to 2.2.0 (#80798)

---
 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 8e82c59568b..20e19c7b36c 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.1.1"],
+  "requirements": ["aiohomekit==2.2.0"],
   "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 2c436f65568..4c54c2bfbc2 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.1.1
+aiohomekit==2.2.0
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index a84a2eb13f7..c6aef7947b7 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.1.1
+aiohomekit==2.2.0
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From 95d6859cb7fb29e58130a03420d77166d03c7124 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 23 Oct 2022 04:34:03 -0500
Subject: [PATCH 703/985] Log bluetooth advertisement before firing bleak
 callbacks (#80800)

The debug log was confusing because the bleak callbacks
were firing before we were logging the advertisements
---
 homeassistant/components/bluetooth/manager.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py
index ba294e8e64a..aaefd3dcfc4 100644
--- a/homeassistant/components/bluetooth/manager.py
+++ b/homeassistant/components/bluetooth/manager.py
@@ -433,11 +433,7 @@ class BluetoothManager:
         ):
             return
 
-        if is_connectable_by_any_source := address in self._connectable_history:
-            # Bleak callbacks must get a connectable device
-            for callback_filters in self._bleak_callbacks:
-                _dispatch_bleak_callback(*callback_filters, device, advertisement_data)
-
+        is_connectable_by_any_source = address in self._connectable_history
         if not connectable and is_connectable_by_any_source:
             # Since we have a connectable path and our BleakClient will
             # route any connection attempts to the connectable path, we
@@ -456,6 +452,11 @@ class BluetoothManager:
             advertisement_data.rssi,
         )
 
+        if is_connectable_by_any_source:
+            # Bleak callbacks must get a connectable device
+            for callback_filters in self._bleak_callbacks:
+                _dispatch_bleak_callback(*callback_filters, device, advertisement_data)
+
         for match in self._callback_index.match_callbacks(service_info):
             callback = match[CALLBACK]
             try:
-- 
GitLab


From a26e4618c749c636949f922a6c6230751e7e0269 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Sun, 23 Oct 2022 11:36:54 +0200
Subject: [PATCH 704/985] Update psutil to 5.9.3 (#80775)

---
 homeassistant/components/systemmonitor/manifest.json | 2 +-
 requirements_all.txt                                 | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json
index 8e9f6d3e896..a2db68f11c7 100644
--- a/homeassistant/components/systemmonitor/manifest.json
+++ b/homeassistant/components/systemmonitor/manifest.json
@@ -2,7 +2,7 @@
   "domain": "systemmonitor",
   "name": "System Monitor",
   "documentation": "https://www.home-assistant.io/integrations/systemmonitor",
-  "requirements": ["psutil==5.9.2"],
+  "requirements": ["psutil==5.9.3"],
   "codeowners": [],
   "iot_class": "local_push",
   "loggers": ["psutil"]
diff --git a/requirements_all.txt b/requirements_all.txt
index 4c54c2bfbc2..60e42e88327 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1342,7 +1342,7 @@ proxmoxer==1.3.1
 psutil-home-assistant==0.0.1
 
 # homeassistant.components.systemmonitor
-psutil==5.9.2
+psutil==5.9.3
 
 # homeassistant.components.pulseaudio_loopback
 pulsectl==20.2.4
-- 
GitLab


From bd6678c73b374a22ef3525d6d1e70e3c4312758a Mon Sep 17 00:00:00 2001
From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com>
Date: Sun, 23 Oct 2022 12:42:35 +0200
Subject: [PATCH 705/985] Bump plugwise to v0.25.3 (#80782)

---
 homeassistant/components/plugwise/manifest.json               | 2 +-
 requirements_all.txt                                          | 2 +-
 requirements_test_all.txt                                     | 2 +-
 .../components/plugwise/fixtures/m_adam_cooling/all_data.json | 4 ++--
 4 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json
index 43da399a8f2..da639e9f918 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.25.2"],
+  "requirements": ["plugwise==0.25.3"],
   "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
   "zeroconf": ["_plugwise._tcp.local."],
   "config_flow": true,
diff --git a/requirements_all.txt b/requirements_all.txt
index 60e42e88327..b325887c6cd 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.25.2
+plugwise==0.25.3
 
 # homeassistant.components.plum_lightpad
 plumlightpad==0.0.11
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index c6aef7947b7..13a5db7baf1 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.25.2
+plugwise==0.25.3
 
 # homeassistant.components.plum_lightpad
 plumlightpad==0.0.11
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 ae9a16b7d1a..06a3fa400bf 100644
--- a/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json
+++ b/tests/components/plugwise/fixtures/m_adam_cooling/all_data.json
@@ -14,7 +14,7 @@
       "name": "Anna",
       "vendor": "Plugwise",
       "thermostat": {
-        "setpoint_low": 20.0,
+        "setpoint_low": 4.0,
         "setpoint_high": 23.5,
         "lower_bound": 1.0,
         "upper_bound": 35.0,
@@ -30,7 +30,7 @@
       "mode": "heat_cool",
       "sensors": {
         "temperature": 25.8,
-        "setpoint_low": 20.0,
+        "setpoint_low": 4.0,
         "setpoint_high": 23.5
       }
     },
-- 
GitLab


From 873ccc4493943ddf78ed404045c93074d07721d7 Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
Date: Mon, 24 Oct 2022 02:38:45 +1100
Subject: [PATCH 706/985] Refactor Powerview sensor to dataclass (#80536)

---
 .../hunterdouglas_powerview/coordinator.py    |   2 +-
 .../hunterdouglas_powerview/sensor.py         | 182 ++++++++++--------
 2 files changed, 105 insertions(+), 79 deletions(-)

diff --git a/homeassistant/components/hunterdouglas_powerview/coordinator.py b/homeassistant/components/hunterdouglas_powerview/coordinator.py
index 7c45feba491..203aea6c49f 100644
--- a/homeassistant/components/hunterdouglas_powerview/coordinator.py
+++ b/homeassistant/components/hunterdouglas_powerview/coordinator.py
@@ -25,7 +25,7 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData])
         shades: Shades,
         hub_address: str,
     ) -> None:
-        """Initialize DataUpdateCoordinator to gather data for specific SmartPlug."""
+        """Initialize DataUpdateCoordinator to gather data for specific Powerview Hub."""
         self.shades = shades
         super().__init__(
             hass,
diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py
index 88e62d51937..a4a3da57121 100644
--- a/homeassistant/components/hunterdouglas_powerview/sensor.py
+++ b/homeassistant/components/hunterdouglas_powerview/sensor.py
@@ -1,9 +1,15 @@
 """Support for hunterdouglass_powerview sensors."""
+
+from collections.abc import Callable
+from dataclasses import dataclass
+from typing import Any, Final
+
 from aiopvapi.resources.shade import BaseShade, factory as PvShade
 
 from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
+    SensorEntityDescription,
     SensorStateClass,
 )
 from homeassistant.config_entries import ConfigEntry
@@ -21,87 +27,60 @@ from .const import (
     SHADE_BATTERY_LEVEL,
     SHADE_BATTERY_LEVEL_MAX,
 )
+from .coordinator import PowerviewShadeUpdateCoordinator
 from .entity import ShadeEntity
-from .model import PowerviewEntryData
-
-
-class PowerViewSensor(ShadeEntity, SensorEntity):
-    """Representation of an shade battery charge sensor."""
-
-    _attr_entity_category = EntityCategory.DIAGNOSTIC
-    _attr_state_class = SensorStateClass.MEASUREMENT
-
-    async def async_added_to_hass(self) -> None:
-        """When entity is added to hass."""
-        self.async_on_remove(
-            self.coordinator.async_add_listener(self._async_update_shade_from_group)
-        )
-
-    @callback
-    def _async_update_shade_from_group(self) -> None:
-        """Update with new data from the coordinator."""
-        self._shade.raw_data = self.data.get_raw_data(self._shade.id)
-        self.async_write_ha_state()
-
-
-class PowerViewShadeBatterySensor(PowerViewSensor):
-    """Representation of an shade battery charge sensor."""
-
-    _attr_native_unit_of_measurement = PERCENTAGE
-    _attr_device_class = SensorDeviceClass.BATTERY
-
-    def __init__(self, coordinator, device_info, room_name, shade, name):
-        """Initialize the shade."""
-        super().__init__(coordinator, device_info, room_name, shade, name)
-        self._attr_unique_id = f"{self._attr_unique_id}_charge"
-        self._attr_name = f"{self._shade_name} Battery"
-
-    @property
-    def native_value(self) -> int:
-        """Get the current value in percentage."""
-        return round(
-            self._shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100
-        )
-
-    async def async_update(self) -> None:
-        """Refresh shade battery."""
-        await self._shade.refresh_battery()
-
-
-class PowerViewShadeSignalSensor(PowerViewSensor):
-    """Representation of an shade signal sensor."""
-
-    _attr_native_unit_of_measurement = PERCENTAGE
-    _attr_state_class = SensorStateClass.MEASUREMENT
-
-    def __init__(self, coordinator, device_info, room_name, shade, name):
-        """Initialize the shade."""
-        super().__init__(coordinator, device_info, room_name, shade, name)
-        self._attr_unique_id = f"{self._attr_unique_id}_signal"
-        self._attr_name = f"{self._shade_name} Signal"
-
-    @property
-    def native_value(self) -> int:
-        """Get the current value in percentage."""
-        return round(
-            self._shade.raw_data[ATTR_SIGNAL_STRENGTH] / ATTR_SIGNAL_STRENGTH_MAX * 100
-        )
-
-    async def async_update(self) -> None:
-        """Refresh signal strength."""
-        await self._shade.refresh()
-
-
-SENSOR_TYPES = {
-    PowerViewShadeBatterySensor: SHADE_BATTERY_LEVEL,
-    PowerViewShadeSignalSensor: ATTR_SIGNAL_STRENGTH,
-}
+from .model import PowerviewDeviceInfo, PowerviewEntryData
+
+
+@dataclass
+class PowerviewSensorDescriptionMixin:
+    """Mixin to describe a Sensor entity."""
+
+    update_fn: Callable[[BaseShade], Any]
+    native_value_fn: Callable[[BaseShade], int]
+    create_sensor_fn: Callable[[BaseShade], bool]
+
+
+@dataclass
+class PowerviewSensorDescription(
+    SensorEntityDescription, PowerviewSensorDescriptionMixin
+):
+    """Class to describe a Sensor entity."""
+
+    entity_category = EntityCategory.DIAGNOSTIC
+    state_class = SensorStateClass.MEASUREMENT
+
+
+SENSORS: Final = [
+    PowerviewSensorDescription(
+        key="charge",
+        name="Battery",
+        device_class=SensorDeviceClass.BATTERY,
+        native_unit_of_measurement=PERCENTAGE,
+        native_value_fn=lambda shade: round(
+            shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100
+        ),
+        create_sensor_fn=lambda shade: bool(SHADE_BATTERY_LEVEL in shade.raw_data),
+        update_fn=lambda shade: shade.refresh_battery(),
+    ),
+    PowerviewSensorDescription(
+        key="signal",
+        name="Signal",
+        device_class=SensorDeviceClass.SIGNAL_STRENGTH,
+        native_unit_of_measurement=PERCENTAGE,
+        native_value_fn=lambda shade: round(
+            shade.raw_data[ATTR_SIGNAL_STRENGTH] / ATTR_SIGNAL_STRENGTH_MAX * 100
+        ),
+        create_sensor_fn=lambda shade: bool(ATTR_SIGNAL_STRENGTH in shade.raw_data),
+        update_fn=lambda shade: shade.refresh(),
+    ),
+]
 
 
 async def async_setup_entry(
     hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
 ) -> None:
-    """Set up the hunter douglas shades sensors."""
+    """Set up the hunter douglas sensor entities."""
 
     pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
 
@@ -111,15 +90,62 @@ async def async_setup_entry(
         name_before_refresh = shade.name
         room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
         room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
-        for cls, attr in SENSOR_TYPES.items():
-            if attr in shade.raw_data:
+
+        for description in SENSORS:
+            if description.create_sensor_fn(shade):
                 entities.append(
-                    cls(
+                    PowerViewSensor(
                         pv_entry.coordinator,
                         pv_entry.device_info,
                         room_name,
                         shade,
                         name_before_refresh,
+                        description,
                     )
                 )
+
     async_add_entities(entities)
+
+
+class PowerViewSensor(ShadeEntity, SensorEntity):
+    """Representation of an shade sensor."""
+
+    entity_description: PowerviewSensorDescription
+
+    def __init__(
+        self,
+        coordinator: PowerviewShadeUpdateCoordinator,
+        device_info: PowerviewDeviceInfo,
+        room_name: str,
+        shade: BaseShade,
+        name: str,
+        description: PowerviewSensorDescription,
+    ) -> None:
+        """Initialize the select entity."""
+        super().__init__(coordinator, device_info, room_name, shade, name)
+        self.entity_description = description
+        self._attr_name = f"{self._shade_name} {description.name}"
+        self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
+        self._attr_native_unit_of_measurement = description.native_unit_of_measurement
+
+    @property
+    def native_value(self) -> int:
+        """Get the current value in percentage."""
+        return self.entity_description.native_value_fn(self._shade)
+
+    async def async_added_to_hass(self) -> None:
+        """When entity is added to hass."""
+        self.async_on_remove(
+            self.coordinator.async_add_listener(self._async_update_shade_from_group)
+        )
+
+    @callback
+    def _async_update_shade_from_group(self) -> None:
+        """Update with new data from the coordinator."""
+        self._shade.raw_data = self.data.get_raw_data(self._shade.id)
+        self.async_write_ha_state()
+
+    async def async_update(self) -> None:
+        """Refresh sensor entity."""
+        await self.entity_description.update_fn(self._shade)
+        self.async_write_ha_state()
-- 
GitLab


From 2966f9ed8e13810302365d3a86a57651ce5d1b86 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
Date: Mon, 24 Oct 2022 03:28:17 +1100
Subject: [PATCH 707/985] Add themes for LIFX multi-zone devices via a new
 select entity (#80067)

---
 homeassistant/components/lifx/const.py       |  2 +
 homeassistant/components/lifx/coordinator.py | 17 +++++-
 homeassistant/components/lifx/manager.py     |  5 +-
 homeassistant/components/lifx/select.py      | 58 +++++++++++++++++++-
 homeassistant/components/lifx/services.yaml  | 37 ++++++++++++-
 homeassistant/components/lifx/util.py        |  4 +-
 tests/components/lifx/test_light.py          | 10 +++-
 tests/components/lifx/test_select.py         | 38 +++++++++++++
 8 files changed, 162 insertions(+), 9 deletions(-)

diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py
index 8acfa35802e..a81cd7d59be 100644
--- a/homeassistant/components/lifx/const.py
+++ b/homeassistant/components/lifx/const.py
@@ -36,6 +36,8 @@ ATTR_POWER = "power"
 ATTR_REMAINING = "remaining"
 ATTR_ZONES = "zones"
 
+ATTR_THEME = "theme"
+
 HEV_CYCLE_STATE = "hev_cycle_state"
 INFRARED_BRIGHTNESS = "infrared_brightness"
 INFRARED_BRIGHTNESS_VALUES_MAP = {
diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py
index 8e9eed34ab3..b89fefb35fd 100644
--- a/homeassistant/components/lifx/coordinator.py
+++ b/homeassistant/components/lifx/coordinator.py
@@ -14,6 +14,7 @@ from aiolifx.aiolifx import (
     TileEffectType,
 )
 from aiolifx.connection import LIFXConnection
+from aiolifx_themes.themes import ThemeLibrary, ThemePainter
 
 from homeassistant.const import Platform
 from homeassistant.core import HomeAssistant, callback
@@ -69,6 +70,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
         self.lock = asyncio.Lock()
         self.active_effect = FirmwareEffect.OFF
         update_interval = timedelta(seconds=10)
+        self.last_used_theme: str = ""
 
         super().__init__(
             hass,
@@ -286,8 +288,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
     async def async_set_multizone_effect(
         self,
         effect: str,
-        speed: float = 3,
+        speed: float = 3.0,
         direction: str = "RIGHT",
+        theme_name: str | None = None,
         power_on: bool = True,
     ) -> None:
         """Control the firmware-based Move effect on a multizone device."""
@@ -295,6 +298,12 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
             if power_on and self.device.power_level == 0:
                 await self.async_set_power(True, 0)
 
+            if theme_name is not None:
+                theme = ThemeLibrary().get_theme(theme_name)
+                await ThemePainter(self.hass.loop).paint(
+                    theme, [self.device], round(speed)
+                )
+
             await async_execute_lifx(
                 partial(
                     self.device.set_multizone_effect,
@@ -345,3 +354,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
         """Set infrared brightness."""
         infrared_brightness = infrared_brightness_option_to_value(option)
         await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness))
+
+    async def async_apply_theme(self, theme_name: str) -> None:
+        """Apply the selected theme to the device."""
+        self.last_used_theme = theme_name
+        theme = ThemeLibrary().get_theme(theme_name)
+        await ThemePainter(self.hass.loop).paint(theme, [self.device])
diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py
index d6ae45c1edc..f91ed761e44 100644
--- a/homeassistant/components/lifx/manager.py
+++ b/homeassistant/components/lifx/manager.py
@@ -29,7 +29,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.service import async_extract_referenced_entity_ids
 
-from .const import DATA_LIFX_MANAGER, DOMAIN
+from .const import ATTR_THEME, DATA_LIFX_MANAGER, DOMAIN
 from .coordinator import LIFXUpdateCoordinator, Light
 from .util import convert_8_to_16, find_hsbk
 
@@ -51,7 +51,6 @@ ATTR_CHANGE = "change"
 ATTR_DIRECTION = "direction"
 ATTR_SPEED = "speed"
 ATTR_PALETTE = "palette"
-ATTR_THEME = "theme"
 
 EFFECT_FLAME = "FLAME"
 EFFECT_MORPH = "MORPH"
@@ -177,6 +176,7 @@ LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
         **LIFX_EFFECT_SCHEMA,
         ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)),
         ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS),
+        ATTR_THEME: vol.Optional(vol.In(ThemeLibrary().themes)),
     }
 )
 
@@ -324,6 +324,7 @@ class LIFXManager:
                         direction=kwargs.get(
                             ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION
                         ),
+                        theme_name=kwargs.get(ATTR_THEME, None),
                         power_on=kwargs.get(ATTR_POWER_ON, False),
                     )
                     for coordinator in coordinators
diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py
index a89159968b1..2abbde9aed2 100644
--- a/homeassistant/components/lifx/select.py
+++ b/homeassistant/components/lifx/select.py
@@ -1,17 +1,26 @@
 """Select sensor entities for LIFX integration."""
 from __future__ import annotations
 
+from aiolifx_themes.themes import ThemeLibrary
+
 from homeassistant.components.select import SelectEntity, SelectEntityDescription
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from .const import DOMAIN, INFRARED_BRIGHTNESS, INFRARED_BRIGHTNESS_VALUES_MAP
+from .const import (
+    ATTR_THEME,
+    DOMAIN,
+    INFRARED_BRIGHTNESS,
+    INFRARED_BRIGHTNESS_VALUES_MAP,
+)
 from .coordinator import LIFXUpdateCoordinator
 from .entity import LIFXEntity
 from .util import lifx_features
 
+THEME_NAMES = [theme_name.lower() for theme_name in ThemeLibrary().themes]
+
 INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription(
     key=INFRARED_BRIGHTNESS,
     name="Infrared brightness",
@@ -19,6 +28,13 @@ INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription(
     options=list(INFRARED_BRIGHTNESS_VALUES_MAP.values()),
 )
 
+THEME_ENTITY = SelectEntityDescription(
+    key=ATTR_THEME,
+    name="Theme",
+    entity_category=EntityCategory.CONFIG,
+    options=THEME_NAMES,
+)
+
 
 async def async_setup_entry(
     hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@@ -30,11 +46,16 @@ async def async_setup_entry(
         async_add_entities(
             [
                 LIFXInfraredBrightnessSelectEntity(
-                    coordinator, description=INFRARED_BRIGHTNESS_ENTITY
+                    coordinator=coordinator, description=INFRARED_BRIGHTNESS_ENTITY
                 )
             ]
         )
 
+    if lifx_features(coordinator.device)["multizone"] is True:
+        async_add_entities(
+            [LIFXThemeSelectEntity(coordinator=coordinator, description=THEME_ENTITY)]
+        )
+
 
 class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
     """LIFX Nightvision infrared brightness configuration entity."""
@@ -65,3 +86,36 @@ class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
     async def async_select_option(self, option: str) -> None:
         """Update the infrared brightness value."""
         await self.coordinator.async_set_infrared_brightness(option)
+
+
+class LIFXThemeSelectEntity(LIFXEntity, SelectEntity):
+    """Theme entity for LIFX multizone devices."""
+
+    _attr_has_entity_name = True
+    _attr_should_poll = False
+
+    def __init__(
+        self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription
+    ) -> None:
+        """Initialise the theme selection entity."""
+
+        super().__init__(coordinator)
+        self.entity_description = description
+        self._attr_name = description.name
+        self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
+        self._attr_current_option = None
+
+    @callback
+    def _handle_coordinator_update(self) -> None:
+        """Handle updated data from the coordinator."""
+        self._async_update_attrs()
+        super()._handle_coordinator_update()
+
+    @callback
+    def _async_update_attrs(self) -> None:
+        """Update attrs from coordinator data."""
+        self._attr_current_option = self.coordinator.last_used_theme
+
+    async def async_select_option(self, option: str) -> None:
+        """Paint the selected theme onto the device."""
+        await self.coordinator.async_apply_theme(option.lower())
diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml
index ced5bacf513..976d4ff5623 100644
--- a/homeassistant/components/lifx/services.yaml
+++ b/homeassistant/components/lifx/services.yaml
@@ -183,6 +183,7 @@ effect_move:
       name: Speed
       description: How long in seconds for the effect to move across the length of the light.
       default: 3.0
+      example: 3.0
       selector:
         number:
           min: 0.1
@@ -193,12 +194,46 @@ effect_move:
       name: Direction
       description: Direction the effect will move across the device.
       default: right
+      example: right
       selector:
         select:
           mode: dropdown
           options:
             - right
             - left
+    theme:
+      name: Theme
+      description: (Optional) set one of the predefined themes onto the device before starting the effect.
+      example: exciting
+      default: exciting
+      selector:
+        select:
+          mode: dropdown
+          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.
@@ -271,7 +306,7 @@ effect_morph:
             - "halloween"
             - "hanukkah"
             - "holly"
-            - "independence day"
+            - "independence_day"
             - "intense"
             - "mellow"
             - "peaceful"
diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py
index 4e811e6c366..46a087296f2 100644
--- a/homeassistant/components/lifx/util.py
+++ b/homeassistant/components/lifx/util.py
@@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers import device_registry as dr
 import homeassistant.util.color as color_util
 
-from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
+from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
 
 FIX_MAC_FW = AwesomeVersion("3.70")
 
@@ -154,7 +154,7 @@ async def async_execute_lifx(method: Callable) -> Message:
             # us by async_timeout when we hit the OVERALL_TIMEOUT
             future.set_result(message)
 
-    _LOGGER.debug("Sending LIFX command: %s", method)
+    # _LOGGER.debug("Sending LIFX command: %s", method)
 
     method(callb=_callback)
     result = None
diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py
index cba5ba4636c..6fe63b14b6a 100644
--- a/tests/components/lifx/test_light.py
+++ b/tests/components/lifx/test_light.py
@@ -801,6 +801,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
     )
     config_entry.add_to_hass(hass)
     bulb = _mocked_light_strip()
+    bulb.product = 38
     bulb.power_level = 0
     bulb.color = [65535, 65535, 65535, 65535]
     with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
@@ -828,6 +829,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
         "speed": 3.0,
         "direction": 0,
     }
+
     bulb.get_multizone_effect.reset_mock()
     bulb.set_multizone_effect.reset_mock()
     bulb.set_power.reset_mock()
@@ -836,7 +838,12 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
     await hass.services.async_call(
         DOMAIN,
         SERVICE_EFFECT_MOVE,
-        {ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 4.5, ATTR_DIRECTION: "left"},
+        {
+            ATTR_ENTITY_ID: entity_id,
+            ATTR_SPEED: 4.5,
+            ATTR_DIRECTION: "left",
+            ATTR_THEME: "sports",
+        },
         blocking=True,
     )
 
@@ -849,6 +856,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None:
     assert state.state == STATE_ON
 
     assert len(bulb.set_power.calls) == 1
+    assert len(bulb.set_extended_color_zones.calls) == 1
     assert len(bulb.set_multizone_effect.calls) == 1
     call_dict = bulb.set_multizone_effect.calls[0][1]
     call_dict.pop("callb")
diff --git a/tests/components/lifx/test_select.py b/tests/components/lifx/test_select.py
index bc2d6f0fc1e..d190cbe6b10 100644
--- a/tests/components/lifx/test_select.py
+++ b/tests/components/lifx/test_select.py
@@ -17,6 +17,7 @@ from . import (
     SERIAL,
     MockLifxCommand,
     _mocked_infrared_bulb,
+    _mocked_light_strip,
     _patch_config_flow_try_connect,
     _patch_device,
     _patch_discovery,
@@ -25,6 +26,43 @@ from . import (
 from tests.common import MockConfigEntry, async_fire_time_changed
 
 
+async def test_theme_select(hass: HomeAssistant) -> None:
+    """Test selecting a theme."""
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_light_strip()
+    bulb.product = 38
+    bulb.power_level = 0
+    bulb.color = [0, 0, 65535, 3500]
+    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 = "select.my_bulb_theme"
+
+    entity_registry = er.async_get(hass)
+    entity = entity_registry.async_get(entity_id)
+    assert entity
+    assert not entity.disabled
+
+    await hass.services.async_call(
+        SELECT_DOMAIN,
+        "select_option",
+        {ATTR_ENTITY_ID: entity_id, "option": "intense"},
+        blocking=True,
+    )
+
+    assert len(bulb.set_extended_color_zones.calls) == 1
+    bulb.set_extended_color_zones.reset_mock()
+
+
 async def test_infrared_brightness(hass: HomeAssistant) -> None:
     """Test getting and setting infrared brightness."""
 
-- 
GitLab


From c6e7b9cc99b01291f1d51c6e8958915b713640d9 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Sun, 23 Oct 2022 19:33:08 +0200
Subject: [PATCH 708/985] Refactor UniFi upgrade entities (#80752)

* Refactor UniFi upgrade entities

* Enable type check for UniFi update platform
---
 .strict-typing                           |   1 +
 homeassistant/components/unifi/update.py | 139 ++++++++++-------------
 mypy.ini                                 |  10 ++
 3 files changed, 72 insertions(+), 78 deletions(-)

diff --git a/.strict-typing b/.strict-typing
index 2e6d68edc4c..3e14772318c 100644
--- a/.strict-typing
+++ b/.strict-typing
@@ -271,6 +271,7 @@ homeassistant.components.trafikverket_train.*
 homeassistant.components.trafikverket_weatherstation.*
 homeassistant.components.tts.*
 homeassistant.components.twentemilieu.*
+homeassistant.components.unifi.update
 homeassistant.components.unifiprotect.*
 homeassistant.components.upcloud.*
 homeassistant.components.update.*
diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py
index 76965c17dad..5f26cba57cd 100644
--- a/homeassistant/components/unifi/update.py
+++ b/homeassistant/components/unifi/update.py
@@ -2,8 +2,9 @@
 from __future__ import annotations
 
 import logging
-from typing import Any
+from typing import TYPE_CHECKING, Any
 
+from aiounifi.interfaces.api_handlers import ItemEvent
 from aiounifi.models.device import DeviceUpgradeRequest
 
 from homeassistant.components.update import (
@@ -13,7 +14,6 @@ from homeassistant.components.update import (
     UpdateEntityFeature,
 )
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_NAME
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -21,7 +21,9 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
-from .unifi_entity_base import UniFiBase
+
+if TYPE_CHECKING:
+    from .controller import UniFiController
 
 LOGGER = logging.getLogger(__name__)
 
@@ -38,103 +40,84 @@ async def async_setup_entry(
     controller.entities[DOMAIN] = {DEVICE_UPDATE: set()}
 
     @callback
-    def items_added(
-        clients: set = controller.api.clients, devices: set = controller.api.devices
-    ) -> None:
-        """Add device update entities."""
-        add_device_update_entities(controller, async_add_entities, devices)
-
-    for signal in (controller.signal_update, controller.signal_options_update):
-        config_entry.async_on_unload(
-            async_dispatcher_connect(hass, signal, items_added)
-        )
-
-    items_added()
-
-
-@callback
-def add_device_update_entities(controller, async_add_entities, devices):
-    """Add new device update entities from the controller."""
-    entities = []
-
-    for mac in devices:
-        if mac in controller.entities[DOMAIN][UniFiDeviceUpdateEntity.TYPE]:
-            continue
+    def async_add_update_entity(_: ItemEvent, obj_id: str) -> None:
+        """Add new device update entities from the controller."""
+        async_add_entities([UnifiDeviceUpdateEntity(obj_id, controller)])
 
-        device = controller.api.devices[mac]
-        entities.append(UniFiDeviceUpdateEntity(device, controller))
+    controller.api.devices.subscribe(async_add_update_entity, ItemEvent.ADDED)
 
-    async_add_entities(entities)
+    for device_id in controller.api.devices:
+        async_add_update_entity(ItemEvent.ADDED, device_id)
 
 
-class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity):
+class UnifiDeviceUpdateEntity(UpdateEntity):
     """Update entity for a UniFi network infrastructure device."""
 
     DOMAIN = DOMAIN
     TYPE = DEVICE_UPDATE
     _attr_device_class = UpdateDeviceClass.FIRMWARE
+    _attr_has_entity_name = True
 
-    def __init__(self, device, controller):
+    def __init__(self, obj_id: str, controller: UniFiController) -> None:
         """Set up device update entity."""
-        super().__init__(device, controller)
-
-        self.device = self._item
+        controller.entities[DOMAIN][DEVICE_UPDATE].add(obj_id)
+        self.controller = controller
+        self._obj_id = obj_id
+        self._attr_unique_id = f"{self.TYPE}-{obj_id}"
 
         self._attr_supported_features = UpdateEntityFeature.PROGRESS
-
-        if self.controller.site_role == "admin":
+        if controller.site_role == "admin":
             self._attr_supported_features |= UpdateEntityFeature.INSTALL
 
-    @property
-    def name(self) -> str:
-        """Return the name of the device."""
-        return self.device.name or self.device.model
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique identifier for this device."""
-        return f"{self.TYPE}-{self.device.mac}"
-
-    @property
-    def available(self) -> bool:
-        """Return if controller is available."""
-        return not self.device.disabled and self.controller.available
-
-    @property
-    def in_progress(self) -> bool:
-        """Update installation in progress."""
-        return self.device.state == 4
-
-    @property
-    def installed_version(self) -> str | None:
-        """Version currently in use."""
-        return self.device.version
-
-    @property
-    def latest_version(self) -> str | None:
-        """Latest version available for install."""
-        return self.device.upgrade_to_firmware or self.device.version
-
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return a device description for device registry."""
-        info = DeviceInfo(
-            connections={(CONNECTION_NETWORK_MAC, self.device.mac)},
+        device = controller.api.devices[obj_id]
+        self._attr_available = controller.available and not device.disabled
+        self._attr_in_progress = device.state == 4
+        self._attr_installed_version = device.version
+        self._attr_latest_version = device.upgrade_to_firmware or device.version
+
+        self._attr_device_info = DeviceInfo(
+            connections={(CONNECTION_NETWORK_MAC, obj_id)},
             manufacturer=ATTR_MANUFACTURER,
-            model=self.device.model,
-            sw_version=self.device.version,
+            model=device.model,
+            name=device.name or None,
+            sw_version=device.version,
+            hw_version=device.board_revision,
+        )
+
+    async def async_added_to_hass(self) -> None:
+        """Entity created."""
+        self.async_on_remove(
+            self.controller.api.devices.subscribe(self.async_signalling_callback)
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                self.controller.signal_reachable,
+                self.async_signal_reachable_callback,
+            )
         )
 
-        if self.device.name:
-            info[ATTR_NAME] = self.device.name
+    async def async_will_remove_from_hass(self) -> None:
+        """Disconnect object when removed."""
+        self.controller.entities[DOMAIN][DEVICE_UPDATE].remove(self._obj_id)
 
-        return info
+    @callback
+    def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
+        """Object has new event."""
+        device = self.controller.api.devices[self._obj_id]
+        self._attr_available = self.controller.available and not device.disabled
+        self._attr_in_progress = device.state == 4
+        self._attr_installed_version = device.version
+        self._attr_latest_version = device.upgrade_to_firmware or device.version
+        self.async_write_ha_state()
 
-    async def options_updated(self) -> None:
-        """No action needed."""
+    @callback
+    def async_signal_reachable_callback(self) -> None:
+        """Call when controller connection state change."""
+        self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
 
     async def async_install(
         self, version: str | None, backup: bool, **kwargs: Any
     ) -> None:
         """Install an update."""
-        await self.controller.api.request(DeviceUpgradeRequest.create(self.device.mac))
+        await self.controller.api.request(DeviceUpgradeRequest.create(self._obj_id))
diff --git a/mypy.ini b/mypy.ini
index 6f6e1921bb5..846f91303e6 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -2463,6 +2463,16 @@ disallow_untyped_defs = true
 warn_return_any = true
 warn_unreachable = true
 
+[mypy-homeassistant.components.unifi.update]
+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.unifiprotect.*]
 check_untyped_defs = true
 disallow_incomplete_defs = true
-- 
GitLab


From 16d3cc905f7d9b747d5f1de430dc2a0e9b52c452 Mon Sep 17 00:00:00 2001
From: Fredrik Erlandsson <fredrik.e@gmail.com>
Date: Sun, 23 Oct 2022 19:54:11 +0200
Subject: [PATCH 709/985] Bump pydaikin to 2.8.0 (#80823)

---
 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 0657f597a5d..0bb1324fbe0 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.2"],
+  "requirements": ["pydaikin==2.8.0"],
   "codeowners": ["@fredrike"],
   "zeroconf": ["_dkapi._tcp.local."],
   "quality_scale": "platinum",
diff --git a/requirements_all.txt b/requirements_all.txt
index b325887c6cd..0834663953b 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.2
+pydaikin==2.8.0
 
 # homeassistant.components.danfoss_air
 pydanfossair==0.1.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 13a5db7baf1..a451e4d1054 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.2
+pydaikin==2.8.0
 
 # homeassistant.components.deconz
 pydeconz==105
-- 
GitLab


From 0444dd71a645f29e7285e9c0f1a064ea2aa826a7 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Sun, 23 Oct 2022 20:28:45 +0200
Subject: [PATCH 710/985] Refactor UniFi outlet switches (#80738)

* Rewrite UniFi outlet switches

* Bump aiounifi to v41

* Remove devices from items_added input
---
 homeassistant/components/unifi/manifest.json |   2 +-
 homeassistant/components/unifi/switch.py     | 127 ++++++++++---------
 requirements_all.txt                         |   2 +-
 requirements_test_all.txt                    |   2 +-
 tests/components/unifi/test_switch.py        |  80 +++++++-----
 5 files changed, 117 insertions(+), 96 deletions(-)

diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json
index ad5178c2d29..5b96560f8c5 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==40"],
+  "requirements": ["aiounifi==41"],
   "codeowners": ["@Kane610"],
   "quality_scale": "platinum",
   "ssdp": [
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index 68fe84b6b60..c88af43cc78 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -20,7 +20,6 @@ from aiounifi.models.event import EventKey
 
 from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import ATTR_NAME
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers import entity_registry as er
 from homeassistant.helpers.device_registry import (
@@ -89,12 +88,9 @@ async def async_setup_entry(
     @callback
     def items_added(
         clients: set = controller.api.clients,
-        devices: set = controller.api.devices,
         dpi_groups: set = controller.api.dpi_groups,
     ) -> None:
         """Update the values of the controller."""
-        add_outlet_entities(controller, async_add_entities, devices)
-
         if controller.option_block_clients:
             add_block_entities(controller, async_add_entities, clients)
 
@@ -112,6 +108,18 @@ async def async_setup_entry(
     items_added()
     known_poe_clients.clear()
 
+    @callback
+    def async_add_outlet_switch(_: ItemEvent, obj_id: str) -> None:
+        """Add power outlet switch from UniFi controller."""
+        if not controller.api.outlets[obj_id].has_relay:
+            return
+        async_add_entities([UnifiOutletSwitch(obj_id, controller)])
+
+    controller.api.ports.subscribe(async_add_outlet_switch, ItemEvent.ADDED)
+
+    for index in controller.api.outlets:
+        async_add_outlet_switch(ItemEvent.ADDED, index)
+
     @callback
     def async_add_poe_switch(_: ItemEvent, obj_id: str) -> None:
         """Add port PoE switch from UniFi controller."""
@@ -207,25 +215,6 @@ def add_dpi_entities(controller, async_add_entities, dpi_groups):
     async_add_entities(switches)
 
 
-@callback
-def add_outlet_entities(controller, async_add_entities, devices):
-    """Add new switch entities from the controller."""
-    switches = []
-
-    for mac in devices:
-        if (
-            mac in controller.entities[DOMAIN][OUTLET_SWITCH]
-            or not (device := controller.api.devices[mac]).outlet_table
-        ):
-            continue
-
-        for outlet in device.outlets.values():
-            if outlet.has_relay:
-                switches.append(UniFiOutletSwitch(device, controller, outlet.index))
-
-    async_add_entities(switches)
-
-
 class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity):
     """Representation of a client that uses POE."""
 
@@ -506,64 +495,77 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity):
         )
 
 
-class UniFiOutletSwitch(UniFiBase, SwitchEntity):
+class UnifiOutletSwitch(SwitchEntity):
     """Representation of a outlet relay."""
 
-    DOMAIN = DOMAIN
-    TYPE = OUTLET_SWITCH
-
     _attr_device_class = SwitchDeviceClass.OUTLET
+    _attr_has_entity_name = True
+    _attr_should_poll = False
 
-    def __init__(self, device, controller, index):
-        """Set up outlet switch."""
-        super().__init__(device, controller)
+    def __init__(self, obj_id: str, controller) -> None:
+        """Set up UniFi Network entity base."""
+        self._device_mac, index = obj_id.split("_", 1)
+        self._index = int(index)
+        self._obj_id = obj_id
+        self.controller = controller
 
-        self._outlet_index = index
+        outlet = self.controller.api.outlets[self._obj_id]
+        self._attr_name = outlet.name
+        self._attr_is_on = outlet.relay_state
+        self._attr_unique_id = f"{self._device_mac}-outlet-{index}"
 
-        self._attr_name = f"{device.name or device.model} {device.outlets[index].name}"
-        self._attr_unique_id = f"{device.mac}-outlet-{index}"
+        device = self.controller.api.devices[self._device_mac]
+        self._attr_available = controller.available and not device.disabled
+        self._attr_device_info = DeviceInfo(
+            connections={(CONNECTION_NETWORK_MAC, device.mac)},
+            manufacturer=ATTR_MANUFACTURER,
+            model=device.model,
+            name=device.name or None,
+            sw_version=device.version,
+            hw_version=device.board_revision,
+        )
 
-    @property
-    def is_on(self):
-        """Return true if outlet is active."""
-        return self._item.outlets[self._outlet_index].relay_state
+    async def async_added_to_hass(self) -> None:
+        """Entity created."""
+        self.async_on_remove(
+            self.controller.api.outlets.subscribe(self.async_signalling_callback)
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                self.controller.signal_reachable,
+                self.async_signal_reachable_callback,
+            )
+        )
 
-    @property
-    def available(self) -> bool:
-        """Return if switch is available."""
-        return not self._item.disabled and self.controller.available
+    @callback
+    def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
+        """Object has new event."""
+        device = self.controller.api.devices[self._device_mac]
+        outlet = self.controller.api.outlets[self._obj_id]
+        self._attr_available = self.controller.available and not device.disabled
+        self._attr_is_on = outlet.relay_state
+        self.async_write_ha_state()
+
+    @callback
+    def async_signal_reachable_callback(self) -> None:
+        """Call when controller connection state change."""
+        self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
 
     async def async_turn_on(self, **kwargs: Any) -> None:
         """Enable outlet relay."""
+        device = self.controller.api.devices[self._device_mac]
         await self.controller.api.request(
-            DeviceSetOutletRelayRequest.create(self._item, self._outlet_index, True)
+            DeviceSetOutletRelayRequest.create(device, self._index, True)
         )
 
     async def async_turn_off(self, **kwargs: Any) -> None:
         """Disable outlet relay."""
+        device = self.controller.api.devices[self._device_mac]
         await self.controller.api.request(
-            DeviceSetOutletRelayRequest.create(self._item, self._outlet_index, False)
+            DeviceSetOutletRelayRequest.create(device, self._index, False)
         )
 
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return a device description for device registry."""
-        info = DeviceInfo(
-            connections={(CONNECTION_NETWORK_MAC, self._item.mac)},
-            manufacturer=ATTR_MANUFACTURER,
-            model=self._item.model,
-            sw_version=self._item.version,
-            hw_version=self._item.board_revision,
-        )
-
-        if self._item.name:
-            info[ATTR_NAME] = self._item.name
-
-        return info
-
-    async def options_updated(self) -> None:
-        """Config entry options are updated, no options to act on."""
-
 
 class UnifiPoePortSwitch(SwitchEntity):
     """Representation of a Power-over-Ethernet source port on an UniFi device."""
@@ -594,6 +596,7 @@ class UnifiPoePortSwitch(SwitchEntity):
             model=device.model,
             name=device.name or None,
             sw_version=device.version,
+            hw_version=device.board_revision,
         )
 
     async def async_added_to_hass(self) -> None:
diff --git a/requirements_all.txt b/requirements_all.txt
index 0834663953b..e61e412f036 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==40
+aiounifi==41
 
 # homeassistant.components.vlc_telnet
 aiovlc==0.1.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index a451e4d1054..a0a95796a5c 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==40
+aiounifi==41
 
 # homeassistant.components.vlc_telnet
 aiovlc==0.1.0
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index e0bac2c3eb3..12ef6f9b965 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -128,6 +128,7 @@ POE_SWITCH_CLIENTS = [
 ]
 
 DEVICE_1 = {
+    "board_rev": 2,
     "device_id": "mock-id",
     "ip": "10.0.1.1",
     "mac": "00:00:00:00:01:01",
@@ -413,9 +414,16 @@ OUTLET_UP1 = {
             "index": 1,
             "has_relay": True,
             "has_metering": False,
+            "relay_state": True,
+            "name": "Outlet 1",
+        },
+        {
+            "index": 2,
+            "has_relay": False,
+            "has_metering": False,
             "relay_state": False,
             "name": "Outlet 1",
-        }
+        },
     ],
     "element_ap_serial": "44:d9:e7:90:f4:24",
     "connected_at": 1641678609,
@@ -910,34 +918,27 @@ async def test_dpi_switches_add_second_app(hass, aioclient_mock, mock_unifi_webs
 
 
 async def test_outlet_switches(hass, aioclient_mock, mock_unifi_websocket):
-    """Test the update_items function with some clients."""
+    """Test the outlet entities."""
     config_entry = await setup_unifi_integration(
-        hass,
-        aioclient_mock,
-        options={CONF_TRACK_DEVICES: False},
-        devices_response=[OUTLET_UP1],
+        hass, aioclient_mock, devices_response=[OUTLET_UP1]
     )
     controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
-
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
 
-    outlet = hass.states.get("switch.plug_outlet_1")
-    assert outlet is not None
-    assert outlet.state == STATE_OFF
-
-    # State change
-
-    outlet_up1 = deepcopy(OUTLET_UP1)
-    outlet_up1["outlet_table"][0]["relay_state"] = True
+    # Validate state object
+    switch_1 = hass.states.get("switch.plug_outlet_1")
+    assert switch_1 is not None
+    assert switch_1.state == STATE_ON
+    assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET
 
-    mock_unifi_websocket(message=MessageKey.DEVICE, data=outlet_up1)
+    # Update state object
+    device_1 = deepcopy(OUTLET_UP1)
+    device_1["outlet_table"][0]["relay_state"] = False
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
     await hass.async_block_till_done()
+    assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF
 
-    outlet = hass.states.get("switch.plug_outlet_1")
-    assert outlet.state == STATE_ON
-
-    # Turn on and off outlet
-
+    # Turn off outlet
     aioclient_mock.clear_requests()
     aioclient_mock.put(
         f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/600c8356942a6ade50707b56",
@@ -945,33 +946,50 @@ async def test_outlet_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     await hass.services.async_call(
         SWITCH_DOMAIN,
-        SERVICE_TURN_ON,
+        SERVICE_TURN_OFF,
         {ATTR_ENTITY_ID: "switch.plug_outlet_1"},
         blocking=True,
     )
     assert aioclient_mock.call_count == 1
     assert aioclient_mock.mock_calls[0][2] == {
-        "outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": True}]
+        "outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": False}]
     }
 
+    # Turn on outlet
     await hass.services.async_call(
         SWITCH_DOMAIN,
-        SERVICE_TURN_OFF,
+        SERVICE_TURN_ON,
         {ATTR_ENTITY_ID: "switch.plug_outlet_1"},
         blocking=True,
     )
     assert aioclient_mock.call_count == 2
     assert aioclient_mock.mock_calls[1][2] == {
-        "outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": False}]
+        "outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": True}]
     }
 
-    # Changes to config entry options shouldn't affect outlets
-    hass.config_entries.async_update_entry(
-        config_entry,
-        options={CONF_BLOCK_CLIENT: []},
-    )
+    # Availability signalling
+
+    # Controller disconnects
+    mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
     await hass.async_block_till_done()
-    assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
+    assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE
+
+    # Controller reconnects
+    mock_unifi_websocket(state=WebsocketState.RUNNING)
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF
+
+    # Device gets disabled
+    device_1["disabled"] = True
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE
+
+    # Device gets re-enabled
+    device_1["disabled"] = False
+    mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1)
+    await hass.async_block_till_done()
+    assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF
 
     # Unload config entry
     await hass.config_entries.async_unload(config_entry.entry_id)
-- 
GitLab


From 18f51db15d816c6902ea73e38edf84fbbd1a1f7e Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
Date: Sun, 23 Oct 2022 13:31:24 -0500
Subject: [PATCH 711/985] Add integration_type to ipp and roku (#80831)

---
 homeassistant/components/ipp/manifest.json  | 1 +
 homeassistant/components/roku/manifest.json | 1 +
 homeassistant/generated/integrations.json   | 4 ++--
 3 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json
index 46f62993295..aadfdc8feea 100644
--- a/homeassistant/components/ipp/manifest.json
+++ b/homeassistant/components/ipp/manifest.json
@@ -2,6 +2,7 @@
   "domain": "ipp",
   "name": "Internet Printing Protocol (IPP)",
   "documentation": "https://www.home-assistant.io/integrations/ipp",
+  "integration_type": "device",
   "requirements": ["pyipp==0.12.0"],
   "codeowners": ["@ctalkington"],
   "config_flow": true,
diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json
index 910516b93e8..1a2d46e6256 100644
--- a/homeassistant/components/roku/manifest.json
+++ b/homeassistant/components/roku/manifest.json
@@ -2,6 +2,7 @@
   "domain": "roku",
   "name": "Roku",
   "documentation": "https://www.home-assistant.io/integrations/roku",
+  "integration_type": "device",
   "requirements": ["rokuecp==0.17.0"],
   "homekit": {
     "models": ["3820X", "3810X", "4660X", "7820X", "C105X", "C135X"]
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 677436c7f81..24c48c59968 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2454,7 +2454,7 @@
     },
     "ipp": {
       "name": "Internet Printing Protocol (IPP)",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -4406,7 +4406,7 @@
     },
     "roku": {
       "name": "Roku",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_polling"
     },
-- 
GitLab


From 423f6aeec2796f5775798d212a3c0699d7f1f8c2 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Sun, 23 Oct 2022 20:33:53 +0200
Subject: [PATCH 712/985] Improve UniFi PoE unique ID (#80740)

* Improve UniFi PoE unique ID

* Set available property on creation
---
 homeassistant/components/unifi/switch.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index c88af43cc78..5e129cc402e 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -579,17 +579,18 @@ class UnifiPoePortSwitch(SwitchEntity):
 
     def __init__(self, obj_id: str, controller) -> None:
         """Set up UniFi Network entity base."""
-        self._attr_unique_id = f"{obj_id}-PoE"
-        self._device_mac, port_idx = obj_id.split("_", 1)
-        self._port_idx = int(port_idx)
+        self._device_mac, index = obj_id.split("_", 1)
+        self._index = int(index)
         self._obj_id = obj_id
         self.controller = controller
 
         port = self.controller.api.ports[self._obj_id]
         self._attr_name = f"{port.name} PoE"
         self._attr_is_on = port.poe_mode != "off"
+        self._attr_unique_id = f"{self._device_mac}-poe-{index}"
 
         device = self.controller.api.devices[self._device_mac]
+        self._attr_available = controller.available and not device.disabled
         self._attr_device_info = DeviceInfo(
             connections={(CONNECTION_NETWORK_MAC, device.mac)},
             manufacturer=ATTR_MANUFACTURER,
@@ -630,12 +631,12 @@ class UnifiPoePortSwitch(SwitchEntity):
         """Enable POE for client."""
         device = self.controller.api.devices[self._device_mac]
         await self.controller.api.request(
-            DeviceSetPoePortModeRequest.create(device, self._port_idx, "auto")
+            DeviceSetPoePortModeRequest.create(device, self._index, "auto")
         )
 
     async def async_turn_off(self, **kwargs: Any) -> None:
         """Disable POE for client."""
         device = self.controller.api.devices[self._device_mac]
         await self.controller.api.request(
-            DeviceSetPoePortModeRequest.create(device, self._port_idx, "off")
+            DeviceSetPoePortModeRequest.create(device, self._index, "off")
         )
-- 
GitLab


From 3df73259ddec078aac175611666b82a305ceeecd Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 23 Oct 2022 13:41:32 -0500
Subject: [PATCH 713/985] Prevent HomeKit Controller BLE connect retries from
 blocking startup (#80827)

---
 homeassistant/components/homekit_controller/connection.py | 7 +++++--
 homeassistant/components/homekit_controller/manifest.json | 2 +-
 requirements_all.txt                                      | 2 +-
 requirements_test_all.txt                                 | 2 +-
 4 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py
index e2ab68f8c63..343fdc565f1 100644
--- a/homeassistant/components/homekit_controller/connection.py
+++ b/homeassistant/components/homekit_controller/connection.py
@@ -20,7 +20,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.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
 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
@@ -192,8 +192,11 @@ class HKDevice:
         # Ideally we would know which entities we are about to add
         # so we only poll those chars but that is not possible
         # yet.
+        attempts = None if self.hass.state == CoreState.running else 1
         try:
-            await self.pairing.async_populate_accessories_state(force_update=True)
+            await self.pairing.async_populate_accessories_state(
+                force_update=True, attempts=attempts
+            )
         except AccessoryNotFoundError:
             if transport != Transport.BLE or not pairing.accessories:
                 # BLE devices may sleep and we can't force a connection
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index 20e19c7b36c..e6182ce1f68 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.0"],
+  "requirements": ["aiohomekit==2.2.1"],
   "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 e61e412f036..0d7a0a9dd83 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.0
+aiohomekit==2.2.1
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index a0a95796a5c..9122b66de97 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.0
+aiohomekit==2.2.1
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From 9734086d9b126e09f766d0d78f59d96bf824ca10 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
Date: Sun, 23 Oct 2022 13:42:25 -0500
Subject: [PATCH 714/985] Add integration_type to jellyfin (#80832)

---
 homeassistant/components/jellyfin/manifest.json | 1 +
 homeassistant/generated/integrations.json       | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json
index 674aab64e0b..6c2cdb98ae4 100644
--- a/homeassistant/components/jellyfin/manifest.json
+++ b/homeassistant/components/jellyfin/manifest.json
@@ -3,6 +3,7 @@
   "name": "Jellyfin",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/jellyfin",
+  "integration_type": "service",
   "requirements": ["jellyfin-apiclient-python==1.9.2"],
   "iot_class": "local_polling",
   "codeowners": ["@j-stienstra", "@ctalkington"],
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 24c48c59968..d85f235ccd2 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2506,7 +2506,7 @@
     },
     "jellyfin": {
       "name": "Jellyfin",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "local_polling"
     },
-- 
GitLab


From d4a5393f7b976120f6e42df395049b453cae8a64 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Sun, 23 Oct 2022 20:43:13 +0200
Subject: [PATCH 715/985] Set integration type on codeowned integrations
 @frenck (#80830)

---
 .../components/cpuspeed/manifest.json         |  3 +-
 .../components/debugpy/manifest.json          |  3 +-
 homeassistant/components/dsmr/manifest.json   |  1 +
 homeassistant/components/elgato/manifest.json |  3 +-
 .../components/forecast_solar/manifest.json   |  3 +-
 .../components/lametric/manifest.json         |  3 +-
 .../components/luftdaten/manifest.json        |  1 +
 homeassistant/components/moon/manifest.json   |  1 +
 .../components/open_meteo/manifest.json       |  1 +
 .../components/plugwise/manifest.json         |  1 +
 .../components/pvoutput/manifest.json         |  3 +-
 .../components/radio_browser/manifest.json    |  1 +
 homeassistant/components/rdw/manifest.json    |  1 +
 homeassistant/components/season/manifest.json |  1 +
 homeassistant/components/sentry/manifest.json |  1 +
 .../components/solaredge/manifest.json        |  1 +
 .../components/spotify/manifest.json          |  1 +
 .../components/stookalert/manifest.json       |  1 +
 .../components/tailscale/manifest.json        |  1 +
 homeassistant/components/tuya/manifest.json   |  1 +
 .../components/twentemilieu/manifest.json     |  1 +
 homeassistant/components/uptime/manifest.json |  1 +
 .../components/verisure/manifest.json         |  1 +
 homeassistant/components/whois/manifest.json  |  1 +
 homeassistant/components/wled/manifest.json   |  1 +
 homeassistant/generated/integrations.json     | 40 +++++++++----------
 26 files changed, 51 insertions(+), 26 deletions(-)

diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json
index 34628b99341..06a331d6d87 100644
--- a/homeassistant/components/cpuspeed/manifest.json
+++ b/homeassistant/components/cpuspeed/manifest.json
@@ -5,5 +5,6 @@
   "requirements": ["py-cpuinfo==8.0.0"],
   "config_flow": true,
   "codeowners": ["@fabaff", "@frenck"],
-  "iot_class": "local_push"
+  "iot_class": "local_push",
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json
index dc0818f54b6..33746d1a5dd 100644
--- a/homeassistant/components/debugpy/manifest.json
+++ b/homeassistant/components/debugpy/manifest.json
@@ -5,5 +5,6 @@
   "requirements": ["debugpy==1.6.3"],
   "codeowners": ["@frenck"],
   "quality_scale": "internal",
-  "iot_class": "local_push"
+  "iot_class": "local_push",
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json
index b086944a4d2..e81fb5f801c 100644
--- a/homeassistant/components/dsmr/manifest.json
+++ b/homeassistant/components/dsmr/manifest.json
@@ -6,5 +6,6 @@
   "codeowners": ["@Robbie1221", "@frenck"],
   "config_flow": true,
   "iot_class": "local_push",
+  "integration_type": "hub",
   "loggers": ["dsmr_parser"]
 }
diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json
index 91e6d09d973..8311d6f7fb5 100644
--- a/homeassistant/components/elgato/manifest.json
+++ b/homeassistant/components/elgato/manifest.json
@@ -7,5 +7,6 @@
   "zeroconf": ["_elg._tcp.local."],
   "codeowners": ["@frenck"],
   "quality_scale": "platinum",
-  "iot_class": "local_polling"
+  "iot_class": "local_polling",
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json
index 472f5cac213..a3ae91b4a92 100644
--- a/homeassistant/components/forecast_solar/manifest.json
+++ b/homeassistant/components/forecast_solar/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["forecast_solar==2.2.0"],
   "codeowners": ["@klaasnicolaas", "@frenck"],
   "quality_scale": "platinum",
-  "iot_class": "cloud_polling"
+  "iot_class": "cloud_polling",
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json
index fcfcc082c35..26963e136ed 100644
--- a/homeassistant/components/lametric/manifest.json
+++ b/homeassistant/components/lametric/manifest.json
@@ -14,5 +14,6 @@
     }
   ],
   "dhcp": [{ "registered_devices": true }],
-  "quality_scale": "platinum"
+  "quality_scale": "platinum",
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json
index 255dc8c52ea..aed8d80f8b1 100644
--- a/homeassistant/components/luftdaten/manifest.json
+++ b/homeassistant/components/luftdaten/manifest.json
@@ -7,5 +7,6 @@
   "codeowners": ["@fabaff", "@frenck"],
   "quality_scale": "gold",
   "iot_class": "cloud_polling",
+  "integration_type": "device",
   "loggers": ["luftdaten"]
 }
diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json
index 0402a87cf1a..af630a56e2a 100644
--- a/homeassistant/components/moon/manifest.json
+++ b/homeassistant/components/moon/manifest.json
@@ -5,5 +5,6 @@
   "codeowners": ["@fabaff", "@frenck"],
   "quality_scale": "internal",
   "iot_class": "local_polling",
+  "integration_type": "service",
   "config_flow": true
 }
diff --git a/homeassistant/components/open_meteo/manifest.json b/homeassistant/components/open_meteo/manifest.json
index ccfc7dbd51d..8a6d1561d96 100644
--- a/homeassistant/components/open_meteo/manifest.json
+++ b/homeassistant/components/open_meteo/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["open-meteo==0.2.1"],
   "dependencies": ["zone"],
   "codeowners": ["@frenck"],
+  "integration_type": "service",
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json
index da639e9f918..7f3e979ab7d 100644
--- a/homeassistant/components/plugwise/manifest.json
+++ b/homeassistant/components/plugwise/manifest.json
@@ -7,5 +7,6 @@
   "zeroconf": ["_plugwise._tcp.local."],
   "config_flow": true,
   "iot_class": "local_polling",
+  "integration_type": "hub",
   "loggers": ["crcmod", "plugwise"]
 }
diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json
index 947238df833..3a77e5b2ddf 100644
--- a/homeassistant/components/pvoutput/manifest.json
+++ b/homeassistant/components/pvoutput/manifest.json
@@ -6,5 +6,6 @@
   "codeowners": ["@frenck"],
   "requirements": ["pvo==0.2.2"],
   "iot_class": "cloud_polling",
-  "quality_scale": "platinum"
+  "quality_scale": "platinum",
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/radio_browser/manifest.json b/homeassistant/components/radio_browser/manifest.json
index 9c6858ae27e..83a87bb800f 100644
--- a/homeassistant/components/radio_browser/manifest.json
+++ b/homeassistant/components/radio_browser/manifest.json
@@ -5,5 +5,6 @@
   "documentation": "https://www.home-assistant.io/integrations/radio",
   "requirements": ["radios==0.1.1"],
   "codeowners": ["@frenck"],
+  "integration_type": "service",
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json
index 774d0234d06..da0f076b3d8 100644
--- a/homeassistant/components/rdw/manifest.json
+++ b/homeassistant/components/rdw/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["vehicle==0.4.0"],
   "codeowners": ["@frenck"],
   "quality_scale": "platinum",
+  "integration_type": "service",
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json
index 7b6feeca8a4..a921d395310 100644
--- a/homeassistant/components/season/manifest.json
+++ b/homeassistant/components/season/manifest.json
@@ -7,5 +7,6 @@
   "quality_scale": "internal",
   "iot_class": "local_polling",
   "loggers": ["ephem"],
+  "integration_type": "service",
   "config_flow": true
 }
diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json
index cc28c2b7ff5..1c4b00e25cc 100644
--- a/homeassistant/components/sentry/manifest.json
+++ b/homeassistant/components/sentry/manifest.json
@@ -5,5 +5,6 @@
   "documentation": "https://www.home-assistant.io/integrations/sentry",
   "requirements": ["sentry-sdk==1.10.0"],
   "codeowners": ["@dcramer", "@frenck"],
+  "integration_type": "service",
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json
index e5c9520f96b..0bd5be1eaec 100644
--- a/homeassistant/components/solaredge/manifest.json
+++ b/homeassistant/components/solaredge/manifest.json
@@ -12,5 +12,6 @@
     }
   ],
   "iot_class": "cloud_polling",
+  "integration_type": "device",
   "loggers": ["solaredge"]
 }
diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json
index 2940700d230..0ce71f371df 100644
--- a/homeassistant/components/spotify/manifest.json
+++ b/homeassistant/components/spotify/manifest.json
@@ -9,5 +9,6 @@
   "config_flow": true,
   "quality_scale": "silver",
   "iot_class": "cloud_polling",
+  "integration_type": "service",
   "loggers": ["spotipy"]
 }
diff --git a/homeassistant/components/stookalert/manifest.json b/homeassistant/components/stookalert/manifest.json
index 401ed5a27e5..cd76a52992e 100644
--- a/homeassistant/components/stookalert/manifest.json
+++ b/homeassistant/components/stookalert/manifest.json
@@ -5,5 +5,6 @@
   "documentation": "https://www.home-assistant.io/integrations/stookalert",
   "codeowners": ["@fwestenberg", "@frenck"],
   "requirements": ["stookalert==0.1.4"],
+  "integration_type": "service",
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json
index 3249705ce7c..d59af231dfe 100644
--- a/homeassistant/components/tailscale/manifest.json
+++ b/homeassistant/components/tailscale/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["tailscale==0.2.0"],
   "codeowners": ["@frenck"],
   "quality_scale": "platinum",
+  "integration_type": "hub",
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json
index cf3808819e7..6321bdefc05 100644
--- a/homeassistant/components/tuya/manifest.json
+++ b/homeassistant/components/tuya/manifest.json
@@ -20,5 +20,6 @@
     { "macaddress": "D4A651*" },
     { "macaddress": "D81F12*" }
   ],
+  "integration_type": "hub",
   "loggers": ["tuya_iot"]
 }
diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json
index c7dd2508fd8..348c9929426 100644
--- a/homeassistant/components/twentemilieu/manifest.json
+++ b/homeassistant/components/twentemilieu/manifest.json
@@ -7,5 +7,6 @@
   "codeowners": ["@frenck"],
   "quality_scale": "platinum",
   "iot_class": "cloud_polling",
+  "integration_type": "service",
   "loggers": ["twentemilieu"]
 }
diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json
index 3bcc47815f8..ef814725699 100644
--- a/homeassistant/components/uptime/manifest.json
+++ b/homeassistant/components/uptime/manifest.json
@@ -5,5 +5,6 @@
   "codeowners": ["@frenck"],
   "quality_scale": "internal",
   "iot_class": "local_push",
+  "integration_type": "service",
   "config_flow": true
 }
diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json
index 820b8a20f14..cc4f90bb92b 100644
--- a/homeassistant/components/verisure/manifest.json
+++ b/homeassistant/components/verisure/manifest.json
@@ -11,5 +11,6 @@
     }
   ],
   "iot_class": "cloud_polling",
+  "integration_type": "hub",
   "loggers": ["verisure"]
 }
diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json
index 00a2821c8c4..104b583ea3a 100644
--- a/homeassistant/components/whois/manifest.json
+++ b/homeassistant/components/whois/manifest.json
@@ -6,5 +6,6 @@
   "config_flow": true,
   "codeowners": ["@frenck"],
   "iot_class": "cloud_polling",
+  "integration_type": "service",
   "loggers": ["whois"]
 }
diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json
index 2fc00131fac..3566349a853 100644
--- a/homeassistant/components/wled/manifest.json
+++ b/homeassistant/components/wled/manifest.json
@@ -7,5 +7,6 @@
   "zeroconf": ["_wled._tcp.local."],
   "codeowners": ["@frenck"],
   "quality_scale": "platinum",
+  "integration_type": "device",
   "iot_class": "local_push"
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index d85f235ccd2..c04d9f28ebe 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -847,7 +847,7 @@
       "supported_by": "overkiz"
     },
     "cpuspeed": {
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_push"
     },
@@ -906,7 +906,7 @@
     },
     "debugpy": {
       "name": "Remote Python Debugger",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": false,
       "iot_class": "local_push"
     },
@@ -1255,7 +1255,7 @@
           "name": "Elgato Avea"
         },
         "elgato": {
-          "integration_type": "hub",
+          "integration_type": "device",
           "config_flow": true,
           "iot_class": "local_polling",
           "name": "Elgato Light"
@@ -1656,7 +1656,7 @@
     },
     "forecast_solar": {
       "name": "Forecast.Solar",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -2668,7 +2668,7 @@
     },
     "lametric": {
       "name": "LaMetric",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -2896,7 +2896,7 @@
     },
     "luftdaten": {
       "name": "Sensor.Community",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -3248,7 +3248,7 @@
       "iot_class": "local_polling"
     },
     "moon": {
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -3687,7 +3687,7 @@
     },
     "open_meteo": {
       "name": "Open-Meteo",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4111,7 +4111,7 @@
     },
     "pvoutput": {
       "name": "PVOutput",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4204,7 +4204,7 @@
     },
     "radio_browser": {
       "name": "Radio Browser",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4276,7 +4276,7 @@
     },
     "rdw": {
       "name": "RDW",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4538,7 +4538,7 @@
     },
     "season": {
       "name": "Season",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -4585,7 +4585,7 @@
     },
     "sentry": {
       "name": "Sentry",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4858,7 +4858,7 @@
       "name": "SolarEdge",
       "integrations": {
         "solaredge": {
-          "integration_type": "hub",
+          "integration_type": "device",
           "config_flow": true,
           "iot_class": "cloud_polling",
           "name": "SolarEdge"
@@ -4977,7 +4977,7 @@
     },
     "spotify": {
       "name": "Spotify",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -5043,7 +5043,7 @@
     },
     "stookalert": {
       "name": "RIVM Stookalert",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -5544,7 +5544,7 @@
     },
     "twentemilieu": {
       "name": "Twente Milieu",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -5671,7 +5671,7 @@
       "supported_by": "motion_blinds"
     },
     "uptime": {
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "local_push"
     },
@@ -5895,7 +5895,7 @@
     },
     "whois": {
       "name": "Whois",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -5931,7 +5931,7 @@
     },
     "wled": {
       "name": "WLED",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_push"
     },
-- 
GitLab


From 57b2bb488916d68e753e1f2b70e7aec1f995b346 Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
Date: Sun, 23 Oct 2022 20:55:53 +0200
Subject: [PATCH 716/985] Add entity service - Set Full AC state to Sensibo
 (#80820)

Co-authored-by: J. Nick Koston <nick@koston.org>
---
 homeassistant/components/sensibo/climate.py   | 63 +++++++++++++
 .../components/sensibo/services.yaml          | 66 +++++++++++++
 tests/components/sensibo/test_climate.py      | 93 +++++++++++++++++++
 3 files changed, 222 insertions(+)

diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py
index 9c3c5c111f5..208e61550e6 100644
--- a/homeassistant/components/sensibo/climate.py
+++ b/homeassistant/components/sensibo/climate.py
@@ -7,12 +7,15 @@ from typing import TYPE_CHECKING, Any
 import voluptuous as vol
 
 from homeassistant.components.climate import (
+    ATTR_FAN_MODE,
+    ATTR_SWING_MODE,
     ClimateEntity,
     ClimateEntityFeature,
     HVACMode,
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
+    ATTR_MODE,
     ATTR_STATE,
     ATTR_TEMPERATURE,
     PRECISION_TENTHS,
@@ -34,12 +37,16 @@ SERVICE_ENABLE_TIMER = "enable_timer"
 ATTR_MINUTES = "minutes"
 SERVICE_ENABLE_PURE_BOOST = "enable_pure_boost"
 SERVICE_DISABLE_PURE_BOOST = "disable_pure_boost"
+SERVICE_FULL_STATE = "full_state"
 
 ATTR_AC_INTEGRATION = "ac_integration"
 ATTR_GEO_INTEGRATION = "geo_integration"
 ATTR_INDOOR_INTEGRATION = "indoor_integration"
 ATTR_OUTDOOR_INTEGRATION = "outdoor_integration"
 ATTR_SENSITIVITY = "sensitivity"
+ATTR_TARGET_TEMPERATURE = "target_temperature"
+ATTR_HORIZONTAL_SWING_MODE = "horizontal_swing_mode"
+ATTR_LIGHT = "light"
 BOOST_INCLUSIVE = "boost_inclusive"
 
 PARALLEL_UPDATES = 0
@@ -118,6 +125,20 @@ async def async_setup_entry(
         },
         "async_enable_pure_boost",
     )
+    platform.async_register_entity_service(
+        SERVICE_FULL_STATE,
+        {
+            vol.Required(ATTR_MODE): vol.In(
+                ["cool", "heat", "fan", "auto", "dry", "off"]
+            ),
+            vol.Optional(ATTR_TARGET_TEMPERATURE): int,
+            vol.Optional(ATTR_FAN_MODE): str,
+            vol.Optional(ATTR_SWING_MODE): str,
+            vol.Optional(ATTR_HORIZONTAL_SWING_MODE): str,
+            vol.Optional(ATTR_LIGHT): vol.In(["on", "off"]),
+        },
+        "async_full_ac_state",
+    )
 
 
 class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
@@ -335,6 +356,37 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
             assumed_state=True,
         )
 
+    async def async_full_ac_state(
+        self,
+        mode: str,
+        target_temperature: int | None = None,
+        fan_mode: str | None = None,
+        swing_mode: str | None = None,
+        horizontal_swing_mode: str | None = None,
+        light: str | None = None,
+    ) -> None:
+        """Set full AC state."""
+        new_ac_state = self.device_data.ac_states
+        new_ac_state.pop("timestamp")
+        new_ac_state["on"] = False
+        if mode != "off":
+            new_ac_state["on"] = True
+            new_ac_state["mode"] = mode
+        if target_temperature:
+            new_ac_state["targetTemperature"] = target_temperature
+        if fan_mode:
+            new_ac_state["fanLevel"] = fan_mode
+        if swing_mode:
+            new_ac_state["swing"] = swing_mode
+        if horizontal_swing_mode:
+            new_ac_state["horizontalSwing"] = horizontal_swing_mode
+        if light:
+            new_ac_state["light"] = light
+
+        await self.api_call_custom_service_full_ac_state(
+            key="hvac_mode", value=mode, data=new_ac_state
+        )
+
     async def async_enable_timer(self, minutes: int) -> None:
         """Enable the timer."""
         new_state = bool(self.device_data.ac_states["on"] is False)
@@ -419,3 +471,14 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
         result = {}
         result = await self._client.async_set_pureboost(self._device_id, data)
         return bool(result.get("status") == "success")
+
+    @async_handle_api_call
+    async def api_call_custom_service_full_ac_state(
+        self,
+        key: str,
+        value: Any,
+        data: dict,
+    ) -> bool:
+        """Make service call to api."""
+        result = await self._client.async_set_ac_states(self._device_id, data)
+        return bool(result.get("result", {}).get("status") == "Success")
diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml
index 9ce13b70eaa..5ff78b2c34e 100644
--- a/homeassistant/components/sensibo/services.yaml
+++ b/homeassistant/components/sensibo/services.yaml
@@ -80,3 +80,69 @@ enable_pure_boost:
           options:
             - "Normal"
             - "Sensitive"
+full_state:
+  name: Set full state
+  description: Set full state for Sensibo device
+  target:
+    entity:
+      integration: sensibo
+      domain: climate
+  fields:
+    mode:
+      name: HVAC mode
+      description: HVAC mode to set
+      required: true
+      example: "heat"
+      selector:
+        select:
+          options:
+            - "cool"
+            - "heat"
+            - "fan"
+            - "auto"
+            - "dry"
+            - "off"
+    target_temperature:
+      name: Target Temperature
+      description: Optionally set target temperature
+      required: false
+      example: 23
+      selector:
+        number:
+          min: 0
+          step: 1
+          mode: box
+    fan_mode:
+      name: Fan mode
+      description: Optionally set fan mode
+      required: false
+      example: "low"
+      selector:
+        text:
+          type: text
+    swing_mode:
+      name: swing mode
+      description: Optionally set swing mode
+      required: false
+      example: "fixedBottom"
+      selector:
+        text:
+          type: text
+    horizontal_swing_mode:
+      name: Horizontal swing mode
+      description: Optionally set horizontal swing mode
+      required: false
+      example: "fixedLeft"
+      selector:
+        text:
+          type: text
+    light:
+      name: Light
+      description: Set light on or off
+      required: false
+      example: "on"
+      selector:
+        select:
+          options:
+            - "on"
+            - "off"
diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py
index f9c3a7cb301..555fc7f2ea5 100644
--- a/tests/components/sensibo/test_climate.py
+++ b/tests/components/sensibo/test_climate.py
@@ -23,19 +23,24 @@ from homeassistant.components.climate import (
 from homeassistant.components.sensibo.climate import (
     ATTR_AC_INTEGRATION,
     ATTR_GEO_INTEGRATION,
+    ATTR_HORIZONTAL_SWING_MODE,
     ATTR_INDOOR_INTEGRATION,
+    ATTR_LIGHT,
     ATTR_MINUTES,
     ATTR_OUTDOOR_INTEGRATION,
     ATTR_SENSITIVITY,
+    ATTR_TARGET_TEMPERATURE,
     SERVICE_ASSUME_STATE,
     SERVICE_ENABLE_PURE_BOOST,
     SERVICE_ENABLE_TIMER,
+    SERVICE_FULL_STATE,
     _find_valid_target_temp,
 )
 from homeassistant.components.sensibo.const import DOMAIN
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
     ATTR_ENTITY_ID,
+    ATTR_MODE,
     ATTR_STATE,
     ATTR_TEMPERATURE,
     SERVICE_TURN_OFF,
@@ -916,3 +921,91 @@ async def test_climate_pure_boost(
     assert state2.state == "on"
     assert state3.state == "on"
     assert state4.state == "s"
+
+
+async def test_climate_full_ac_state(
+    hass: HomeAssistant,
+    entity_registry_enabled_by_default: AsyncMock,
+    load_int: ConfigEntry,
+    monkeypatch: pytest.MonkeyPatch,
+    get_data: SensiboData,
+) -> None:
+    """Test the Sensibo climate Full AC state service."""
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state_climate = hass.states.get("climate.hallway")
+    assert state_climate.state == "heat"
+
+    with patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
+    ), patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states",
+    ):
+        with pytest.raises(MultipleInvalid):
+            await hass.services.async_call(
+                DOMAIN,
+                SERVICE_FULL_STATE,
+                {
+                    ATTR_ENTITY_ID: state_climate.entity_id,
+                    ATTR_TARGET_TEMPERATURE: 22,
+                },
+                blocking=True,
+            )
+    await hass.async_block_till_done()
+
+    with patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ), patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states",
+        return_value={"result": {"status": "Success"}},
+    ):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_FULL_STATE,
+            {
+                ATTR_ENTITY_ID: state_climate.entity_id,
+                ATTR_MODE: "cool",
+                ATTR_TARGET_TEMPERATURE: 22,
+                ATTR_FAN_MODE: "high",
+                ATTR_SWING_MODE: "stopped",
+                ATTR_HORIZONTAL_SWING_MODE: "stopped",
+                ATTR_LIGHT: "on",
+            },
+            blocking=True,
+        )
+    await hass.async_block_till_done()
+
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "cool")
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True)
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "target_temp", 22)
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_mode", "high")
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_mode", "stopped")
+    monkeypatch.setattr(
+        get_data.parsed["ABC999111"], "horizontal_swing_mode", "stopped"
+    )
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "light_mode", "on")
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state = hass.states.get("climate.hallway")
+
+    assert state.state == "cool"
+    assert state.attributes["temperature"] == 22
-- 
GitLab


From 5fbf6ce8bdd3a52333d5d75d3b8ad9c443235460 Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
Date: Sun, 23 Oct 2022 20:58:10 +0200
Subject: [PATCH 717/985] Minor cleanup Sensibo (#80835)

---
 homeassistant/components/sensibo/climate.py    |  2 --
 homeassistant/components/sensibo/services.yaml | 18 +++++++++---------
 homeassistant/components/sensibo/strings.json  |  4 ++--
 homeassistant/components/sensibo/switch.py     |  3 ---
 .../components/sensibo/translations/en.json    |  4 ++--
 5 files changed, 13 insertions(+), 18 deletions(-)

diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py
index 208e61550e6..d4891d7cdf9 100644
--- a/homeassistant/components/sensibo/climate.py
+++ b/homeassistant/components/sensibo/climate.py
@@ -456,7 +456,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
         data: dict,
     ) -> bool:
         """Make service call to api."""
-        result = {}
         result = await self._client.async_set_timer(self._device_id, data)
         return bool(result.get("status") == "success")
 
@@ -468,7 +467,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
         data: dict,
     ) -> bool:
         """Make service call to api."""
-        result = {}
         result = await self._client.async_set_pureboost(self._device_id, data)
         return bool(result.get("status") == "success")
 
diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml
index 5ff78b2c34e..0f7e97976c6 100644
--- a/homeassistant/components/sensibo/services.yaml
+++ b/homeassistant/components/sensibo/services.yaml
@@ -1,6 +1,6 @@
 assume_state:
   name: Assume state
-  description: Set Sensibo device to external state.
+  description: Set Sensibo device to external state
   target:
     entity:
       integration: sensibo
@@ -8,7 +8,7 @@ assume_state:
   fields:
     state:
       name: State
-      description: State to set.
+      description: State to set
       required: true
       example: "on"
       selector:
@@ -18,7 +18,7 @@ assume_state:
             - "off"
 enable_timer:
   name: Enable Timer
-  description: Enable the timer with custom time.
+  description: Enable the timer with custom time
   target:
     entity:
       integration: sensibo
@@ -36,7 +36,7 @@ enable_timer:
           mode: box
 enable_pure_boost:
   name: Enable Pure Boost
-  description: Enable and configure Pure Boost settings.
+  description: Enable and configure Pure Boost settings
   target:
     entity:
       integration: sensibo
@@ -44,35 +44,35 @@ enable_pure_boost:
   fields:
     ac_integration:
       name: AC Integration
-      description: Integrate with Air Conditioner.
+      description: Integrate with Air Conditioner
       required: true
       example: true
       selector:
         boolean:
     geo_integration:
       name: Geo Integration
-      description: Integrate with Presence.
+      description: Integrate with Presence
       required: true
       example: true
       selector:
         boolean:
     indoor_integration:
       name: Indoor Air Quality
-      description: Integrate with checking indoor air quality.
+      description: Integrate with checking indoor air quality
       required: true
       example: true
       selector:
         boolean:
     outdoor_integration:
       name: Outdoor Air Quality
-      description: Integrate with checking outdoor air quality.
+      description: Integrate with checking outdoor air quality
       required: true
       example: true
       selector:
         boolean:
     sensitivity:
       name: Sensitivity
-      description: Set the sensitivity for Pure Boost.
+      description: Set the sensitivity for Pure Boost
       required: true
       example: "Normal"
       selector:
diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json
index 19c6a7e594a..2af4e6043cb 100644
--- a/homeassistant/components/sensibo/strings.json
+++ b/homeassistant/components/sensibo/strings.json
@@ -17,7 +17,7 @@
           "api_key": "[%key:common::config_flow::data::api_key%]"
         },
         "data_description": {
-          "api_key": "Follow the documentation to get your api key."
+          "api_key": "Follow the documentation to get your api key"
         }
       },
       "reauth_confirm": {
@@ -25,7 +25,7 @@
           "api_key": "[%key:common::config_flow::data::api_key%]"
         },
         "data_description": {
-          "api_key": "Follow the documentation to get a new api key."
+          "api_key": "[%key:component::sensibo::config::step::user::data_description::api_key%]"
         }
       }
     }
diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py
index cefa8ece084..3b75268fa9c 100644
--- a/homeassistant/components/sensibo/switch.py
+++ b/homeassistant/components/sensibo/switch.py
@@ -139,7 +139,6 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
     @async_handle_api_call
     async def async_turn_on_timer(self, key: str, value: Any) -> bool:
         """Make service call to api for setting timer."""
-        result = {}
         new_state = bool(self.device_data.ac_states["on"] is False)
         data = {
             "minutesFromNow": 60,
@@ -151,14 +150,12 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
     @async_handle_api_call
     async def async_turn_off_timer(self, key: str, value: Any) -> bool:
         """Make service call to api for deleting timer."""
-        result = {}
         result = await self._client.async_del_timer(self._device_id)
         return bool(result.get("status") == "success")
 
     @async_handle_api_call
     async def async_turn_on_off_pure_boost(self, key: str, value: Any) -> bool:
         """Make service call to api for setting Pure Boost."""
-        result = {}
         new_state = bool(self.device_data.pure_boost_enabled is False)
         data: dict[str, Any] = {"enabled": new_state}
         if self.device_data.pure_measure_integration is None:
diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json
index 3f78e3be98d..3b73823a2f7 100644
--- a/homeassistant/components/sensibo/translations/en.json
+++ b/homeassistant/components/sensibo/translations/en.json
@@ -17,7 +17,7 @@
                     "api_key": "API Key"
                 },
                 "data_description": {
-                    "api_key": "Follow the documentation to get a new api key."
+                    "api_key": "Follow the documentation to get your api key"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "API Key"
                 },
                 "data_description": {
-                    "api_key": "Follow the documentation to get your api key."
+                    "api_key": "Follow the documentation to get your api key"
                 }
             }
         }
-- 
GitLab


From 0f50b2edd3f9114c15e39b6bec9ea53d0ef27e60 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 23 Oct 2022 14:17:42 -0500
Subject: [PATCH 718/985] Small lifx fixes (#80828)

---
 homeassistant/components/lifx/select.py | 17 +++++++++--------
 homeassistant/components/lifx/util.py   |  2 --
 2 files changed, 9 insertions(+), 10 deletions(-)

diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py
index 2abbde9aed2..fe49e9d8599 100644
--- a/homeassistant/components/lifx/select.py
+++ b/homeassistant/components/lifx/select.py
@@ -41,21 +41,22 @@ async def async_setup_entry(
 ) -> None:
     """Set up LIFX from a config entry."""
     coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+    entities: list[LIFXEntity] = []
 
     if lifx_features(coordinator.device)["infrared"]:
-        async_add_entities(
-            [
-                LIFXInfraredBrightnessSelectEntity(
-                    coordinator=coordinator, description=INFRARED_BRIGHTNESS_ENTITY
-                )
-            ]
+        entities.append(
+            LIFXInfraredBrightnessSelectEntity(
+                coordinator=coordinator, description=INFRARED_BRIGHTNESS_ENTITY
+            )
         )
 
     if lifx_features(coordinator.device)["multizone"] is True:
-        async_add_entities(
-            [LIFXThemeSelectEntity(coordinator=coordinator, description=THEME_ENTITY)]
+        entities.append(
+            LIFXThemeSelectEntity(coordinator=coordinator, description=THEME_ENTITY)
         )
 
+    async_add_entities(entities)
+
 
 class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
     """LIFX Nightvision infrared brightness configuration entity."""
diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py
index 46a087296f2..135e1a7e8e9 100644
--- a/homeassistant/components/lifx/util.py
+++ b/homeassistant/components/lifx/util.py
@@ -154,8 +154,6 @@ async def async_execute_lifx(method: Callable) -> Message:
             # us by async_timeout when we hit the OVERALL_TIMEOUT
             future.set_result(message)
 
-    # _LOGGER.debug("Sending LIFX command: %s", method)
-
     method(callb=_callback)
     result = None
 
-- 
GitLab


From 071f335fdb60d38b0b5837e0c5ba67f606e24ea2 Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
Date: Sun, 23 Oct 2022 15:57:04 -0400
Subject: [PATCH 719/985] Add dynamic generation of device triggers from keypad
 buttons (#80797)

Co-authored-by: J. Nick Koston <nick@koston.org>
---
 .../components/lutron_caseta/__init__.py      | 240 ++++++++++++------
 .../components/lutron_caseta/button.py        |  14 +-
 .../lutron_caseta/device_trigger.py           | 221 +++++++---------
 .../components/lutron_caseta/diagnostics.py   |   7 +-
 .../components/lutron_caseta/logbook.py       |  28 +-
 .../components/lutron_caseta/models.py        |  42 ++-
 .../components/lutron_caseta/switch.py        |   4 +-
 tests/components/lutron_caseta/__init__.py    |   2 +-
 tests/components/lutron_caseta/test_button.py |   2 +-
 .../lutron_caseta/test_device_trigger.py      | 115 +++++----
 .../lutron_caseta/test_diagnostics.py         |  68 ++++-
 .../components/lutron_caseta/test_logbook.py  |  20 +-
 12 files changed, 481 insertions(+), 282 deletions(-)

diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py
index dae83a045a2..5e6d656bf35 100644
--- a/homeassistant/components/lutron_caseta/__init__.py
+++ b/homeassistant/components/lutron_caseta/__init__.py
@@ -37,6 +37,7 @@ from .const import (
     CONF_CA_CERTS,
     CONF_CERTFILE,
     CONF_KEYFILE,
+    CONF_SUBTYPE,
     CONFIG_URL,
     DOMAIN,
     LUTRON_CASETA_BUTTON_EVENT,
@@ -45,10 +46,11 @@ from .const import (
 )
 from .device_trigger import (
     DEVICE_TYPE_SUBTYPE_MAP_TO_LIP,
+    KEYPAD_LEAP_BUTTON_NAME_OVERRIDE,
     LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP,
-    _lutron_model_to_device_type,
+    LUTRON_BUTTON_TRIGGER_SCHEMA,
 )
-from .models import LutronCasetaData
+from .models import LutronButton, LutronCasetaData, LutronKeypad, LutronKeypadData
 from .util import serial_to_unique_id
 
 _LOGGER = logging.getLogger(__name__)
@@ -169,24 +171,25 @@ async def async_setup_entry(
     _LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host)
     await _async_migrate_unique_ids(hass, config_entry)
 
-    devices = bridge.get_devices()
-    bridge_device = devices[BRIDGE_DEVICE_ID]
+    bridge_devices = bridge.get_devices()
+    bridge_device = bridge_devices[BRIDGE_DEVICE_ID]
+
     if not config_entry.unique_id:
         hass.config_entries.async_update_entry(
             config_entry, unique_id=serial_to_unique_id(bridge_device["serial"])
         )
 
-    buttons = bridge.buttons
     _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
-    )
-    _async_subscribe_pico_remote_events(hass, bridge, buttons)
+
+    keypad_data = _async_setup_keypads(hass, entry_id, bridge, bridge_device)
 
     # 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, device_info_by_device_id
+        bridge,
+        bridge_device,
+        keypad_data,
     )
 
     await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
@@ -218,57 +221,154 @@ def _async_register_bridge_device(
 
 
 @callback
-def _async_register_button_devices(
+def _async_setup_keypads(
     hass: HomeAssistant,
     config_entry_id: str,
     bridge: Smartbridge,
     bridge_device: dict[str, Any],
-    button_devices_by_id: dict[int, dict],
-) -> tuple[dict[str, dict], dict[int, DeviceInfo]]:
-    """Register button devices (Pico Remotes) in the device registry."""
+) -> LutronKeypadData:
+    """Register keypad devices (Keypads and 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, DeviceInfo] = {}
-    seen: set[str] = set()
+
     bridge_devices = bridge.get_devices()
+    bridge_buttons = bridge.buttons
 
-    for device in button_devices_by_id.values():
+    dr_device_id_to_keypad: dict[str, LutronKeypad] = {}
+    keypads: dict[int, LutronKeypad] = {}
+    keypad_buttons: dict[int, LutronButton] = {}
+    keypad_button_names_to_leap: dict[int, dict[str, int]] = {}
 
-        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"]]
+    for bridge_button in bridge_buttons.values():
+
+        bridge_keypad = bridge_devices[bridge_button["parent_device"]]
+        keypad_device_id = bridge_keypad["device_id"]
+        button_device_id = bridge_button["device_id"]
+
+        if not (keypad := keypads.get(keypad_device_id)):
+            # First time seeing this keypad, build keypad data and store in keypads
+            keypad = keypads[keypad_device_id] = _async_build_lutron_keypad(
+                bridge, bridge_device, bridge_keypad, keypad_device_id
+            )
 
-        ha_device_serial = _handle_none_keypad_serial(
-            ha_device, bridge_device["serial"]
+            # Register the keypad device
+            dr_device = device_registry.async_get_or_create(
+                **keypad["device_info"], config_entry_id=config_entry_id
+            )
+            keypad["dr_device_id"] = dr_device.id
+            dr_device_id_to_keypad[dr_device.id] = keypad
+
+        # Add button to parent keypad, and build keypad_buttons and keypad_button_names_to_leap
+        button = keypad_buttons[button_device_id] = LutronButton(
+            lutron_device_id=button_device_id,
+            leap_button_number=bridge_button["button_number"],
+            button_name=_get_button_name(keypad, bridge_button),
+            led_device_id=bridge_button.get("button_led"),
+            parent_keypad=keypad["lutron_device_id"],
         )
 
-        if "serial" not in ha_device or ha_device_serial in seen:
-            continue
-        seen.add(ha_device_serial)
-
-        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,
-            "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
+        keypad["buttons"].append(button["lutron_device_id"])
 
-        dr_device = device_registry.async_get_or_create(
-            **device_args, config_entry_id=config_entry_id
+        keypad_button_names_to_leap.setdefault(keypad["lutron_device_id"], {}).update(
+            {button["button_name"]: int(button["leap_button_number"])}
         )
-        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, device_info_by_device_id
+    keypad_trigger_schemas = _async_build_trigger_schemas(keypad_button_names_to_leap)
+
+    _async_subscribe_keypad_events(hass, bridge, keypads, keypad_buttons)
+
+    return LutronKeypadData(
+        dr_device_id_to_keypad,
+        keypads,
+        keypad_buttons,
+        keypad_button_names_to_leap,
+        keypad_trigger_schemas,
+    )
+
+
+@callback
+def _async_build_trigger_schemas(
+    keypad_button_names_to_leap: dict[int, dict[str, int]]
+) -> dict[int, vol.Schema]:
+    """Build device trigger schemas."""
+
+    return {
+        keypad_id: LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
+            {
+                vol.Required(CONF_SUBTYPE): vol.In(
+                    keypad_button_names_to_leap[keypad_id]
+                ),
+            }
+        )
+        for keypad_id in keypad_button_names_to_leap
+    }
+
+
+@callback
+def _async_build_lutron_keypad(
+    bridge: Smartbridge,
+    bridge_device: dict[str, Any],
+    bridge_keypad: dict[str, Any],
+    keypad_device_id: int,
+) -> LutronKeypad:
+    # First time seeing this keypad, build keypad data and store in keypads
+
+    area_name = _area_name_from_id(bridge.areas, bridge_keypad["area"])
+    keypad_name = bridge_keypad["name"].split("_")[-1]
+    keypad_serial = _handle_none_keypad_serial(bridge_keypad, bridge_device["serial"])
+    device_info = DeviceInfo(
+        name=f"{area_name} {keypad_name}",
+        manufacturer=MANUFACTURER,
+        identifiers={(DOMAIN, keypad_serial)},
+        model=f"{bridge_keypad['model']} ({bridge_keypad['type']})",
+        via_device=(DOMAIN, bridge_device["serial"]),
+    )
+    if area_name != UNASSIGNED_AREA:
+        device_info["suggested_area"] = area_name
+
+    return LutronKeypad(
+        lutron_device_id=keypad_device_id,
+        dr_device_id="",
+        area_id=bridge_keypad["area"],
+        area_name=area_name,
+        name=keypad_name,
+        serial=keypad_serial,
+        device_info=device_info,
+        model=bridge_keypad["model"],
+        type=bridge_keypad["type"],
+        buttons=[],
+    )
+
+
+def _get_button_name(keypad: LutronKeypad, bridge_button: dict[str, Any]) -> str:
+    """Get the LEAP button name and check for override."""
+
+    button_number = bridge_button["button_number"]
+    button_name = bridge_button.get("device_name")
+
+    if button_name is None:
+        # This is a Caseta Button retrieve name from hardcoded trigger definitions.
+        return _get_button_name_from_triggers(keypad, button_number)
+
+    keypad_model = keypad["model"]
+    if keypad_model_override := KEYPAD_LEAP_BUTTON_NAME_OVERRIDE.get(keypad_model):
+        if alt_button_name := keypad_model_override.get(button_number):
+            return alt_button_name
+
+    return button_name
+
+
+def _get_button_name_from_triggers(keypad: LutronKeypad, button_number: int) -> str:
+    """Retrieve the caseta button name from device triggers."""
+    button_number_map = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(keypad["type"], {})
+    return (
+        button_number_map.get(
+            button_number,
+            f"button {button_number}",
+        )
+        .replace("_", " ")
+        .title()
+    )
 
 
 def _handle_none_keypad_serial(keypad_device: dict, bridge_serial: int) -> str:
@@ -303,17 +403,19 @@ def async_get_lip_button(device_type: str, leap_button: int) -> int | None:
 
 
 @callback
-def _async_subscribe_pico_remote_events(
+def _async_subscribe_keypad_events(
     hass: HomeAssistant,
-    bridge_device: Smartbridge,
-    button_devices_by_id: dict[int, dict],
+    bridge: Smartbridge,
+    keypads: dict[int, Any],
+    keypad_buttons: dict[int, Any],
 ):
     """Subscribe to lutron events."""
-    dev_reg = dr.async_get(hass)
 
     @callback
     def _async_button_event(button_id, event_type):
-        if not (device := button_devices_by_id.get(button_id)):
+        if not (button := keypad_buttons.get(button_id)) or not (
+            keypad := keypads.get(button["parent_keypad"])
+        ):
             return
 
         if event_type == BUTTON_STATUS_PRESSED:
@@ -321,40 +423,26 @@ def _async_subscribe_pico_remote_events(
         else:
             action = ACTION_RELEASE
 
-        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"]]
-
-        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 = _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)})
+        keypad_type = keypad["type"]
+        leap_button_number = button["leap_button_number"]
+        lip_button_number = async_get_lip_button(keypad_type, leap_button_number)
 
         hass.bus.async_fire(
             LUTRON_CASETA_BUTTON_EVENT,
             {
-                ATTR_SERIAL: ha_device_serial,
-                ATTR_TYPE: type_,
+                ATTR_SERIAL: keypad["serial"],
+                ATTR_TYPE: keypad_type,
                 ATTR_BUTTON_NUMBER: lip_button_number,
                 ATTR_LEAP_BUTTON_NUMBER: leap_button_number,
-                ATTR_DEVICE_NAME: name,
-                ATTR_DEVICE_ID: hass_device.id,
-                ATTR_AREA_NAME: area,
+                ATTR_DEVICE_NAME: keypad["name"],
+                ATTR_DEVICE_ID: keypad["dr_device_id"],
+                ATTR_AREA_NAME: keypad["area_name"],
                 ATTR_ACTION: action,
             },
         )
 
-    for button_id in button_devices_by_id:
-        bridge_device.add_button_subscriber(
+    for button_id in keypad_buttons:
+        bridge.add_button_subscriber(
             str(button_id),
             lambda event_type, button_id=button_id: _async_button_event(
                 button_id, event_type
diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py
index b8cbb23774a..ee737673082 100644
--- a/homeassistant/components/lutron_caseta/button.py
+++ b/homeassistant/components/lutron_caseta/button.py
@@ -11,10 +11,7 @@ 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 .device_trigger import LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP
 from .models import LutronCasetaData
 
 
@@ -28,12 +25,13 @@ async def async_setup_entry(
     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
+    keypads = data.keypad_data.keypads
     entities: list[LutronCasetaButton] = []
 
     for device in button_devices.values():
 
-        parent_device_info = device_info_by_device_id[device["parent_device"]]
+        parent_keypad = keypads[device["parent_device"]]
+        parent_device_info = parent_keypad["device_info"]
 
         enabled_default = True
         if not (device_name := device.get("device_name")):
@@ -43,9 +41,7 @@ async def async_setup_entry(
             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"]
-                ),
+                keypad_device["type"],
                 {},
             )
             device_name = (
diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py
index 30e4e772c99..2dfbe526c93 100644
--- a/homeassistant/components/lutron_caseta/device_trigger.py
+++ b/homeassistant/components/lutron_caseta/device_trigger.py
@@ -1,6 +1,8 @@
 """Provides device triggers for lutron caseta."""
 from __future__ import annotations
 
+import logging
+
 import voluptuous as vol
 
 from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
@@ -17,7 +19,6 @@ from homeassistant.const import (
 )
 from homeassistant.core import CALLBACK_TYPE, HomeAssistant
 from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import device_registry as dr
 from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
 from homeassistant.helpers.typing import ConfigType
 
@@ -33,19 +34,14 @@ from .const import (
 )
 from .models import LutronCasetaData
 
+_LOGGER = logging.getLogger(__name__)
+
 
 def _reverse_dict(forward_dict: dict) -> dict:
     """Reverse a dictionary."""
     return {v: k for k, v in forward_dict.items()}
 
 
-LUTRON_MODEL_TO_TYPE = {
-    "RRST-W2B-XX": "SunnataKeypad_2Button",
-    "RRST-W3RL-XX": "SunnataKeypad_3ButtonRaiseLower",
-    "RRST-W4B-XX": "SunnataKeypad_4Button",
-}
-
-
 SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE]
 
 LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
@@ -55,6 +51,20 @@ LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
 )
 
 
+KEYPAD_LEAP_BUTTON_NAME_OVERRIDE = {
+    "RRD-W2RLD": {
+        17: "raise_1",
+        16: "lower_1",
+        19: "raise_2",
+        18: "lower_2",
+    },
+    "RRD-W1RLD": {
+        19: "raise",
+        18: "lower",
+    },
+}
+
+
 PICO_2_BUTTON_BUTTON_TYPES_TO_LIP = {
     "on": 2,
     "off": 4,
@@ -271,72 +281,6 @@ FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
 )
 
 
-SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP = {
-    "button_1": 1,
-    "button_2": 2,
-}
-SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
-    {
-        vol.Required(CONF_SUBTYPE): vol.In(
-            SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP
-        ),
-    }
-)
-
-
-SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = {
-    "button_1": 1,
-    "button_2": 2,
-    "button_3": 3,
-    "raise": 19,
-    "lower": 18,
-}
-SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = (
-    LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
-        {
-            vol.Required(CONF_SUBTYPE): vol.In(
-                SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP
-            ),
-        }
-    )
-)
-
-SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP = {
-    "button_1": 1,
-    "button_2": 2,
-    "button_3": 3,
-    "button_4": 4,
-}
-SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
-    {
-        vol.Required(CONF_SUBTYPE): vol.In(
-            SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP
-        ),
-    }
-)
-
-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,
     "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA,
@@ -347,11 +291,6 @@ DEVICE_TYPE_SCHEMA_MAP = {
     "Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA,
     "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA,
     "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA,
-    "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 = {
@@ -376,11 +315,6 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = {
     "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP,
     "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP,
     "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_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 = {
@@ -395,32 +329,52 @@ TRIGGER_SCHEMA = vol.Any(
     PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA,
     PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA,
     FOUR_GROUP_REMOTE_TRIGGER_SCHEMA,
-    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,
 )
 
 
 async def async_validate_trigger_config(
     hass: HomeAssistant, config: ConfigType
 ) -> ConfigType:
-    """Validate config."""
-    # if device is available verify parameters against device capabilities
-    device = get_button_device_by_dr_id(hass, config[CONF_DEVICE_ID])
+    """Validate trigger config."""
+
+    device_id = config[CONF_DEVICE_ID]
+    subtype = config[CONF_SUBTYPE]
 
-    if not device:
+    if not (data := get_lutron_data_by_dr_id(hass, device_id)) or not (
+        keypad := data.keypad_data.dr_device_id_to_keypad.get(device_id)
+    ):
         return config
 
+    keypad_trigger_schemas = data.keypad_data.trigger_schemas
+    keypad_button_names_to_leap = data.keypad_data.button_names_to_leap
+
+    # Retrieve trigger schema, preferring hard-coded triggers from device_trigger.py
     if not (
         schema := DEVICE_TYPE_SCHEMA_MAP.get(
-            _lutron_model_to_device_type(device["model"], device["type"])
+            keypad["type"],
+            keypad_trigger_schemas.get(keypad["lutron_device_id"]),
         )
     ):
-        raise InvalidDeviceAutomationConfig(
-            f"Device model {device['model']} with type {device['type']} not supported: {config[CONF_DEVICE_ID]}"
+        # Trigger schema not found - log error
+        _LOGGER.error(
+            "Cannot validate trigger %s because the trigger schema was not found",
+            config,
+        )
+        return config
+
+    # Retrieve list of valid buttons, preferring hard-coded triggers from device_trigger.py
+    device_type = keypad["type"]
+    valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(
+        device_type,
+        keypad_button_names_to_leap[keypad["lutron_device_id"]],
+    )
+
+    if subtype not in valid_buttons:
+        # Trigger subtype is invalid - raise error
+        _LOGGER.error(
+            "Cannot validate trigger %s because subtype %s is invalid", config, subtype
         )
+        return config
 
     return schema(config)
 
@@ -431,12 +385,18 @@ async def async_get_triggers(
     """List device triggers for lutron caseta devices."""
     triggers = []
 
-    if not (device := get_button_device_by_dr_id(hass, device_id)):
-        # Check if device is a valid button device.  Return empty if not.
+    # Check if device is a valid keypad.  Return empty if not.
+    if not (data := get_lutron_data_by_dr_id(hass, device_id)) or not (
+        keypad := data.keypad_data.dr_device_id_to_keypad.get(device_id)
+    ):
         return []
 
+    keypad_button_names_to_leap = data.keypad_data.button_names_to_leap
+
+    # Retrieve list of valid buttons, preferring hard-coded triggers from device_trigger.py
     valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(
-        _lutron_model_to_device_type(device["model"], device["type"]), {}
+        keypad["type"],
+        keypad_button_names_to_leap[keypad["lutron_device_id"]],
     )
 
     for trigger in SUPPORTED_INPUTS_EVENTS_TYPES:
@@ -454,18 +414,6 @@ async def async_get_triggers(
     return triggers
 
 
-def _device_model_to_type(device_registry_model: str) -> str:
-    """Convert a lutron_caseta device registry entry model to 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:
-    """Get the mapped type based on the lutron model or type."""
-    return LUTRON_MODEL_TO_TYPE.get(model, device_type)
-
-
 async def async_attach_trigger(
     hass: HomeAssistant,
     config: ConfigType,
@@ -473,42 +421,63 @@ async def async_attach_trigger(
     trigger_info: TriggerInfo,
 ) -> CALLBACK_TYPE:
     """Attach a trigger."""
-    device_registry = dr.async_get(hass)
-    if (
-        not (device := device_registry.async_get(config[CONF_DEVICE_ID]))
-        or not device.model
+    device_id = config[CONF_DEVICE_ID]
+    subtype = config[CONF_SUBTYPE]
+    if not (data := get_lutron_data_by_dr_id(hass, device_id)) or not (
+        keypad := data.keypad_data.dr_device_id_to_keypad[device_id]
     ):
         raise HomeAssistantError(
-            f"Cannot attach trigger {config} because device with id {config[CONF_DEVICE_ID]} is missing or invalid"
+            f"Cannot attach trigger {config} because device with id {device_id} is missing or invalid"
         )
-    device_type = _device_model_to_type(device.model)
-    _, serial = list(device.identifiers)[0]
-    schema = DEVICE_TYPE_SCHEMA_MAP[device_type]
-    valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP[device_type]
+
+    keypad_trigger_schemas = data.keypad_data.trigger_schemas
+    keypad_button_names_to_leap = data.keypad_data.button_names_to_leap
+
+    device_type = keypad["type"]
+    serial = keypad["serial"]
+    lutron_device_id = keypad["lutron_device_id"]
+
+    # Retrieve trigger schema, preferring hard-coded triggers from device_trigger.py
+    schema = DEVICE_TYPE_SCHEMA_MAP.get(
+        device_type,
+        keypad_trigger_schemas[lutron_device_id],
+    )
+
+    # Retrieve list of valid buttons, preferring hard-coded triggers from device_trigger.py
+    valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(
+        device_type,
+        keypad_button_names_to_leap[lutron_device_id],
+    )
+
+    if subtype not in valid_buttons:
+        raise InvalidDeviceAutomationConfig(
+            f"Cannot attach trigger {config} because subtype {subtype} is invalid"
+        )
+
     config = schema(config)
     event_config = {
         event_trigger.CONF_PLATFORM: CONF_EVENT,
         event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT,
         event_trigger.CONF_EVENT_DATA: {
             ATTR_SERIAL: serial,
-            ATTR_LEAP_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]],
+            ATTR_LEAP_BUTTON_NUMBER: valid_buttons[subtype],
             ATTR_ACTION: config[CONF_TYPE],
         },
     }
     event_config = event_trigger.TRIGGER_SCHEMA(event_config)
+
     return await event_trigger.async_attach_trigger(
         hass, event_config, action, trigger_info, platform_type="device"
     )
 
 
-def get_button_device_by_dr_id(hass: HomeAssistant, device_id: str):
-    """Get a lutron device for the given device id."""
+def get_lutron_data_by_dr_id(hass: HomeAssistant, device_id: str):
+    """Get a lutron integration data for the given device registry device id."""
     if DOMAIN not in hass.data:
         return None
 
     for entry_id in hass.data[DOMAIN]:
         data: LutronCasetaData = hass.data[DOMAIN][entry_id]
-        if device := data.button_devices.get(device_id):
-            return device
-
+        if data.keypad_data.dr_device_id_to_keypad.get(device_id):
+            return data
     return None
diff --git a/homeassistant/components/lutron_caseta/diagnostics.py b/homeassistant/components/lutron_caseta/diagnostics.py
index afe69b813f9..07bd0a9e8ce 100644
--- a/homeassistant/components/lutron_caseta/diagnostics.py
+++ b/homeassistant/components/lutron_caseta/diagnostics.py
@@ -21,11 +21,16 @@ async def async_get_config_entry_diagnostics(
             "title": entry.title,
             "data": dict(entry.data),
         },
-        "data": {
+        "bridge_data": {
             "devices": bridge.devices,
             "buttons": bridge.buttons,
             "scenes": bridge.scenes,
             "occupancy_groups": bridge.occupancy_groups,
             "areas": bridge.areas,
         },
+        "integration_data": {
+            "keypad_button_names_to_leap": data.keypad_data.button_names_to_leap,
+            "keypad_buttons": data.keypad_data.buttons,
+            "keypads": data.keypad_data.keypads,
+        },
     }
diff --git a/homeassistant/components/lutron_caseta/logbook.py b/homeassistant/components/lutron_caseta/logbook.py
index 7bf1b467ff6..ccefaff2a78 100644
--- a/homeassistant/components/lutron_caseta/logbook.py
+++ b/homeassistant/components/lutron_caseta/logbook.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 from collections.abc import Callable
 
 from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME
+from homeassistant.const import ATTR_DEVICE_ID
 from homeassistant.core import Event, HomeAssistant, callback
 
 from .const import (
@@ -15,7 +16,11 @@ from .const import (
     DOMAIN,
     LUTRON_CASETA_BUTTON_EVENT,
 )
-from .device_trigger import LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP
+from .device_trigger import (
+    LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP,
+    _reverse_dict,
+    get_lutron_data_by_dr_id,
+)
 
 
 @callback
@@ -28,11 +33,28 @@ def async_describe_events(
     @callback
     def async_describe_button_event(event: Event) -> dict[str, str]:
         """Describe lutron_caseta_button_event logbook event."""
+
         data = event.data
         device_type = data[ATTR_TYPE]
         leap_button_number = data[ATTR_LEAP_BUTTON_NUMBER]
-        button_map = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP[device_type]
-        button_description = button_map[leap_button_number]
+        dr_device_id = data[ATTR_DEVICE_ID]
+        lutron_data = get_lutron_data_by_dr_id(hass, dr_device_id)
+        keypad = lutron_data.keypad_data.dr_device_id_to_keypad.get(dr_device_id)
+        keypad_id = keypad["lutron_device_id"]
+
+        keypad_button_names_to_leap = lutron_data.keypad_data.button_names_to_leap
+
+        if not (rev_button_map := LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(device_type)):
+            if fwd_button_map := keypad_button_names_to_leap.get(keypad_id):
+                rev_button_map = _reverse_dict(fwd_button_map)
+
+        if rev_button_map is None:
+            return {
+                LOGBOOK_ENTRY_NAME: f"{data[ATTR_AREA_NAME]} {data[ATTR_DEVICE_NAME]}",
+                LOGBOOK_ENTRY_MESSAGE: f"{data[ATTR_ACTION]} Error retrieving button description",
+            }
+
+        button_description = rev_button_map.get(leap_button_number)
         return {
             LOGBOOK_ENTRY_NAME: f"{data[ATTR_AREA_NAME]} {data[ATTR_DEVICE_NAME]}",
             LOGBOOK_ENTRY_MESSAGE: f"{data[ATTR_ACTION]} {button_description}",
diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py
index e7fb8a2f2b8..576387bd36b 100644
--- a/homeassistant/components/lutron_caseta/models.py
+++ b/homeassistant/components/lutron_caseta/models.py
@@ -2,9 +2,10 @@
 from __future__ import annotations
 
 from dataclasses import dataclass
-from typing import Any
+from typing import Any, TypedDict
 
 from pylutron_caseta.smartbridge import Smartbridge
+import voluptuous as vol
 
 from homeassistant.helpers.entity import DeviceInfo
 
@@ -15,5 +16,40 @@ class LutronCasetaData:
 
     bridge: Smartbridge
     bridge_device: dict[str, Any]
-    button_devices: dict[str, dict]
-    device_info_by_device_id: dict[int, DeviceInfo]
+    keypad_data: LutronKeypadData
+
+
+@dataclass
+class LutronKeypadData:
+    """Data for the lutron_caseta integration keypads."""
+
+    dr_device_id_to_keypad: dict[str, LutronKeypad]
+    keypads: dict[int, LutronKeypad]
+    buttons: dict[int, LutronButton]
+    button_names_to_leap: dict[int, dict[str, int]]
+    trigger_schemas: dict[int, vol.Schema]
+
+
+class LutronKeypad(TypedDict):
+    """A lutron_caseta keypad device."""
+
+    lutron_device_id: int
+    dr_device_id: str
+    area_id: int
+    area_name: str
+    name: str
+    serial: str
+    device_info: DeviceInfo
+    model: str
+    type: str
+    buttons: list[int]
+
+
+class LutronButton(TypedDict):
+    """A lutron_caseta button."""
+
+    lutron_device_id: int
+    leap_button_number: int
+    button_name: str
+    led_device_id: int
+    parent_keypad: int
diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py
index 50c01e6a31f..795435d5f7c 100644
--- a/homeassistant/components/lutron_caseta/switch.py
+++ b/homeassistant/components/lutron_caseta/switch.py
@@ -42,7 +42,9 @@ class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, SwitchEntity):
         if "parent_device" not in device:
             return
 
-        parent_device_info = data.device_info_by_device_id.get(device["parent_device"])
+        keypads = data.keypad_data.keypads
+        parent_keypad = keypads[device["parent_device"]]
+        parent_device_info = parent_keypad["device_info"]
         # 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
diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py
index cc6818dab14..c5d63c9e256 100644
--- a/tests/components/lutron_caseta/__init__.py
+++ b/tests/components/lutron_caseta/__init__.py
@@ -250,7 +250,7 @@ class MockBridge:
             "111": {
                 "device_id": "111",
                 "current_state": "Release",
-                "button_number": 0,
+                "button_number": 1,
                 "name": "Dining Room_Pico",
                 "type": "Pico3ButtonRaiseLower",
                 "model": "PJ2-3BRL-GXX-X01",
diff --git a/tests/components/lutron_caseta/test_button.py b/tests/components/lutron_caseta/test_button.py
index 767d9a59df4..68742e5bae3 100644
--- a/tests/components/lutron_caseta/test_button.py
+++ b/tests/components/lutron_caseta/test_button.py
@@ -15,7 +15,7 @@ async def test_button_unique_id(hass: HomeAssistant) -> None:
     ra3_button_entity_id = (
         "button.hallway_main_stairs_position_1_keypad_kitchen_pendants"
     )
-    caseta_button_entity_id = "button.dining_room_pico_on"
+    caseta_button_entity_id = "button.dining_room_pico_stop"
 
     entity_registry = er.async_get(hass)
 
diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py
index 991fa191f69..a1558822619 100644
--- a/tests/components/lutron_caseta/test_device_trigger.py
+++ b/tests/components/lutron_caseta/test_device_trigger.py
@@ -1,5 +1,5 @@
 """The tests for Lutron Caséta device triggers."""
-from unittest.mock import MagicMock
+from unittest.mock import patch
 
 import pytest
 
@@ -14,16 +14,26 @@ from homeassistant.components.lutron_caseta import (
 )
 from homeassistant.components.lutron_caseta.const import (
     ATTR_LEAP_BUTTON_NUMBER,
+    CONF_CA_CERTS,
+    CONF_CERTFILE,
+    CONF_KEYFILE,
     DOMAIN,
     LUTRON_CASETA_BUTTON_EVENT,
-    MANUFACTURER,
 )
 from homeassistant.components.lutron_caseta.device_trigger import CONF_SUBTYPE
 from homeassistant.components.lutron_caseta.models import LutronCasetaData
-from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
+from homeassistant.const import (
+    CONF_DEVICE_ID,
+    CONF_DOMAIN,
+    CONF_HOST,
+    CONF_PLATFORM,
+    CONF_TYPE,
+)
 from homeassistant.helpers import device_registry
 from homeassistant.setup import async_setup_component
 
+from . import MockBridge
+
 from tests.common import (
     MockConfigEntry,
     assert_lists_same,
@@ -34,8 +44,8 @@ from tests.common import (
 
 MOCK_BUTTON_DEVICES = [
     {
-        "device_id": "710",
-        "Name": "Back Hall Pico",
+        "device_id": "9",
+        "Name": "Dining Room_Pico",
         "ID": 2,
         "Area": {"Name": "Back Hall"},
         "Buttons": [
@@ -45,14 +55,14 @@ MOCK_BUTTON_DEVICES = [
             {"Number": 5},
             {"Number": 6},
         ],
-        "leap_name": "Back Hall_Back Hall Pico",
+        "leap_name": "Dining Room_Pico",
         "type": "Pico3ButtonRaiseLower",
         "model": "PJ2-3BRL-GXX-X01",
-        "serial": 43845548,
+        "serial": 68551522,
     },
     {
-        "device_id": "742",
-        "Name": "Front Steps Sunnata Keypad",
+        "device_id": "1355",
+        "Name": "Main Stairs Position 1 Keypad",
         "ID": 3,
         "Area": {"Name": "Front Steps"},
         "Buttons": [
@@ -65,12 +75,12 @@ MOCK_BUTTON_DEVICES = [
         "leap_name": "Front Steps_Front Steps Sunnata Keypad",
         "type": "SunnataKeypad",
         "model": "RRST-W4B-XX",
-        "serial": 43845547,
+        "serial": 66286451,
     },
     {
         "device_id": "786",
         "Name": "Example Homeowner Keypad",
-        "ID": 3,
+        "ID": 4,
         "Area": {"Name": "Front Steps"},
         "Buttons": [
             {"Number": 12},
@@ -84,7 +94,7 @@ MOCK_BUTTON_DEVICES = [
         "leap_name": "Front Steps_Example Homeowner Keypad",
         "type": "HomeownerKeypad",
         "model": "Homeowner Keypad",
-        "serial": None,
+        "serial": "1234_786",
     },
 ]
 
@@ -101,39 +111,36 @@ def device_reg(hass):
     return mock_device_registry(hass)
 
 
-async def _async_setup_lutron_with_picos(hass, device_reg):
+async def _async_setup_lutron_with_picos(hass):
     """Setups a lutron bridge with picos."""
-    await async_setup_component(hass, DOMAIN, {})
-
-    config_entry = MockConfigEntry(domain=DOMAIN, data={})
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_HOST: "1.1.1.1",
+            CONF_KEYFILE: "",
+            CONF_CERTFILE: "",
+            CONF_CA_CERTS: "",
+        },
+        unique_id="abc",
+    )
     config_entry.add_to_hass(hass)
-    dr_button_devices = {}
-    device_info_by_device_id = {}
-
-    for device in MOCK_BUTTON_DEVICES:
-        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, device_info_by_device_id
-    )
+    with patch(
+        "homeassistant.components.lutron_caseta.Smartbridge.create_tls",
+        return_value=MockBridge(can_connect=True),
+    ):
+        await hass.config_entries.async_setup(config_entry.entry_id)
+        await hass.async_block_till_done()
+
     return config_entry.entry_id
 
 
 async def test_get_triggers(hass, device_reg):
     """Test we get the expected triggers from a lutron pico."""
-    config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg)
+    config_entry_id = await _async_setup_lutron_with_picos(hass)
     data: LutronCasetaData = hass.data[DOMAIN][config_entry_id]
-    dr_button_devices = data.button_devices
-    device_id = list(dr_button_devices)[0]
+    keypads = data.keypad_data.keypads
+    device_id = keypads[list(keypads)[0]]["dr_device_id"]
 
     expected_triggers = [
         {
@@ -167,7 +174,7 @@ async def test_get_triggers(hass, device_reg):
 
 async def test_get_triggers_for_invalid_device_id(hass, device_reg):
     """Test error raised for invalid lutron device_id."""
-    config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg)
+    config_entry_id = await _async_setup_lutron_with_picos(hass)
 
     invalid_device = device_reg.async_get_or_create(
         config_entry_id=config_entry_id,
@@ -183,7 +190,7 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg):
 
 async def test_get_triggers_for_non_button_device(hass, device_reg):
     """Test error raised for invalid lutron device_id."""
-    config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg)
+    config_entry_id = await _async_setup_lutron_with_picos(hass)
 
     invalid_device = device_reg.async_get_or_create(
         config_entry_id=config_entry_id,
@@ -199,7 +206,7 @@ async def test_get_triggers_for_non_button_device(hass, device_reg):
 
 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)
+    config_entry_id = await _async_setup_lutron_with_picos(hass)
 
     keypad_device = device_reg.async_get_or_create(
         config_entry_id=config_entry_id,
@@ -211,11 +218,13 @@ async def test_none_serial_keypad(hass, device_reg):
 
 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)
+    await _async_setup_lutron_with_picos(hass)
+
     device = MOCK_BUTTON_DEVICES[0]
     dr = device_registry.async_get(hass)
     dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])})
     device_id = dr_device.id
+
     assert await async_setup_component(
         hass,
         automation.DOMAIN,
@@ -255,7 +264,7 @@ async def test_if_fires_on_button_event(hass, calls, device_reg):
 
 async def test_if_fires_on_button_event_without_lip(hass, calls, device_reg):
     """Test for press trigger firing on a device that does not support lip."""
-    await _async_setup_lutron_with_picos(hass, device_reg)
+    await _async_setup_lutron_with_picos(hass)
     device = MOCK_BUTTON_DEVICES[1]
     dr = device_registry.async_get(hass)
     dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])})
@@ -271,7 +280,7 @@ async def test_if_fires_on_button_event_without_lip(hass, calls, device_reg):
                         CONF_DOMAIN: DOMAIN,
                         CONF_DEVICE_ID: device_id,
                         CONF_TYPE: "press",
-                        CONF_SUBTYPE: "button_1",
+                        CONF_SUBTYPE: "Kitchen Pendants",
                     },
                     "action": {
                         "service": "test.automation",
@@ -285,7 +294,7 @@ async def test_if_fires_on_button_event_without_lip(hass, calls, device_reg):
     message = {
         ATTR_SERIAL: device.get("serial"),
         ATTR_TYPE: device.get("type"),
-        ATTR_LEAP_BUTTON_NUMBER: 1,
+        ATTR_LEAP_BUTTON_NUMBER: 3,
         ATTR_DEVICE_NAME: device["Name"],
         ATTR_AREA_NAME: device.get("Area", {}).get("Name"),
         ATTR_ACTION: "press",
@@ -338,12 +347,13 @@ async def test_validate_trigger_config_no_device(hass, calls, device_reg):
 async def test_validate_trigger_config_unknown_device(hass, calls, device_reg):
     """Test for no press with an unknown device."""
 
-    config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg)
+    config_entry_id = await _async_setup_lutron_with_picos(hass)
     data: LutronCasetaData = hass.data[DOMAIN][config_entry_id]
-    dr_button_devices = data.button_devices
-    device_id = list(dr_button_devices)[0]
-    device = dr_button_devices[device_id]
-    device["type"] = "unknown"
+    keypads = data.keypad_data.keypads
+    lutron_device_id = list(keypads)[0]
+    keypad = keypads[lutron_device_id]
+    device_id = keypad["dr_device_id"]
+    keypad["type"] = "unknown"
 
     assert await async_setup_component(
         hass,
@@ -382,10 +392,13 @@ async def test_validate_trigger_config_unknown_device(hass, calls, device_reg):
 
 async def test_validate_trigger_invalid_triggers(hass, device_reg):
     """Test for click_event with invalid triggers."""
-    config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg)
+    config_entry_id = await _async_setup_lutron_with_picos(hass)
     data: LutronCasetaData = hass.data[DOMAIN][config_entry_id]
-    dr_button_devices = data.button_devices
-    device_id = list(dr_button_devices)[0]
+    keypads = data.keypad_data.keypads
+    lutron_device_id = list(keypads)[0]
+    keypad = keypads[lutron_device_id]
+    device_id = keypad["dr_device_id"]
+
     assert await async_setup_component(
         hass,
         automation.DOMAIN,
diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py
index 8fa293c19d6..80643737aaa 100644
--- a/tests/components/lutron_caseta/test_diagnostics.py
+++ b/tests/components/lutron_caseta/test_diagnostics.py
@@ -1,6 +1,6 @@
 """Test the Lutron Caseta diagnostics."""
 
-from unittest.mock import patch
+from unittest.mock import ANY, patch
 
 from homeassistant.components.lutron_caseta import DOMAIN
 from homeassistant.components.lutron_caseta.const import (
@@ -39,7 +39,7 @@ async def test_diagnostics(hass, hass_client) -> None:
 
     diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
     assert diag == {
-        "data": {
+        "bridge_data": {
             "areas": {
                 "898": {"id": "898", "name": "Basement", "parent_id": None},
                 "822": {"id": "822", "name": "Bedroom", "parent_id": "898"},
@@ -53,7 +53,7 @@ async def test_diagnostics(hass, hass_client) -> None:
                 "111": {
                     "device_id": "111",
                     "current_state": "Release",
-                    "button_number": 0,
+                    "button_number": 1,
                     "name": "Dining Room_Pico",
                     "type": "Pico3ButtonRaiseLower",
                     "model": "PJ2-3BRL-GXX-X01",
@@ -185,4 +185,66 @@ async def test_diagnostics(hass, hass_client) -> None:
             "data": {"ca_certs": "", "certfile": "", "host": "1.1.1.1", "keyfile": ""},
             "title": "Mock Title",
         },
+        "integration_data": {
+            "keypad_button_names_to_leap": {
+                "1355": {"Kitchen Pendants": 3},
+                "9": {"Stop": 1},
+            },
+            "keypad_buttons": {
+                "111": {
+                    "button_name": "Stop",
+                    "leap_button_number": 1,
+                    "led_device_id": None,
+                    "lutron_device_id": "111",
+                    "parent_keypad": "9",
+                },
+                "1372": {
+                    "button_name": "Kitchen " "Pendants",
+                    "leap_button_number": 3,
+                    "led_device_id": "1362",
+                    "lutron_device_id": "1372",
+                    "parent_keypad": "1355",
+                },
+            },
+            "keypads": {
+                "1355": {
+                    "area_id": "1205",
+                    "area_name": "Hallway",
+                    "buttons": ["1372"],
+                    "device_info": {
+                        "identifiers": [["lutron_caseta", 66286451]],
+                        "manufacturer": "Lutron " "Electronics " "Co., " "Inc",
+                        "model": "RRST-W3RL-XX " "(SunnataKeypad)",
+                        "name": "Hallway " "Main " "Stairs " "Position 1 " "Keypad",
+                        "suggested_area": "Hallway",
+                        "via_device": ["lutron_caseta", 1234],
+                    },
+                    "dr_device_id": ANY,
+                    "lutron_device_id": "1355",
+                    "model": "RRST-W3RL-XX",
+                    "name": "Main Stairs Position 1 " "Keypad",
+                    "serial": 66286451,
+                    "type": "SunnataKeypad",
+                },
+                "9": {
+                    "area_id": "1026",
+                    "area_name": "Dining Room",
+                    "buttons": ["111"],
+                    "device_info": {
+                        "identifiers": [["lutron_caseta", 68551522]],
+                        "manufacturer": "Lutron " "Electronics " "Co., " "Inc",
+                        "model": "PJ2-3BRL-GXX-X01 " "(Pico3ButtonRaiseLower)",
+                        "name": "Dining Room " "Pico",
+                        "suggested_area": "Dining " "Room",
+                        "via_device": ["lutron_caseta", 1234],
+                    },
+                    "dr_device_id": ANY,
+                    "lutron_device_id": "9",
+                    "model": "PJ2-3BRL-GXX-X01",
+                    "name": "Pico",
+                    "serial": 68551522,
+                    "type": "Pico3ButtonRaiseLower",
+                },
+            },
+        },
     }
diff --git a/tests/components/lutron_caseta/test_logbook.py b/tests/components/lutron_caseta/test_logbook.py
index 3a202eadf58..c189238d9df 100644
--- a/tests/components/lutron_caseta/test_logbook.py
+++ b/tests/components/lutron_caseta/test_logbook.py
@@ -15,6 +15,7 @@ from homeassistant.components.lutron_caseta.const import (
     DOMAIN,
     LUTRON_CASETA_BUTTON_EVENT,
 )
+from homeassistant.components.lutron_caseta.models import LutronCasetaData
 from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST
 from homeassistant.setup import async_setup_component
 
@@ -49,25 +50,30 @@ async def test_humanify_lutron_caseta_button_event(hass):
 
     await hass.async_block_till_done()
 
+    data: LutronCasetaData = hass.data[DOMAIN][config_entry.entry_id]
+    keypads = data.keypad_data.keypads
+    keypad = keypads["9"]
+    dr_device_id = keypad["dr_device_id"]
+
     (event1,) = mock_humanify(
         hass,
         [
             MockRow(
                 LUTRON_CASETA_BUTTON_EVENT,
                 {
-                    ATTR_SERIAL: "123",
-                    ATTR_DEVICE_ID: "1234",
+                    ATTR_SERIAL: "68551522",
+                    ATTR_DEVICE_ID: dr_device_id,
                     ATTR_TYPE: "Pico3ButtonRaiseLower",
-                    ATTR_LEAP_BUTTON_NUMBER: 3,
-                    ATTR_BUTTON_NUMBER: 3,
+                    ATTR_LEAP_BUTTON_NUMBER: 1,
+                    ATTR_BUTTON_NUMBER: 1,
                     ATTR_DEVICE_NAME: "Pico",
-                    ATTR_AREA_NAME: "Living Room",
+                    ATTR_AREA_NAME: "Dining Room",
                     ATTR_ACTION: "press",
                 },
             ),
         ],
     )
 
-    assert event1["name"] == "Living Room Pico"
+    assert event1["name"] == "Dining Room Pico"
     assert event1["domain"] == DOMAIN
-    assert event1["message"] == "press raise"
+    assert event1["message"] == "press stop"
-- 
GitLab


From 746bdb44ac612f6326c22686fee3eae8d4adbf51 Mon Sep 17 00:00:00 2001
From: Nathan Spencer <natekspencer@gmail.com>
Date: Sun, 23 Oct 2022 14:19:57 -0600
Subject: [PATCH 720/985] Bump pylitterbot to 2022.10.2 (#80836)

---
 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 61f74bf5a64..0965670e569 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.10.0"],
+  "requirements": ["pylitterbot==2022.10.2"],
   "codeowners": ["@natekspencer", "@tkdrob"],
   "dhcp": [{ "hostname": "litter-robot4" }],
   "iot_class": "cloud_push",
diff --git a/requirements_all.txt b/requirements_all.txt
index 0d7a0a9dd83..c6eaf9ec492 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1685,7 +1685,7 @@ pylibrespot-java==0.1.1
 pylitejet==0.3.0
 
 # homeassistant.components.litterrobot
-pylitterbot==2022.10.0
+pylitterbot==2022.10.2
 
 # homeassistant.components.lutron_caseta
 pylutron-caseta==0.16.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 9122b66de97..e2c6020a5bd 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1186,7 +1186,7 @@ pylibrespot-java==0.1.1
 pylitejet==0.3.0
 
 # homeassistant.components.litterrobot
-pylitterbot==2022.10.0
+pylitterbot==2022.10.2
 
 # homeassistant.components.lutron_caseta
 pylutron-caseta==0.16.0
-- 
GitLab


From b04165b495ecdd0b1309169ec2712ff726fe4a2a Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
Date: Sun, 23 Oct 2022 22:22:14 +0200
Subject: [PATCH 721/985] Add Sensibo Climate React (#78221)

---
 homeassistant/components/sensibo/climate.py   |  67 ++++
 homeassistant/components/sensibo/sensor.py    |  31 +-
 .../components/sensibo/services.yaml          |  53 +++
 .../components/sensibo/strings.sensor.json    |   5 +
 homeassistant/components/sensibo/switch.py    |  24 ++
 .../sensibo/translations/sensor.en.json       |   5 +
 tests/components/sensibo/test_climate.py      | 311 ++++++++++++++++++
 tests/components/sensibo/test_sensor.py       |  19 +-
 tests/components/sensibo/test_switch.py       | 110 +++++++
 9 files changed, 623 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py
index d4891d7cdf9..70d4a12406a 100644
--- a/homeassistant/components/sensibo/climate.py
+++ b/homeassistant/components/sensibo/climate.py
@@ -38,6 +38,12 @@ ATTR_MINUTES = "minutes"
 SERVICE_ENABLE_PURE_BOOST = "enable_pure_boost"
 SERVICE_DISABLE_PURE_BOOST = "disable_pure_boost"
 SERVICE_FULL_STATE = "full_state"
+SERVICE_ENABLE_CLIMATE_REACT = "enable_climate_react"
+ATTR_HIGH_TEMPERATURE_THRESHOLD = "high_temperature_threshold"
+ATTR_HIGH_TEMPERATURE_STATE = "high_temperature_state"
+ATTR_LOW_TEMPERATURE_THRESHOLD = "low_temperature_threshold"
+ATTR_LOW_TEMPERATURE_STATE = "low_temperature_state"
+ATTR_SMART_TYPE = "smart_type"
 
 ATTR_AC_INTEGRATION = "ac_integration"
 ATTR_GEO_INTEGRATION = "geo_integration"
@@ -140,6 +146,20 @@ async def async_setup_entry(
         "async_full_ac_state",
     )
 
+    platform.async_register_entity_service(
+        SERVICE_ENABLE_CLIMATE_REACT,
+        {
+            vol.Required(ATTR_HIGH_TEMPERATURE_THRESHOLD): float,
+            vol.Required(ATTR_HIGH_TEMPERATURE_STATE): dict,
+            vol.Required(ATTR_LOW_TEMPERATURE_THRESHOLD): float,
+            vol.Required(ATTR_LOW_TEMPERATURE_STATE): dict,
+            vol.Required(ATTR_SMART_TYPE): vol.In(
+                ["temperature", "feelsLike", "humidity"]
+            ),
+        },
+        "async_enable_climate_react",
+    )
+
 
 class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
     """Representation of a Sensibo device."""
@@ -430,6 +450,42 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
             data=params,
         )
 
+    async def async_enable_climate_react(
+        self,
+        high_temperature_threshold: float,
+        high_temperature_state: dict[str, Any],
+        low_temperature_threshold: float,
+        low_temperature_state: dict[str, Any],
+        smart_type: str,
+    ) -> None:
+        """Enable Climate React Configuration."""
+        high_temp = high_temperature_threshold
+        low_temp = low_temperature_threshold
+
+        if high_temperature_state.get("temperatureUnit") == "F":
+            high_temp = TemperatureConverter.convert(
+                high_temperature_threshold, TEMP_FAHRENHEIT, TEMP_CELSIUS
+            )
+            low_temp = TemperatureConverter.convert(
+                low_temperature_threshold, TEMP_FAHRENHEIT, TEMP_CELSIUS
+            )
+
+        params: dict[str, str | bool | float | dict] = {
+            "enabled": True,
+            "deviceUid": self._device_id,
+            "highTemperatureState": high_temperature_state,
+            "highTemperatureThreshold": high_temp,
+            "lowTemperatureState": low_temperature_state,
+            "lowTemperatureThreshold": low_temp,
+            "type": smart_type,
+        }
+
+        await self.api_call_custom_service_climate_react(
+            key="smart_on",
+            value=True,
+            data=params,
+        )
+
     @async_handle_api_call
     async def async_send_api_call(
         self,
@@ -470,6 +526,17 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
         result = await self._client.async_set_pureboost(self._device_id, data)
         return bool(result.get("status") == "success")
 
+    @async_handle_api_call
+    async def api_call_custom_service_climate_react(
+        self,
+        key: str,
+        value: Any,
+        data: dict,
+    ) -> bool:
+        """Make service call to api."""
+        result = await self._client.async_set_climate_react(self._device_id, data)
+        return bool(result.get("status") == "success")
+
     @async_handle_api_call
     async def api_call_custom_service_full_ac_state(
         self,
diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py
index 3fa764a8757..25d83089a52 100644
--- a/homeassistant/components/sensibo/sensor.py
+++ b/homeassistant/components/sensibo/sensor.py
@@ -155,6 +155,32 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
         extra_fn=None,
         entity_registry_enabled_default=False,
     ),
+    SensiboDeviceSensorEntityDescription(
+        key="climate_react_low",
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        name="Climate React low temperature threshold",
+        value_fn=lambda data: data.smart_low_temp_threshold,
+        extra_fn=lambda data: data.smart_low_state,
+        entity_registry_enabled_default=False,
+    ),
+    SensiboDeviceSensorEntityDescription(
+        key="climate_react_high",
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        name="Climate React high temperature threshold",
+        value_fn=lambda data: data.smart_high_temp_threshold,
+        extra_fn=lambda data: data.smart_high_state,
+        entity_registry_enabled_default=False,
+    ),
+    SensiboDeviceSensorEntityDescription(
+        key="climate_react_type",
+        device_class="sensibo__smart_type",
+        name="Climate React type",
+        value_fn=lambda data: data.smart_type,
+        extra_fn=None,
+        entity_registry_enabled_default=False,
+    ),
     FILTER_LAST_RESET_DESCRIPTION,
 )
 
@@ -281,7 +307,10 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity):
     @property
     def native_value(self) -> StateType | datetime:
         """Return value of sensor."""
-        return self.entity_description.value_fn(self.device_data)
+        state = self.entity_description.value_fn(self.device_data)
+        if isinstance(state, str):
+            return state.lower()
+        return state
 
     @property
     def extra_state_attributes(self) -> Mapping[str, Any] | None:
diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml
index 0f7e97976c6..f9f9365eb8e 100644
--- a/homeassistant/components/sensibo/services.yaml
+++ b/homeassistant/components/sensibo/services.yaml
@@ -146,3 +146,56 @@ full_state:
           options:
             - "on"
             - "off"
+enable_climate_react:
+  name: Enable Climate React
+  description: Enable and configure Climate React
+  target:
+    entity:
+      integration: sensibo
+      domain: climate
+  fields:
+    high_temperature_threshold:
+      name: Threshold high
+      description: When temp/humidity goes above
+      required: true
+      example: 24
+      selector:
+        number:
+          min: 0
+          max: 150
+          step: 0.1
+          mode: box
+    high_temperature_state:
+      name: State high threshold
+      description: What should happen at high threshold. Requires full state
+      required: true
+      selector:
+        object:
+    low_temperature_threshold:
+      name: Threshold low
+      description: When temp/humidity goes below
+      required: true
+      example: 19
+      selector:
+        number:
+          min: 0
+          max: 150
+          step: 0.1
+          mode: box
+    low_temperature_state:
+      name: State low threshold
+      description: What should happen at low threshold. Requires full state
+      required: true
+      selector:
+        object:
+    smart_type:
+      name: Trigger type
+      description: Choose between temperature/feels like/humidity
+      required: true
+      example: "temperature"
+      selector:
+        select:
+          options:
+            - "temperature"
+            - "feelsLike"
+            - "humidity"
diff --git a/homeassistant/components/sensibo/strings.sensor.json b/homeassistant/components/sensibo/strings.sensor.json
index 2e4e05fba5b..6226fd26a0f 100644
--- a/homeassistant/components/sensibo/strings.sensor.json
+++ b/homeassistant/components/sensibo/strings.sensor.json
@@ -3,6 +3,11 @@
     "sensibo__sensitivity": {
       "n": "Normal",
       "s": "Sensitive"
+    },
+    "sensibo__smart_type": {
+      "temperature": "Temperature",
+      "feelslike": "Feels like",
+      "humidity": "Humidity"
     }
   }
 }
diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py
index 3b75268fa9c..f57d72e5fb3 100644
--- a/homeassistant/components/sensibo/switch.py
+++ b/homeassistant/components/sensibo/switch.py
@@ -14,6 +14,7 @@ from homeassistant.components.switch import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import DOMAIN
@@ -53,6 +54,17 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = (
         command_off="async_turn_off_timer",
         data_key="timer_on",
     ),
+    SensiboDeviceSwitchEntityDescription(
+        key="climate_react_switch",
+        device_class=SwitchDeviceClass.SWITCH,
+        name="Climate React",
+        icon="mdi:wizard-hat",
+        value_fn=lambda data: data.smart_on,
+        extra_fn=lambda data: {"type": data.smart_type},
+        command_on="async_turn_on_off_smart",
+        command_off="async_turn_on_off_smart",
+        data_key="smart_on",
+    ),
 )
 
 PURE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = (
@@ -166,3 +178,15 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
             data["primeIntegration"] = False
         result = await self._client.async_set_pureboost(self._device_id, data)
         return bool(result.get("status") == "success")
+
+    @async_handle_api_call
+    async def async_turn_on_off_smart(self, key: str, value: Any) -> bool:
+        """Make service call to api for setting Climate React."""
+        if self.device_data.smart_type is None:
+            raise HomeAssistantError(
+                "Use Sensibo Enable Climate React Service once to enable switch or the Sensibo app"
+            )
+        new_state = bool(self.device_data.smart_on is False)
+        data: dict[str, Any] = {"enabled": new_state}
+        result = await self._client.async_enable_climate_react(self._device_id, data)
+        return bool(result.get("status") == "success")
diff --git a/homeassistant/components/sensibo/translations/sensor.en.json b/homeassistant/components/sensibo/translations/sensor.en.json
index 9ea1818b37c..b846d6a2074 100644
--- a/homeassistant/components/sensibo/translations/sensor.en.json
+++ b/homeassistant/components/sensibo/translations/sensor.en.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Normal",
             "s": "Sensitive"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "Feels like",
+            "humidity": "Humidity",
+            "temperature": "Temperature"
         }
     }
 }
\ No newline at end of file
diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py
index 555fc7f2ea5..224248c2d16 100644
--- a/tests/components/sensibo/test_climate.py
+++ b/tests/components/sensibo/test_climate.py
@@ -23,14 +23,20 @@ from homeassistant.components.climate import (
 from homeassistant.components.sensibo.climate import (
     ATTR_AC_INTEGRATION,
     ATTR_GEO_INTEGRATION,
+    ATTR_HIGH_TEMPERATURE_STATE,
+    ATTR_HIGH_TEMPERATURE_THRESHOLD,
     ATTR_HORIZONTAL_SWING_MODE,
     ATTR_INDOOR_INTEGRATION,
     ATTR_LIGHT,
+    ATTR_LOW_TEMPERATURE_STATE,
+    ATTR_LOW_TEMPERATURE_THRESHOLD,
     ATTR_MINUTES,
     ATTR_OUTDOOR_INTEGRATION,
     ATTR_SENSITIVITY,
+    ATTR_SMART_TYPE,
     ATTR_TARGET_TEMPERATURE,
     SERVICE_ASSUME_STATE,
+    SERVICE_ENABLE_CLIMATE_REACT,
     SERVICE_ENABLE_PURE_BOOST,
     SERVICE_ENABLE_TIMER,
     SERVICE_FULL_STATE,
@@ -923,6 +929,311 @@ async def test_climate_pure_boost(
     assert state4.state == "s"
 
 
+async def test_climate_climate_react(
+    hass: HomeAssistant,
+    entity_registry_enabled_by_default: AsyncMock,
+    load_int: ConfigEntry,
+    monkeypatch: pytest.MonkeyPatch,
+    get_data: SensiboData,
+) -> None:
+    """Test the Sensibo climate react custom service."""
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state_climate = hass.states.get("climate.hallway")
+
+    with patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
+    ), patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react",
+    ):
+        with pytest.raises(MultipleInvalid):
+            await hass.services.async_call(
+                DOMAIN,
+                SERVICE_ENABLE_PURE_BOOST,
+                {
+                    ATTR_ENTITY_ID: state_climate.entity_id,
+                    ATTR_LOW_TEMPERATURE_THRESHOLD: 0.2,
+                    ATTR_HIGH_TEMPERATURE_THRESHOLD: 30.3,
+                    ATTR_SMART_TYPE: "temperature",
+                },
+                blocking=True,
+            )
+    await hass.async_block_till_done()
+
+    with patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ), patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react",
+        return_value={
+            "status": "success",
+            "result": {
+                "enabled": True,
+                "deviceUid": "ABC999111",
+                "highTemperatureState": {
+                    "on": True,
+                    "targetTemperature": 15,
+                    "temperatureUnit": "C",
+                    "mode": "cool",
+                    "fanLevel": "high",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                "highTemperatureThreshold": 30.5,
+                "lowTemperatureState": {
+                    "on": True,
+                    "targetTemperature": 25,
+                    "temperatureUnit": "C",
+                    "mode": "heat",
+                    "fanLevel": "low",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                "lowTemperatureThreshold": 5.5,
+                "type": "temperature",
+            },
+        },
+    ):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_ENABLE_CLIMATE_REACT,
+            {
+                ATTR_ENTITY_ID: state_climate.entity_id,
+                ATTR_LOW_TEMPERATURE_THRESHOLD: 5.5,
+                ATTR_HIGH_TEMPERATURE_THRESHOLD: 30.5,
+                ATTR_LOW_TEMPERATURE_STATE: {
+                    "on": True,
+                    "targetTemperature": 25,
+                    "temperatureUnit": "C",
+                    "mode": "heat",
+                    "fanLevel": "low",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                ATTR_HIGH_TEMPERATURE_STATE: {
+                    "on": True,
+                    "targetTemperature": 15,
+                    "temperatureUnit": "C",
+                    "mode": "cool",
+                    "fanLevel": "high",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                ATTR_SMART_TYPE: "temperature",
+            },
+            blocking=True,
+        )
+    await hass.async_block_till_done()
+
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True)
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", "temperature")
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_low_temp_threshold", 5.5)
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_high_temp_threshold", 30.5)
+    monkeypatch.setattr(
+        get_data.parsed["ABC999111"],
+        "smart_low_state",
+        {
+            "on": True,
+            "targetTemperature": 25,
+            "temperatureUnit": "C",
+            "mode": "heat",
+            "fanLevel": "low",
+            "swing": "stopped",
+            "horizontalSwing": "stopped",
+            "light": "on",
+        },
+    )
+    monkeypatch.setattr(
+        get_data.parsed["ABC999111"],
+        "smart_high_state",
+        {
+            "on": True,
+            "targetTemperature": 15,
+            "temperatureUnit": "C",
+            "mode": "cool",
+            "fanLevel": "high",
+            "swing": "stopped",
+            "horizontalSwing": "stopped",
+            "light": "on",
+        },
+    )
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state1 = hass.states.get("switch.hallway_climate_react")
+    state2 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold")
+    state3 = hass.states.get("sensor.hallway_climate_react_high_temperature_threshold")
+    state4 = hass.states.get("sensor.hallway_climate_react_type")
+    assert state1.state == "on"
+    assert state2.state == "5.5"
+    assert state3.state == "30.5"
+    assert state4.state == "temperature"
+
+
+async def test_climate_climate_react_fahrenheit(
+    hass: HomeAssistant,
+    entity_registry_enabled_by_default: AsyncMock,
+    load_int: ConfigEntry,
+    monkeypatch: pytest.MonkeyPatch,
+    get_data: SensiboData,
+) -> None:
+    """Test the Sensibo climate react custom service with fahrenheit."""
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state_climate = hass.states.get("climate.hallway")
+
+    with patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ), patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react",
+        return_value={
+            "status": "success",
+            "result": {
+                "enabled": True,
+                "deviceUid": "ABC999111",
+                "highTemperatureState": {
+                    "on": True,
+                    "targetTemperature": 65,
+                    "temperatureUnit": "F",
+                    "mode": "cool",
+                    "fanLevel": "high",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                "highTemperatureThreshold": 77,
+                "lowTemperatureState": {
+                    "on": True,
+                    "targetTemperature": 85,
+                    "temperatureUnit": "F",
+                    "mode": "heat",
+                    "fanLevel": "low",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                "lowTemperatureThreshold": 32,
+                "type": "temperature",
+            },
+        },
+    ):
+        await hass.services.async_call(
+            DOMAIN,
+            SERVICE_ENABLE_CLIMATE_REACT,
+            {
+                ATTR_ENTITY_ID: state_climate.entity_id,
+                ATTR_LOW_TEMPERATURE_THRESHOLD: 32.0,
+                ATTR_HIGH_TEMPERATURE_THRESHOLD: 77.0,
+                ATTR_LOW_TEMPERATURE_STATE: {
+                    "on": True,
+                    "targetTemperature": 85,
+                    "temperatureUnit": "F",
+                    "mode": "heat",
+                    "fanLevel": "low",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                ATTR_HIGH_TEMPERATURE_STATE: {
+                    "on": True,
+                    "targetTemperature": 65,
+                    "temperatureUnit": "F",
+                    "mode": "cool",
+                    "fanLevel": "high",
+                    "swing": "stopped",
+                    "horizontalSwing": "stopped",
+                    "light": "on",
+                },
+                ATTR_SMART_TYPE: "temperature",
+            },
+            blocking=True,
+        )
+    await hass.async_block_till_done()
+
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True)
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", "temperature")
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_low_temp_threshold", 0)
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_high_temp_threshold", 25)
+    monkeypatch.setattr(
+        get_data.parsed["ABC999111"],
+        "smart_low_state",
+        {
+            "on": True,
+            "targetTemperature": 85,
+            "temperatureUnit": "F",
+            "mode": "heat",
+            "fanLevel": "low",
+            "swing": "stopped",
+            "horizontalSwing": "stopped",
+            "light": "on",
+        },
+    )
+    monkeypatch.setattr(
+        get_data.parsed["ABC999111"],
+        "smart_high_state",
+        {
+            "on": True,
+            "targetTemperature": 65,
+            "temperatureUnit": "F",
+            "mode": "cool",
+            "fanLevel": "high",
+            "swing": "stopped",
+            "horizontalSwing": "stopped",
+            "light": "on",
+        },
+    )
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state1 = hass.states.get("switch.hallway_climate_react")
+    state2 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold")
+    state3 = hass.states.get("sensor.hallway_climate_react_high_temperature_threshold")
+    state4 = hass.states.get("sensor.hallway_climate_react_type")
+    assert state1.state == "on"
+    assert state2.state == "0"
+    assert state3.state == "25"
+    assert state4.state == "temperature"
+
+
 async def test_climate_full_ac_state(
     hass: HomeAssistant,
     entity_registry_enabled_by_default: AsyncMock,
diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py
index 426416ae2b9..676e9f1f73d 100644
--- a/tests/components/sensibo/test_sensor.py
+++ b/tests/components/sensibo/test_sensor.py
@@ -2,7 +2,7 @@
 from __future__ import annotations
 
 from datetime import timedelta
-from unittest.mock import patch
+from unittest.mock import AsyncMock, patch
 
 from pysensibo.model import SensiboData
 from pytest import MonkeyPatch
@@ -16,6 +16,7 @@ from tests.common import async_fire_time_changed
 
 async def test_sensor(
     hass: HomeAssistant,
+    entity_registry_enabled_by_default: AsyncMock,
     load_int: ConfigEntry,
     monkeypatch: MonkeyPatch,
     get_data: SensiboData,
@@ -25,9 +26,11 @@ async def test_sensor(
     state1 = hass.states.get("sensor.hallway_motion_sensor_battery_voltage")
     state2 = hass.states.get("sensor.kitchen_pm2_5")
     state3 = hass.states.get("sensor.kitchen_pure_sensitivity")
+    state4 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold")
     assert state1.state == "3000"
     assert state2.state == "1"
     assert state3.state == "n"
+    assert state4.state == "0.0"
     assert state2.attributes == {
         "state_class": "measurement",
         "unit_of_measurement": "µg/m³",
@@ -35,6 +38,20 @@ async def test_sensor(
         "icon": "mdi:air-filter",
         "friendly_name": "Kitchen PM2.5",
     }
+    assert state4.attributes == {
+        "device_class": "temperature",
+        "friendly_name": "Hallway Climate React low temperature threshold",
+        "state_class": "measurement",
+        "unit_of_measurement": "°C",
+        "on": True,
+        "targetTemperature": 21,
+        "temperatureUnit": "C",
+        "mode": "heat",
+        "fanLevel": "low",
+        "swing": "stopped",
+        "horizontalSwing": "stopped",
+        "light": "on",
+    }
 
     monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pm25", 2)
 
diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py
index 2b99fa2e227..3ff0e52a0f8 100644
--- a/tests/components/sensibo/test_switch.py
+++ b/tests/components/sensibo/test_switch.py
@@ -223,3 +223,113 @@ async def test_switch_command_failure(
                 },
                 blocking=True,
             )
+
+
+async def test_switch_climate_react(
+    hass: HomeAssistant,
+    load_int: ConfigEntry,
+    monkeypatch: MonkeyPatch,
+    get_data: SensiboData,
+) -> None:
+    """Test the Sensibo switch for climate react."""
+
+    state1 = hass.states.get("switch.hallway_climate_react")
+    assert state1.state == STATE_OFF
+
+    with patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ), patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react",
+        return_value={"status": "success"},
+    ):
+        await hass.services.async_call(
+            SWITCH_DOMAIN,
+            SERVICE_TURN_ON,
+            {
+                ATTR_ENTITY_ID: state1.entity_id,
+            },
+            blocking=True,
+        )
+    await hass.async_block_till_done()
+
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True)
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+    state1 = hass.states.get("switch.hallway_climate_react")
+    assert state1.state == STATE_ON
+
+    with patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ), patch(
+        "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react",
+        return_value={"status": "success"},
+    ):
+        await hass.services.async_call(
+            SWITCH_DOMAIN,
+            SERVICE_TURN_OFF,
+            {
+                ATTR_ENTITY_ID: state1.entity_id,
+            },
+            blocking=True,
+        )
+    await hass.async_block_till_done()
+
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", False)
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state1 = hass.states.get("switch.hallway_climate_react")
+    assert state1.state == STATE_OFF
+
+
+async def test_switch_climate_react_no_data(
+    hass: HomeAssistant,
+    load_int: ConfigEntry,
+    monkeypatch: MonkeyPatch,
+    get_data: SensiboData,
+) -> None:
+    """Test the Sensibo switch for climate react."""
+
+    monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", None)
+
+    with patch(
+        "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
+        return_value=get_data,
+    ):
+        async_fire_time_changed(
+            hass,
+            dt.utcnow() + timedelta(minutes=5),
+        )
+        await hass.async_block_till_done()
+
+    state1 = hass.states.get("switch.hallway_climate_react")
+    assert state1.state == STATE_OFF
+
+    with pytest.raises(HomeAssistantError):
+        await hass.services.async_call(
+            SWITCH_DOMAIN,
+            SERVICE_TURN_ON,
+            {
+                ATTR_ENTITY_ID: state1.entity_id,
+            },
+            blocking=True,
+        )
+    await hass.async_block_till_done()
-- 
GitLab


From d85866d49cc2a2743cd4e170e063dedbb8d9da6e Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
Date: Sun, 23 Oct 2022 22:24:55 +0200
Subject: [PATCH 722/985] Fix temperature unit in sensor for Sensibo (#80843)

---
 homeassistant/components/sensibo/sensor.py | 9 ++-------
 1 file changed, 2 insertions(+), 7 deletions(-)

diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py
index 25d83089a52..22981ada51f 100644
--- a/homeassistant/components/sensibo/sensor.py
+++ b/homeassistant/components/sensibo/sensor.py
@@ -23,7 +23,6 @@ from homeassistant.const import (
     PERCENTAGE,
     SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
     TEMP_CELSIUS,
-    TEMP_FAHRENHEIT,
 )
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity import EntityCategory
@@ -263,9 +262,7 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity):
     def native_unit_of_measurement(self) -> str | None:
         """Add native unit of measurement."""
         if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE:
-            return (
-                TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT
-            )
+            return TEMP_CELSIUS
         return self.entity_description.native_unit_of_measurement
 
     @property
@@ -299,9 +296,7 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity):
     def native_unit_of_measurement(self) -> str | None:
         """Add native unit of measurement."""
         if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE:
-            return (
-                TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT
-            )
+            return TEMP_CELSIUS
         return self.entity_description.native_unit_of_measurement
 
     @property
-- 
GitLab


From d75834cd1e41ef0051490783d08076ef35d7968e Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Sun, 23 Oct 2022 22:30:03 +0200
Subject: [PATCH 723/985] Add presence duration number (#79498)

---
 homeassistant/components/deconz/number.py | 111 +++++++++++++++-------
 tests/components/deconz/test_number.py    |  54 ++++++++++-
 2 files changed, 126 insertions(+), 39 deletions(-)

diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py
index 9baa54efb56..789a155477a 100644
--- a/homeassistant/components/deconz/number.py
+++ b/homeassistant/components/deconz/number.py
@@ -1,11 +1,15 @@
-"""Support for configuring different deCONZ sensors."""
+"""Support for configuring different deCONZ numbers."""
 
 from __future__ import annotations
 
-from collections.abc import Callable
+from collections.abc import Callable, Coroutine
 from dataclasses import dataclass
+from typing import Any, Generic, TypeVar
 
+from pydeconz.gateway import DeconzSession
+from pydeconz.interfaces.sensors import SensorResources
 from pydeconz.models.event import EventType
+from pydeconz.models.sensor import SensorBase as PydeconzSensorBase
 from pydeconz.models.sensor.presence import Presence
 
 from homeassistant.components.number import (
@@ -17,39 +21,76 @@ from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
+import homeassistant.helpers.entity_registry as er
 
+from .const import DOMAIN as DECONZ_DOMAIN
 from .deconz_device import DeconzDevice
 from .gateway import DeconzGateway, get_gateway_from_config_entry
 
+T = TypeVar("T", Presence, PydeconzSensorBase)
+
 
 @dataclass
-class DeconzNumberDescriptionMixin:
+class DeconzNumberDescriptionMixin(Generic[T]):
     """Required values when describing deCONZ number entities."""
 
-    suffix: str
+    instance_check: type[T]
+    name_suffix: str
+    set_fn: Callable[[DeconzSession, str, int], Coroutine[Any, Any, dict[str, Any]]]
     update_key: str
-    value_fn: Callable[[Presence], float | None]
+    value_fn: Callable[[T], float | None]
 
 
 @dataclass
-class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin):
+class DeconzNumberDescription(NumberEntityDescription, DeconzNumberDescriptionMixin[T]):
     """Class describing deCONZ number entities."""
 
 
-ENTITY_DESCRIPTIONS = {
-    Presence: [
-        DeconzNumberDescription(
-            key="delay",
-            value_fn=lambda device: device.delay,
-            suffix="Delay",
-            update_key="delay",
-            native_max_value=65535,
-            native_min_value=0,
-            native_step=1,
-            entity_category=EntityCategory.CONFIG,
-        )
-    ]
-}
+ENTITY_DESCRIPTIONS: tuple[DeconzNumberDescription, ...] = (
+    DeconzNumberDescription[Presence](
+        key="delay",
+        instance_check=Presence,
+        name_suffix="Delay",
+        set_fn=lambda api, id, v: api.sensors.presence.set_config(id=id, delay=v),
+        update_key="delay",
+        value_fn=lambda device: device.delay,
+        native_max_value=65535,
+        native_min_value=0,
+        native_step=1,
+        entity_category=EntityCategory.CONFIG,
+    ),
+    DeconzNumberDescription[Presence](
+        key="duration",
+        instance_check=Presence,
+        name_suffix="Duration",
+        set_fn=lambda api, id, v: api.sensors.presence.set_config(id=id, duration=v),
+        update_key="duration",
+        value_fn=lambda device: device.duration,
+        native_max_value=65535,
+        native_min_value=0,
+        native_step=1,
+        entity_category=EntityCategory.CONFIG,
+    ),
+)
+
+
+@callback
+def async_update_unique_id(
+    hass: HomeAssistant, unique_id: str, description: DeconzNumberDescription
+) -> None:
+    """Update unique ID base to be on full unique ID rather than device serial.
+
+    Introduced with release 2022.11.
+    """
+    ent_reg = er.async_get(hass)
+
+    new_unique_id = f"{unique_id}-{description.key}"
+    if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id):
+        return
+
+    unique_id = f'{unique_id.split("-", 1)[0]}-{description.key}'
+    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)
 
 
 async def async_setup_entry(
@@ -66,12 +107,14 @@ async def async_setup_entry(
         """Add sensor from deCONZ."""
         sensor = gateway.api.sensors.presence[sensor_id]
 
-        for description in ENTITY_DESCRIPTIONS.get(type(sensor), []):
+        for description in ENTITY_DESCRIPTIONS:
             if (
-                not hasattr(sensor, description.key)
+                not isinstance(sensor, description.instance_check)
                 or description.value_fn(sensor) is None
             ):
                 continue
+            if description.key == "delay":
+                async_update_unique_id(hass, sensor.unique_id, description)
             async_add_entities([DeconzNumber(sensor, gateway, description)])
 
     gateway.register_platform_add_device_callback(
@@ -81,21 +124,23 @@ async def async_setup_entry(
     )
 
 
-class DeconzNumber(DeconzDevice[Presence], NumberEntity):
+class DeconzNumber(DeconzDevice[SensorResources], NumberEntity):
     """Representation of a deCONZ number entity."""
 
     TYPE = DOMAIN
+    entity_description: DeconzNumberDescription
 
     def __init__(
         self,
-        device: Presence,
+        device: SensorResources,
         gateway: DeconzGateway,
         description: DeconzNumberDescription,
     ) -> None:
         """Initialize deCONZ number entity."""
-        self.entity_description: DeconzNumberDescription = description
-        self._update_key = self.entity_description.update_key
-        self._name_suffix = description.suffix
+        self.entity_description = description
+        self.unique_id_suffix = description.key
+        self._name_suffix = description.name_suffix
+        self._update_key = description.update_key
         super().__init__(device, gateway)
 
     @property
@@ -105,12 +150,8 @@ class DeconzNumber(DeconzDevice[Presence], NumberEntity):
 
     async def async_set_native_value(self, value: float) -> None:
         """Set sensor config."""
-        await self.gateway.api.sensors.presence.set_config(
-            id=self._device.resource_id,
-            delay=int(value),
+        await self.entity_description.set_fn(
+            self.gateway.api,
+            self._device.resource_id,
+            int(value),
         )
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique identifier for this entity."""
-        return f"{self.serial}-{self.entity_description.suffix.lower()}"
diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py
index e0c469a1ba2..63dac8dde37 100644
--- a/tests/components/deconz/test_number.py
+++ b/tests/components/deconz/test_number.py
@@ -4,6 +4,7 @@ from unittest.mock import patch
 
 import pytest
 
+from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
 from homeassistant.components.number import (
     ATTR_VALUE,
     DOMAIN as NUMBER_DOMAIN,
@@ -44,7 +45,8 @@ TEST_DATA = [
             "entity_count": 3,
             "device_count": 3,
             "entity_id": "number.presence_sensor_delay",
-            "unique_id": "00:00:00:00:00:00:00:00-delay",
+            "unique_id": "00:00:00:00:00:00:00:00-00-delay",
+            "old_unique_id": "00:00:00:00:00:00:00:00-delay",
             "state": "0",
             "entity_category": EntityCategory.CONFIG,
             "attributes": {
@@ -62,7 +64,43 @@ TEST_DATA = [
             "unsupported_service_response": {"delay": 0},
             "out_of_range_service_value": 66666,
         },
-    )
+    ),
+    (  # Presence sensor - duration configuration
+        {
+            "name": "Presence sensor",
+            "type": "ZHAPresence",
+            "state": {"dark": False, "presence": False},
+            "config": {
+                "duration": 0,
+                "on": True,
+                "reachable": True,
+                "temperature": 10,
+            },
+            "uniqueid": "00:00:00:00:00:00:00:00-00",
+        },
+        {
+            "entity_count": 3,
+            "device_count": 3,
+            "entity_id": "number.presence_sensor_duration",
+            "unique_id": "00:00:00:00:00:00:00:00-00-duration",
+            "state": "0",
+            "entity_category": EntityCategory.CONFIG,
+            "attributes": {
+                "min": 0,
+                "max": 65535,
+                "step": 1,
+                "mode": "auto",
+                "friendly_name": "Presence sensor Duration",
+            },
+            "websocket_event": {"config": {"duration": 10}},
+            "next_state": "10",
+            "supported_service_value": 111,
+            "supported_service_response": {"duration": 111},
+            "unsupported_service_value": 0.1,
+            "unsupported_service_response": {"duration": 0},
+            "out_of_range_service_value": 66666,
+        },
+    ),
 ]
 
 
@@ -74,6 +112,15 @@ async def test_number_entities(
     ent_reg = er.async_get(hass)
     dev_reg = dr.async_get(hass)
 
+    # Create entity entry to migrate to new unique ID
+    if "old_unique_id" in expected:
+        ent_reg.async_get_or_create(
+            NUMBER_DOMAIN,
+            DECONZ_DOMAIN,
+            expected["old_unique_id"],
+            suggested_object_id=expected["entity_id"].replace("number.", ""),
+        )
+
     with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"0": sensor_data}}):
         config_entry = await setup_deconz_integration(hass, aioclient_mock)
 
@@ -105,8 +152,7 @@ async def test_number_entities(
         "e": "changed",
         "r": "sensors",
         "id": "0",
-        "config": {"delay": 10},
-    }
+    } | expected["websocket_event"]
     await mock_deconz_websocket(data=event_changed_sensor)
     await hass.async_block_till_done()
     assert hass.states.get(expected["entity_id"]).state == expected["next_state"]
-- 
GitLab


From 03bf37e12cc0b5d40ecf4df4e99953efb9f6f25d Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Sun, 23 Oct 2022 22:42:24 +0200
Subject: [PATCH 724/985] Refactor UniFi DPI switch entities (#80761)

* Refactor UniFi DPI switch entities

* Remove dpi presence from items_added
---
 homeassistant/components/unifi/controller.py |  15 +-
 homeassistant/components/unifi/switch.py     | 182 +++++++++----------
 tests/components/unifi/test_switch.py        |  14 +-
 3 files changed, 104 insertions(+), 107 deletions(-)

diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 1f3fd48140e..3cc786f7c86 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -9,12 +9,7 @@ from typing import Any
 
 from aiohttp import CookieJar
 import aiounifi
-from aiounifi.interfaces.messages import (
-    DATA_CLIENT_REMOVED,
-    DATA_DPI_GROUP,
-    DATA_DPI_GROUP_REMOVED,
-    DATA_EVENT,
-)
+from aiounifi.interfaces.messages import DATA_CLIENT_REMOVED, DATA_EVENT
 from aiounifi.models.event import EventKey
 from aiounifi.websocket import WebsocketSignal, WebsocketState
 import async_timeout
@@ -247,14 +242,6 @@ class UniFiController:
                     self.hass, self.signal_remove, data[DATA_CLIENT_REMOVED]
                 )
 
-            elif DATA_DPI_GROUP in data:
-                async_dispatcher_send(self.hass, self.signal_update)
-
-            elif DATA_DPI_GROUP_REMOVED in data:
-                async_dispatcher_send(
-                    self.hass, self.signal_remove, data[DATA_DPI_GROUP_REMOVED]
-                )
-
     @property
     def signal_reachable(self) -> str:
         """Integration specific event to signal a change in connection status."""
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index 5e129cc402e..ed55b155559 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -33,7 +33,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
 
 from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
 from .unifi_client import UniFiClient
-from .unifi_entity_base import UniFiBase
 
 BLOCK_SWITCH = "block"
 DPI_SWITCH = "dpi"
@@ -88,7 +87,7 @@ async def async_setup_entry(
     @callback
     def items_added(
         clients: set = controller.api.clients,
-        dpi_groups: set = controller.api.dpi_groups,
+        devices: set = controller.api.devices,
     ) -> None:
         """Update the values of the controller."""
         if controller.option_block_clients:
@@ -97,9 +96,6 @@ async def async_setup_entry(
         if controller.option_poe_clients:
             add_poe_entities(controller, async_add_entities, clients, known_poe_clients)
 
-        if controller.option_dpi_restrictions:
-            add_dpi_entities(controller, async_add_entities, dpi_groups)
-
     for signal in (controller.signal_update, controller.signal_options_update):
         config_entry.async_on_unload(
             async_dispatcher_connect(hass, signal, items_added)
@@ -120,6 +116,20 @@ async def async_setup_entry(
     for index in controller.api.outlets:
         async_add_outlet_switch(ItemEvent.ADDED, index)
 
+    def async_add_dpi_switch(_: ItemEvent, obj_id: str) -> None:
+        """Add DPI switch from UniFi controller."""
+        if (
+            not controller.option_dpi_restrictions
+            or not controller.api.dpi_groups[obj_id].dpiapp_ids
+        ):
+            return
+        async_add_entities([UnifiDPIRestrictionSwitch(obj_id, controller)])
+
+    controller.api.ports.subscribe(async_add_dpi_switch, ItemEvent.ADDED)
+
+    for dpi_group_id in controller.api.dpi_groups:
+        async_add_dpi_switch(ItemEvent.ADDED, dpi_group_id)
+
     @callback
     def async_add_poe_switch(_: ItemEvent, obj_id: str) -> None:
         """Add port PoE switch from UniFi controller."""
@@ -198,23 +208,6 @@ def add_poe_entities(controller, async_add_entities, clients, known_poe_clients)
     async_add_entities(switches)
 
 
-@callback
-def add_dpi_entities(controller, async_add_entities, dpi_groups):
-    """Add new switch entities from the controller."""
-    switches = []
-
-    for group in dpi_groups:
-        if (
-            group in controller.entities[DOMAIN][DPI_SWITCH]
-            or not dpi_groups[group].dpiapp_ids
-        ):
-            continue
-
-        switches.append(UniFiDPIRestrictionSwitch(dpi_groups[group], controller))
-
-    async_add_entities(switches)
-
-
 class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity):
     """Representation of a client that uses POE."""
 
@@ -367,132 +360,139 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchEntity):
             await self.remove_item({self.client.mac})
 
 
-class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity):
+class UnifiDPIRestrictionSwitch(SwitchEntity):
     """Representation of a DPI restriction group."""
 
-    DOMAIN = DOMAIN
-    TYPE = DPI_SWITCH
-
     _attr_entity_category = EntityCategory.CONFIG
 
-    def __init__(self, dpi_group, controller):
+    def __init__(self, obj_id: str, controller):
         """Set up dpi switch."""
-        super().__init__(dpi_group, controller)
+        controller.entities[DOMAIN][DPI_SWITCH].add(obj_id)
+        self._obj_id = obj_id
+        self.controller = controller
 
-        self._is_enabled = self.calculate_enabled()
+        dpi_group = controller.api.dpi_groups[obj_id]
         self._known_app_ids = dpi_group.dpiapp_ids
 
-    @property
-    def key(self) -> Any:
-        """Return item key."""
-        return self._item.id
+        self._attr_available = controller.available
+        self._attr_is_on = self.calculate_enabled()
+        self._attr_name = dpi_group.name
+        self._attr_unique_id = dpi_group.id
+        self._attr_device_info = DeviceInfo(
+            entry_type=DeviceEntryType.SERVICE,
+            identifiers={(DOMAIN, f"unifi_controller_{obj_id}")},
+            manufacturer=ATTR_MANUFACTURER,
+            model="UniFi Network",
+            name="UniFi Network",
+        )
 
     async def async_added_to_hass(self) -> None:
         """Register callback to known apps."""
-        await super().async_added_to_hass()
-
-        apps = self.controller.api.dpi_apps
-        for app_id in self._item.dpiapp_ids:
-            apps[app_id].register_callback(self.async_update_callback)
+        self.async_on_remove(
+            self.controller.api.dpi_groups.subscribe(self.async_signalling_callback)
+        )
+        self.async_on_remove(
+            self.controller.api.dpi_apps.subscribe(
+                self.async_signalling_callback, ItemEvent.CHANGED
+            ),
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass, self.controller.signal_remove, self.remove_item
+            )
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass, self.controller.signal_options_update, self.options_updated
+            )
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                self.controller.signal_reachable,
+                self.async_signal_reachable_callback,
+            )
+        )
 
     async def async_will_remove_from_hass(self) -> None:
-        """Remove registered callbacks."""
-        apps = self.controller.api.dpi_apps
-        for app_id in self._item.dpiapp_ids:
-            apps[app_id].remove_callback(self.async_update_callback)
-
-        await super().async_will_remove_from_hass()
+        """Disconnect object when removed."""
+        self.controller.entities[DOMAIN][DPI_SWITCH].remove(self._obj_id)
 
     @callback
-    def async_update_callback(self) -> None:
-        """Update the DPI switch state.
-
-        Remove entity when no apps are paired with group.
-        Register callbacks to new apps.
-        Calculate and update entity state if it has changed.
-        """
-        if not self._item.dpiapp_ids:
-            self.hass.async_create_task(self.remove_item({self.key}))
+    def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
+        """Object has new event."""
+        if event == ItemEvent.DELETED:
+            self.hass.async_create_task(self.remove_item({self._obj_id}))
             return
 
-        if self._known_app_ids != self._item.dpiapp_ids:
-            self._known_app_ids = self._item.dpiapp_ids
-
-            apps = self.controller.api.dpi_apps
-            for app_id in self._item.dpiapp_ids:
-                apps[app_id].register_callback(self.async_update_callback)
-
-        if (enabled := self.calculate_enabled()) != self._is_enabled:
-            self._is_enabled = enabled
-            super().async_update_callback()
+        dpi_group = self.controller.api.dpi_groups[self._obj_id]
+        if not dpi_group.dpiapp_ids:
+            self.hass.async_create_task(self.remove_item({self._obj_id}))
+            return
 
-    @property
-    def unique_id(self):
-        """Return a unique identifier for this switch."""
-        return self._item.id
+        self._attr_available = self.controller.available
+        self._attr_is_on = self.calculate_enabled()
+        self.async_write_ha_state()
 
-    @property
-    def name(self) -> str:
-        """Return the name of the DPI group."""
-        return self._item.name
+    @callback
+    def async_signal_reachable_callback(self) -> None:
+        """Call when controller connection state change."""
+        self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
 
     @property
     def icon(self):
         """Return the icon to use in the frontend."""
-        if self._is_enabled:
+        if self._attr_is_on:
             return "mdi:network"
         return "mdi:network-off"
 
     def calculate_enabled(self) -> bool:
         """Calculate if all apps are enabled."""
+        dpi_group = self.controller.api.dpi_groups[self._obj_id]
         return all(
             self.controller.api.dpi_apps[app_id].enabled
-            for app_id in self._item.dpiapp_ids
+            for app_id in dpi_group.dpiapp_ids
             if app_id in self.controller.api.dpi_apps
         )
 
-    @property
-    def is_on(self):
-        """Return true if DPI group app restriction is enabled."""
-        return self._is_enabled
-
     async def async_turn_on(self, **kwargs: Any) -> None:
         """Restrict access of apps related to DPI group."""
+        dpi_group = self.controller.api.dpi_groups[self._obj_id]
         return await asyncio.gather(
             *[
                 self.controller.api.request(
                     DPIRestrictionAppEnableRequest.create(app_id, True)
                 )
-                for app_id in self._item.dpiapp_ids
+                for app_id in dpi_group.dpiapp_ids
             ]
         )
 
     async def async_turn_off(self, **kwargs: Any) -> None:
         """Remove restriction of apps related to DPI group."""
+        dpi_group = self.controller.api.dpi_groups[self._obj_id]
         return await asyncio.gather(
             *[
                 self.controller.api.request(
                     DPIRestrictionAppEnableRequest.create(app_id, False)
                 )
-                for app_id in self._item.dpiapp_ids
+                for app_id in dpi_group.dpiapp_ids
             ]
         )
 
     async def options_updated(self) -> None:
         """Config entry options are updated, remove entity if option is disabled."""
         if not self.controller.option_dpi_restrictions:
-            await self.remove_item({self.key})
+            await self.remove_item({self._attr_unique_id})
 
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return a service description for device registry."""
-        return DeviceInfo(
-            entry_type=DeviceEntryType.SERVICE,
-            identifiers={(DOMAIN, f"unifi_controller_{self._item.site_id}")},
-            manufacturer=ATTR_MANUFACTURER,
-            model="UniFi Network",
-            name="UniFi Network",
-        )
+    async def remove_item(self, keys: set) -> None:
+        """Remove entity if key is part of set."""
+        if self._attr_unique_id not in keys:
+            return
+
+        if self.registry_entry:
+            er.async_get(self.hass).async_remove(self.entity_id)
+        else:
+            await self.async_remove(force_remove=True)
 
 
 class UnifiOutletSwitch(SwitchEntity):
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index 12ef6f9b965..db0b358179c 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -761,7 +761,6 @@ async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     mock_unifi_websocket(data=DPI_GROUP_REMOVED_EVENT)
     await hass.async_block_till_done()
-    await hass.async_block_till_done()
 
     assert hass.states.get("switch.block_media_streaming") is None
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
@@ -852,9 +851,20 @@ async def test_dpi_switches(hass, aioclient_mock, mock_unifi_websocket):
 
     assert hass.states.get("switch.block_media_streaming").state == STATE_OFF
 
-    mock_unifi_websocket(data=DPI_GROUP_REMOVE_APP)
+    # Availability signalling
+
+    # Controller disconnects
+    mock_unifi_websocket(state=WebsocketState.DISCONNECTED)
     await hass.async_block_till_done()
+    assert hass.states.get("switch.block_media_streaming").state == STATE_UNAVAILABLE
+
+    # Controller reconnects
+    mock_unifi_websocket(state=WebsocketState.RUNNING)
     await hass.async_block_till_done()
+    assert hass.states.get("switch.block_media_streaming").state == STATE_OFF
+
+    # Remove app
+    mock_unifi_websocket(data=DPI_GROUP_REMOVE_APP)
     await hass.async_block_till_done()
 
     assert hass.states.get("switch.block_media_streaming") is None
-- 
GitLab


From 3f3518e29d3c7f18b68010db312537475c949e29 Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
Date: Sun, 23 Oct 2022 18:28:15 -0400
Subject: [PATCH 725/985] Fix Lutron Caseta area names by ignoring root area
 during area name retrieval (#80576)

---
 .../components/lutron_caseta/__init__.py      | 20 ++++++++++++-------
 .../components/lutron_caseta/manifest.json    |  2 +-
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 tests/components/lutron_caseta/__init__.py    | 11 +++++-----
 .../lutron_caseta/test_diagnostics.py         | 11 +++++-----
 6 files changed, 28 insertions(+), 20 deletions(-)

diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py
index 5e6d656bf35..385fdf94a62 100644
--- a/homeassistant/components/lutron_caseta/__init__.py
+++ b/homeassistant/components/lutron_caseta/__init__.py
@@ -375,19 +375,25 @@ 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_name_from_id(areas: dict[str, dict], area_id: str) -> str:
+def _area_name_from_id(areas: dict[str, dict], area_id: str | None) -> str:
     """Return the full area name including parent(s)."""
-
     if area_id is None:
         return UNASSIGNED_AREA
+    return _construct_area_name_from_id(areas, area_id, [])
+
 
+def _construct_area_name_from_id(
+    areas: dict[str, dict], area_id: str, labels: list[str]
+) -> str:
+    """Recursively construct the full area name including parent(s)."""
     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']}"
+    parent_area_id = area["parent_id"]
+    if parent_area_id is None:
+        # This is the root area, return last area
+        return " ".join(labels)
 
-    return area["name"]
+    labels.insert(0, area["name"])
+    return _construct_area_name_from_id(areas, parent_area_id, labels)
 
 
 @callback
diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json
index ad933dc0a69..b4f790292c4 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.16.0"],
+  "requirements": ["pylutron-caseta==0.17.1"],
   "config_flow": true,
   "zeroconf": ["_leap._tcp.local."],
   "homekit": {
diff --git a/requirements_all.txt b/requirements_all.txt
index c6eaf9ec492..dd1a8e53270 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1688,7 +1688,7 @@ pylitejet==0.3.0
 pylitterbot==2022.10.2
 
 # homeassistant.components.lutron_caseta
-pylutron-caseta==0.16.0
+pylutron-caseta==0.17.1
 
 # homeassistant.components.lutron
 pylutron==0.2.8
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e2c6020a5bd..72f67faf438 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1189,7 +1189,7 @@ pylitejet==0.3.0
 pylitterbot==2022.10.2
 
 # homeassistant.components.lutron_caseta
-pylutron-caseta==0.16.0
+pylutron-caseta==0.17.1
 
 # homeassistant.components.mailgun
 pymailgunner==1.4
diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py
index c5d63c9e256..5e3db30ad5b 100644
--- a/tests/components/lutron_caseta/__init__.py
+++ b/tests/components/lutron_caseta/__init__.py
@@ -129,13 +129,14 @@ class MockBridge:
     def load_areas(self):
         """Loak mock areas into self.areas."""
         return {
-            "898": {"id": "898", "name": "Basement", "parent_id": None},
+            "3": {"id": "3", "name": "House", "parent_id": None},
+            "898": {"id": "898", "name": "Basement", "parent_id": "3"},
             "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},
+            "1024": {"id": "1024", "name": "Master Bedroom", "parent_id": "3"},
+            "1025": {"id": "1025", "name": "Kitchen", "parent_id": "3"},
+            "1026": {"id": "1026", "name": "Dining Room", "parent_id": "3"},
+            "1205": {"id": "1205", "name": "Hallway", "parent_id": "3"},
         }
 
     def load_devices(self):
diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py
index 80643737aaa..98a5b26e809 100644
--- a/tests/components/lutron_caseta/test_diagnostics.py
+++ b/tests/components/lutron_caseta/test_diagnostics.py
@@ -41,13 +41,14 @@ async def test_diagnostics(hass, hass_client) -> None:
     assert diag == {
         "bridge_data": {
             "areas": {
-                "898": {"id": "898", "name": "Basement", "parent_id": None},
+                "3": {"id": "3", "name": "House", "parent_id": None},
+                "898": {"id": "898", "name": "Basement", "parent_id": "3"},
                 "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},
+                "1024": {"id": "1024", "name": "Master Bedroom", "parent_id": "3"},
+                "1025": {"id": "1025", "name": "Kitchen", "parent_id": "3"},
+                "1026": {"id": "1026", "name": "Dining Room", "parent_id": "3"},
+                "1205": {"id": "1205", "name": "Hallway", "parent_id": "3"},
             },
             "buttons": {
                 "111": {
-- 
GitLab


From 679562773442d40cb4cc3d41b787a5a87308aed5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Julian=20L=C3=B6hr?= <julian.loehr@outlook.com>
Date: Mon, 24 Oct 2022 00:46:47 +0200
Subject: [PATCH 726/985] Add ZHA StartUpColorTemperature configuration entity
 (#80853)

* Add ZHA start up color temperature entity

* Use device reported min max values

* Add color number test

* Fix code style
---
 .../components/zha/core/channels/lighting.py  |   1 +
 homeassistant/components/zha/number.py        |  26 ++++
 tests/components/zha/test_number.py           | 114 +++++++++++++++++-
 3 files changed, 140 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py
index 1754b9aff68..e70eea11a87 100644
--- a/homeassistant/components/zha/core/channels/lighting.py
+++ b/homeassistant/components/zha/core/channels/lighting.py
@@ -44,6 +44,7 @@ class ColorChannel(ZigbeeChannel):
         "color_temp_physical_max": True,
         "color_capabilities": True,
         "color_loop_active": False,
+        "start_up_color_temperature": True,
     }
 
     @cached_property
diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py
index 9203986057d..1776cabf125 100644
--- a/homeassistant/components/zha/number.py
+++ b/homeassistant/components/zha/number.py
@@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from .core import discovery
 from .core.const import (
     CHANNEL_ANALOG_OUTPUT,
+    CHANNEL_COLOR,
     CHANNEL_INOVELLI,
     CHANNEL_LEVEL,
     DATA_ZHA,
@@ -528,6 +529,31 @@ class StartUpCurrentLevelConfigurationEntity(
     _attr_name = "Start-up current level"
 
 
+@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_COLOR)
+class StartUpColorTemperatureConfigurationEntity(
+    ZHANumberConfigurationEntity, id_suffix="start_up_color_temperature"
+):
+    """Representation of a ZHA startup color temperature configuration entity."""
+
+    _attr_native_min_value: float = 153
+    _attr_native_max_value: float = 500
+    _zcl_attribute: str = "start_up_color_temperature"
+    _attr_name = "Start-up color temperature"
+
+    def __init__(
+        self,
+        unique_id: str,
+        zha_device: ZHADevice,
+        channels: list[ZigbeeChannel],
+        **kwargs: Any,
+    ) -> None:
+        """Init this ZHA startup color temperature entity."""
+        super().__init__(unique_id, zha_device, channels, **kwargs)
+        if self._channel:
+            self._attr_native_min_value: float = self._channel.min_mireds
+            self._attr_native_max_value: float = self._channel.max_mireds
+
+
 @CONFIG_DIAGNOSTIC_MATCH(
     channel_names="tuya_manufacturer",
     manufacturers={
diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py
index 6af98b35e09..219c77f76d7 100644
--- a/tests/components/zha/test_number.py
+++ b/tests/components/zha/test_number.py
@@ -5,6 +5,7 @@ import pytest
 from zigpy.exceptions import ZigbeeException
 from zigpy.profiles import zha
 import zigpy.zcl.clusters.general as general
+import zigpy.zcl.clusters.lighting as lighting
 import zigpy.zcl.foundation as zcl_f
 
 from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
@@ -64,12 +65,13 @@ async def light(zigpy_device_mock):
         {
             1: {
                 SIG_EP_PROFILE: zha.PROFILE_ID,
-                SIG_EP_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
                 SIG_EP_INPUT: [
                     general.Basic.cluster_id,
                     general.Identify.cluster_id,
                     general.OnOff.cluster_id,
                     general.LevelControl.cluster_id,
+                    lighting.Color.cluster_id,
                 ],
                 SIG_EP_OUTPUT: [general.Ota.cluster_id],
             }
@@ -322,3 +324,113 @@ async def test_level_control_number(
         attr: new_value,
     }
     assert hass.states.get(entity_id).state == str(initial_value)
+
+
+@pytest.mark.parametrize(
+    "attr, initial_value, new_value",
+    (("start_up_color_temperature", 500, 350),),
+)
+async def test_color_number(
+    hass, light, zha_device_joined, attr, initial_value, new_value
+):
+    """Test zha color number entities - new join."""
+
+    entity_registry = er.async_get(hass)
+    color_cluster = light.endpoints[1].light_color
+    color_cluster.PLUGGED_ATTR_READS = {
+        attr: initial_value,
+    }
+    zha_device = await zha_device_joined(light)
+
+    entity_id = await find_entity_id(
+        Platform.NUMBER,
+        zha_device,
+        hass,
+        qualifier=attr,
+    )
+    assert entity_id is not None
+
+    assert color_cluster.read_attributes.call_count == 3
+    assert (
+        call(
+            [
+                "color_temp_physical_min",
+                "color_temp_physical_max",
+                "color_capabilities",
+                "start_up_color_temperature",
+            ],
+            allow_cache=True,
+            only_cache=False,
+            manufacturer=None,
+        )
+        in color_cluster.read_attributes.call_args_list
+    )
+
+    state = hass.states.get(entity_id)
+    assert state
+    assert state.state == str(initial_value)
+
+    entity_entry = entity_registry.async_get(entity_id)
+    assert entity_entry
+    assert entity_entry.entity_category == EntityCategory.CONFIG
+
+    # Test number set_value
+    await hass.services.async_call(
+        "number",
+        "set_value",
+        {
+            "entity_id": entity_id,
+            "value": new_value,
+        },
+        blocking=True,
+    )
+
+    assert color_cluster.write_attributes.call_count == 1
+    assert color_cluster.write_attributes.call_args[0][0] == {
+        attr: new_value,
+    }
+
+    state = hass.states.get(entity_id)
+    assert state
+    assert state.state == str(new_value)
+
+    color_cluster.read_attributes.reset_mock()
+    await async_setup_component(hass, "homeassistant", {})
+    await hass.async_block_till_done()
+
+    await hass.services.async_call(
+        "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
+    )
+    # the mocking doesn't update the attr cache so this flips back to initial value
+    assert hass.states.get(entity_id).state == str(initial_value)
+    assert color_cluster.read_attributes.call_count == 1
+    assert (
+        call(
+            [
+                attr,
+            ],
+            allow_cache=False,
+            only_cache=False,
+            manufacturer=None,
+        )
+        in color_cluster.read_attributes.call_args_list
+    )
+
+    color_cluster.write_attributes.reset_mock()
+    color_cluster.write_attributes.side_effect = ZigbeeException
+
+    await hass.services.async_call(
+        "number",
+        "set_value",
+        {
+            "entity_id": entity_id,
+            "value": new_value,
+        },
+        blocking=True,
+    )
+
+    assert color_cluster.write_attributes.call_count == 1
+    assert color_cluster.write_attributes.call_args[0][0] == {
+        attr: new_value,
+    }
+    assert hass.states.get(entity_id).state == str(initial_value)
-- 
GitLab


From 59b2869f6a1cb26b4502c077dd5add45271fa937 Mon Sep 17 00:00:00 2001
From: PeteRager <76050312+PeteRager@users.noreply.github.com>
Date: Sun, 23 Oct 2022 19:01:43 -0400
Subject: [PATCH 727/985] Fix oncue data unavailable when genset disconnected
 (#80668)

Co-authored-by: J. Nick Koston <nick@koston.org>
---
 CODEOWNERS                                   |   4 +-
 homeassistant/components/oncue/const.py      |   4 +
 homeassistant/components/oncue/entity.py     |  22 +-
 homeassistant/components/oncue/manifest.json |   2 +-
 tests/components/oncue/__init__.py           | 289 +++++++++++++++++++
 tests/components/oncue/test_binary_sensor.py |  26 +-
 tests/components/oncue/test_sensor.py        | 165 ++++++++++-
 7 files changed, 504 insertions(+), 8 deletions(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index e1e1d7282d3..2d9bcf41db8 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -798,8 +798,8 @@ build.json @home-assistant/supervisor
 /tests/components/omnilogic/ @oliver84 @djtimca @gentoosu
 /homeassistant/components/onboarding/ @home-assistant/core
 /tests/components/onboarding/ @home-assistant/core
-/homeassistant/components/oncue/ @bdraco
-/tests/components/oncue/ @bdraco
+/homeassistant/components/oncue/ @bdraco @peterager
+/tests/components/oncue/ @bdraco @peterager
 /homeassistant/components/ondilo_ico/ @JeromeHXP
 /tests/components/ondilo_ico/ @JeromeHXP
 /homeassistant/components/onewire/ @garbled1 @epenet
diff --git a/homeassistant/components/oncue/const.py b/homeassistant/components/oncue/const.py
index 5adabc84bcf..bf248369987 100644
--- a/homeassistant/components/oncue/const.py
+++ b/homeassistant/components/oncue/const.py
@@ -7,3 +7,7 @@ import aiohttp
 DOMAIN = "oncue"
 
 CONNECTION_EXCEPTIONS = (asyncio.TimeoutError, aiohttp.ClientError)
+
+CONNECTION_ESTABLISHED_KEY: str = "NetworkConnectionEstablished"
+
+VALUE_UNAVAILABLE: str = "--"
diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py
index d1942c532e7..60a3826df42 100644
--- a/homeassistant/components/oncue/entity.py
+++ b/homeassistant/components/oncue/entity.py
@@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import (
     DataUpdateCoordinator,
 )
 
-from .const import DOMAIN
+from .const import CONNECTION_ESTABLISHED_KEY, DOMAIN, VALUE_UNAVAILABLE
 
 
 class OncueEntity(CoordinatorEntity, Entity):
@@ -53,3 +53,23 @@ class OncueEntity(CoordinatorEntity, Entity):
         device: OncueDevice = self.coordinator.data[self._device_id]
         sensor: OncueSensor = device.sensors[self.entity_description.key]
         return sensor.value
+
+    @property
+    def available(self) -> bool:
+        """Return if entity is available."""
+        # The binary sensor that tracks the connection should not go unavailable.
+        if self.entity_description.key != CONNECTION_ESTABLISHED_KEY:
+            # If Kohler returns -- the entity is unavailable.
+            if self._oncue_value == VALUE_UNAVAILABLE:
+                return False
+            # If the cloud is reporting that the generator is not connected
+            # this also indicates the data is not available.
+            # The battery voltage sensor reports 0.0 rather than -- hence the purpose of this check.
+            device: OncueDevice = self.coordinator.data[self._device_id]
+            conn_established: OncueSensor = device.sensors[CONNECTION_ESTABLISHED_KEY]
+            if (
+                conn_established is not None
+                and conn_established.value == VALUE_UNAVAILABLE
+            ):
+                return False
+        return super().available
diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json
index e0533129d94..26a55cd0a96 100644
--- a/homeassistant/components/oncue/manifest.json
+++ b/homeassistant/components/oncue/manifest.json
@@ -10,7 +10,7 @@
   ],
   "documentation": "https://www.home-assistant.io/integrations/oncue",
   "requirements": ["aiooncue==0.3.4"],
-  "codeowners": ["@bdraco"],
+  "codeowners": ["@bdraco","@peterager"],
   "iot_class": "cloud_polling",
   "loggers": ["aiooncue"]
 }
diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py
index 32845aa8d26..2ddaf1987f8 100644
--- a/tests/components/oncue/__init__.py
+++ b/tests/components/oncue/__init__.py
@@ -533,6 +533,270 @@ MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE = {
     )
 }
 
+MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE = {
+    "456789": OncueDevice(
+        name="My Generator",
+        state="Off",
+        product_name="RDC 2.4",
+        hardware_version="319",
+        serial_number="SERIAL",
+        sensors={
+            "Product": OncueSensor(
+                name="Product",
+                display_name="Controller Type",
+                value="--",
+                display_value="RDC 2.4",
+                unit=None,
+            ),
+            "FirmwareVersion": OncueSensor(
+                name="FirmwareVersion",
+                display_name="Current Firmware",
+                value="--",
+                display_value="2.0.6",
+                unit=None,
+            ),
+            "LatestFirmware": OncueSensor(
+                name="LatestFirmware",
+                display_name="Latest Firmware",
+                value="--",
+                display_value="2.0.6",
+                unit=None,
+            ),
+            "EngineSpeed": OncueSensor(
+                name="EngineSpeed",
+                display_name="Engine Speed",
+                value="--",
+                display_value="0 R/min",
+                unit="R/min",
+            ),
+            "EngineTargetSpeed": OncueSensor(
+                name="EngineTargetSpeed",
+                display_name="Engine Target Speed",
+                value="--",
+                display_value="0 R/min",
+                unit="R/min",
+            ),
+            "EngineOilPressure": OncueSensor(
+                name="EngineOilPressure",
+                display_name="Engine Oil Pressure",
+                value="--",
+                display_value="0 Psi",
+                unit="Psi",
+            ),
+            "EngineCoolantTemperature": OncueSensor(
+                name="EngineCoolantTemperature",
+                display_name="Engine Coolant Temperature",
+                value="--",
+                display_value="32 F",
+                unit="F",
+            ),
+            "BatteryVoltage": OncueSensor(
+                name="BatteryVoltage",
+                display_name="Battery Voltage",
+                value="0.0",
+                display_value="13.4 V",
+                unit="V",
+            ),
+            "LubeOilTemperature": OncueSensor(
+                name="LubeOilTemperature",
+                display_name="Lube Oil Temperature",
+                value="--",
+                display_value="32 F",
+                unit="F",
+            ),
+            "GensetControllerTemperature": OncueSensor(
+                name="GensetControllerTemperature",
+                display_name="Generator Controller Temperature",
+                value="--",
+                display_value="84.2 F",
+                unit="F",
+            ),
+            "EngineCompartmentTemperature": OncueSensor(
+                name="EngineCompartmentTemperature",
+                display_name="Engine Compartment Temperature",
+                value="--",
+                display_value="62.6 F",
+                unit="F",
+            ),
+            "GeneratorTrueTotalPower": OncueSensor(
+                name="GeneratorTrueTotalPower",
+                display_name="Generator True Total Power",
+                value="--",
+                display_value="0.0 W",
+                unit="W",
+            ),
+            "GeneratorTruePercentOfRatedPower": OncueSensor(
+                name="GeneratorTruePercentOfRatedPower",
+                display_name="Generator True Percent Of Rated Power",
+                value="--",
+                display_value="0 %",
+                unit="%",
+            ),
+            "GeneratorVoltageAB": OncueSensor(
+                name="GeneratorVoltageAB",
+                display_name="Generator Voltage AB",
+                value="--",
+                display_value="0.0 V",
+                unit="V",
+            ),
+            "GeneratorVoltageAverageLineToLine": OncueSensor(
+                name="GeneratorVoltageAverageLineToLine",
+                display_name="Generator Voltage Average Line To Line",
+                value="--",
+                display_value="0.0 V",
+                unit="V",
+            ),
+            "GeneratorCurrentAverage": OncueSensor(
+                name="GeneratorCurrentAverage",
+                display_name="Generator Current Average",
+                value="--",
+                display_value="0.0 A",
+                unit="A",
+            ),
+            "GeneratorFrequency": OncueSensor(
+                name="GeneratorFrequency",
+                display_name="Generator Frequency",
+                value="--",
+                display_value="0.0 Hz",
+                unit="Hz",
+            ),
+            "GensetSerialNumber": OncueSensor(
+                name="GensetSerialNumber",
+                display_name="Generator Serial Number",
+                value="--",
+                display_value="33FDGMFR0026",
+                unit=None,
+            ),
+            "GensetState": OncueSensor(
+                name="GensetState",
+                display_name="Generator State",
+                value="--",
+                display_value="Off",
+                unit=None,
+            ),
+            "GensetControllerSerialNumber": OncueSensor(
+                name="GensetControllerSerialNumber",
+                display_name="Generator Controller Serial Number",
+                value="--",
+                display_value="-1",
+                unit=None,
+            ),
+            "GensetModelNumberSelect": OncueSensor(
+                name="GensetModelNumberSelect",
+                display_name="Genset Model Number Select",
+                value="--",
+                display_value="38 RCLB",
+                unit=None,
+            ),
+            "GensetControllerClockTime": OncueSensor(
+                name="GensetControllerClockTime",
+                display_name="Generator Controller Clock Time",
+                value="--",
+                display_value="2022-01-13 18:08:13",
+                unit=None,
+            ),
+            "GensetControllerTotalOperationTime": OncueSensor(
+                name="GensetControllerTotalOperationTime",
+                display_name="Generator Controller Total Operation Time",
+                value="--",
+                display_value="16770.8 h",
+                unit="h",
+            ),
+            "EngineTotalRunTime": OncueSensor(
+                name="EngineTotalRunTime",
+                display_name="Engine Total Run Time",
+                value="--",
+                display_value="28.1 h",
+                unit="h",
+            ),
+            "EngineTotalRunTimeLoaded": OncueSensor(
+                name="EngineTotalRunTimeLoaded",
+                display_name="Engine Total Run Time Loaded",
+                value="--",
+                display_value="5.5 h",
+                unit="h",
+            ),
+            "EngineTotalNumberOfStarts": OncueSensor(
+                name="EngineTotalNumberOfStarts",
+                display_name="Engine Total Number Of Starts",
+                value="--",
+                display_value="101",
+                unit=None,
+            ),
+            "GensetTotalEnergy": OncueSensor(
+                name="GensetTotalEnergy",
+                display_name="Genset Total Energy",
+                value="--",
+                display_value="1.2022309E7 kWh",
+                unit="kWh",
+            ),
+            "AtsContactorPosition": OncueSensor(
+                name="AtsContactorPosition",
+                display_name="Ats Contactor Position",
+                value="--",
+                display_value="Source1",
+                unit=None,
+            ),
+            "AtsSourcesAvailable": OncueSensor(
+                name="AtsSourcesAvailable",
+                display_name="Ats Sources Available",
+                value="--",
+                display_value="Source1",
+                unit=None,
+            ),
+            "Source1VoltageAverageLineToLine": OncueSensor(
+                name="Source1VoltageAverageLineToLine",
+                display_name="Source1 Voltage Average Line To Line",
+                value="--",
+                display_value="253.5 V",
+                unit="V",
+            ),
+            "Source2VoltageAverageLineToLine": OncueSensor(
+                name="Source2VoltageAverageLineToLine",
+                display_name="Source2 Voltage Average Line To Line",
+                value="--",
+                display_value="0.0 V",
+                unit="V",
+            ),
+            "IPAddress": OncueSensor(
+                name="IPAddress",
+                display_name="IP Address",
+                value="--",
+                display_value="1.2.3.4:1026",
+                unit=None,
+            ),
+            "MacAddress": OncueSensor(
+                name="MacAddress",
+                display_name="Mac Address",
+                value="--",
+                display_value="--",
+                unit=None,
+            ),
+            "ConnectedServerIPAddress": OncueSensor(
+                name="ConnectedServerIPAddress",
+                display_name="Connected Server IP Address",
+                value="--",
+                display_value="40.117.195.28",
+                unit=None,
+            ),
+            "NetworkConnectionEstablished": OncueSensor(
+                name="NetworkConnectionEstablished",
+                display_name="Network Connection Established",
+                value="--",
+                display_value="True",
+                unit=None,
+            ),
+            "SerialNumber": OncueSensor(
+                name="SerialNumber",
+                display_name="Serial Number",
+                value="--",
+                display_value="1073879692",
+                unit=None,
+            ),
+        },
+    )
+}
+
 
 def _patch_login_and_data():
     @contextmanager
@@ -556,3 +820,28 @@ def _patch_login_and_data_offline_device():
             yield
 
     return _patcher()
+
+
+def _patch_login_and_data_unavailable():
+    @contextmanager
+    def _patcher():
+        with patch("homeassistant.components.oncue.Oncue.async_login"), patch(
+            "homeassistant.components.oncue.Oncue.async_fetch_all",
+            return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE,
+        ):
+            yield
+
+    return _patcher()
+
+
+def _patch_login_and_data_unavailable_device():
+    @contextmanager
+    def _patcher():
+
+        with patch("homeassistant.components.oncue.Oncue.async_login"), patch(
+            "homeassistant.components.oncue.Oncue.async_fetch_all",
+            return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE,
+        ):
+            yield
+
+    return _patcher()
diff --git a/tests/components/oncue/test_binary_sensor.py b/tests/components/oncue/test_binary_sensor.py
index 020b914c76b..f2e7657089f 100644
--- a/tests/components/oncue/test_binary_sensor.py
+++ b/tests/components/oncue/test_binary_sensor.py
@@ -4,11 +4,11 @@ from __future__ import annotations
 from homeassistant.components import oncue
 from homeassistant.components.oncue.const import DOMAIN
 from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_ON
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON
 from homeassistant.core import HomeAssistant
 from homeassistant.setup import async_setup_component
 
-from . import _patch_login_and_data
+from . import _patch_login_and_data, _patch_login_and_data_unavailable
 
 from tests.common import MockConfigEntry
 
@@ -33,3 +33,25 @@ async def test_binary_sensors(hass: HomeAssistant) -> None:
         ).state
         == STATE_ON
     )
+
+
+async def test_binary_sensors_not_unavailable(hass: HomeAssistant) -> None:
+    """Test the network connection established binary sensor is available when connection status is false."""
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_USERNAME: "any", CONF_PASSWORD: "any"},
+        unique_id="any",
+    )
+    config_entry.add_to_hass(hass)
+    with _patch_login_and_data_unavailable():
+        await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}})
+        await hass.async_block_till_done()
+    assert config_entry.state == ConfigEntryState.LOADED
+
+    assert len(hass.states.async_all("binary_sensor")) == 1
+    assert (
+        hass.states.get(
+            "binary_sensor.my_generator_network_connection_established"
+        ).state
+        == STATE_OFF
+    )
diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py
index 60c9f68f81b..6319bcdd9f9 100644
--- a/tests/components/oncue/test_sensor.py
+++ b/tests/components/oncue/test_sensor.py
@@ -6,12 +6,17 @@ import pytest
 from homeassistant.components import oncue
 from homeassistant.components.oncue.const import DOMAIN
 from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import device_registry as dr, entity_registry as er
 from homeassistant.setup import async_setup_component
 
-from . import _patch_login_and_data, _patch_login_and_data_offline_device
+from . import (
+    _patch_login_and_data,
+    _patch_login_and_data_offline_device,
+    _patch_login_and_data_unavailable,
+    _patch_login_and_data_unavailable_device,
+)
 
 from tests.common import MockConfigEntry
 
@@ -141,3 +146,159 @@ async def test_sensors(hass: HomeAssistant, patcher, connections) -> None:
     assert (
         hass.states.get("sensor.my_generator_generator_current_average").state == "0.0"
     )
+
+
+@pytest.mark.parametrize(
+    "patcher, connections",
+    [
+        [_patch_login_and_data_unavailable_device, set()],
+        [_patch_login_and_data_unavailable, {("mac", "c9:24:22:6f:14:00")}],
+    ],
+)
+async def test_sensors_unavailable(hass: HomeAssistant, patcher, connections) -> None:
+    """Test that the sensors are unavailable."""
+    config_entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_USERNAME: "any", CONF_PASSWORD: "any"},
+        unique_id="any",
+    )
+    config_entry.add_to_hass(hass)
+    with patcher():
+        await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}})
+        await hass.async_block_till_done()
+    assert config_entry.state == ConfigEntryState.LOADED
+
+    assert len(hass.states.async_all("sensor")) == 25
+    assert (
+        hass.states.get("sensor.my_generator_latest_firmware").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_engine_speed").state == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_engine_oil_pressure").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_engine_coolant_temperature").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_battery_voltage").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_lube_oil_temperature").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_generator_controller_temperature").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_engine_compartment_temperature").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_generator_true_total_power").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get(
+            "sensor.my_generator_generator_true_percent_of_rated_power"
+        ).state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get(
+            "sensor.my_generator_generator_voltage_average_line_to_line"
+        ).state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_generator_frequency").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_generator_state").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get(
+            "sensor.my_generator_generator_controller_total_operation_time"
+        ).state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_engine_total_run_time").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_ats_contactor_position").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert hass.states.get("sensor.my_generator_ip_address").state == STATE_UNAVAILABLE
+
+    assert (
+        hass.states.get("sensor.my_generator_connected_server_ip_address").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_engine_target_speed").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_engine_total_run_time_loaded").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get(
+            "sensor.my_generator_source1_voltage_average_line_to_line"
+        ).state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get(
+            "sensor.my_generator_source2_voltage_average_line_to_line"
+        ).state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_genset_total_energy").state
+        == STATE_UNAVAILABLE
+    )
+    assert (
+        hass.states.get("sensor.my_generator_engine_total_number_of_starts").state
+        == STATE_UNAVAILABLE
+    )
+    assert (
+        hass.states.get("sensor.my_generator_generator_current_average").state
+        == STATE_UNAVAILABLE
+    )
+
+    assert (
+        hass.states.get("sensor.my_generator_battery_voltage").state
+        == STATE_UNAVAILABLE
+    )
-- 
GitLab


From 3c40634fbb3cacaefedd704bc0ee51342ace3ebe Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 23 Oct 2022 18:04:42 -0500
Subject: [PATCH 728/985] Try again to populate HKC BLE accessory state after
 startup (#80854)

---
 .../homekit_controller/connection.py          | 28 +++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py
index 343fdc565f1..4d8d26fb557 100644
--- a/homeassistant/components/homekit_controller/connection.py
+++ b/homeassistant/components/homekit_controller/connection.py
@@ -19,8 +19,8 @@ from aiohomekit.model.characteristics import Characteristic
 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, CoreState, HomeAssistant, callback
+from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STARTED
+from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
 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
@@ -176,6 +176,24 @@ class HKDevice:
         self.available = available
         async_dispatcher_send(self.hass, self.signal_state_updated)
 
+    async def _async_retry_populate_ble_accessory_state(self, event: Event) -> None:
+        """Try again to populate the BLE accessory state.
+
+        If the accessory was asleep at startup we need to retry
+        since we continued on to allow startup to proceed.
+
+        If this fails the state may be inconsistent, but will
+        get corrected as soon as the accessory advertises again.
+        """
+        try:
+            await self.pairing.async_populate_accessories_state(force_update=True)
+        except (asyncio.TimeoutError, AccessoryNotFoundError):
+            _LOGGER.debug(
+                "Failed to populate BLE accessory state for %s, accessory may be sleeping"
+                " and will be retried the next time it advertises",
+                self.config_entry.title,
+            )
+
     async def async_setup(self) -> None:
         """Prepare to use a paired HomeKit device in Home Assistant."""
         pairing = self.pairing
@@ -201,6 +219,12 @@ class HKDevice:
             if transport != Transport.BLE or not pairing.accessories:
                 # BLE devices may sleep and we can't force a connection
                 raise
+            entry.async_on_unload(
+                self.hass.bus.async_listen_once(
+                    EVENT_HOMEASSISTANT_STARTED,
+                    self._async_retry_populate_ble_accessory_state,
+                )
+            )
 
         entry.async_on_unload(pairing.dispatcher_connect(self.process_new_events))
         entry.async_on_unload(
-- 
GitLab


From fdcce0446ca686dc9227d4c385aae581f9913380 Mon Sep 17 00:00:00 2001
From: Kevin Addeman <kevin.addeman@gmail.com>
Date: Sun, 23 Oct 2022 19:17:36 -0400
Subject: [PATCH 729/985] Add Lutron Caseta zeroconf discovery for RA3/HWQSX
 (#80852)

---
 .../components/lutron_caseta/manifest.json    | 15 +++++++++++-
 homeassistant/generated/zeroconf.py           | 23 +++++++++++++++----
 2 files changed, 33 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json
index b4f790292c4..d65ca852da7 100644
--- a/homeassistant/components/lutron_caseta/manifest.json
+++ b/homeassistant/components/lutron_caseta/manifest.json
@@ -4,7 +4,20 @@
   "documentation": "https://www.home-assistant.io/integrations/lutron_caseta",
   "requirements": ["pylutron-caseta==0.17.1"],
   "config_flow": true,
-  "zeroconf": ["_leap._tcp.local."],
+  "zeroconf": [
+    {
+      "type": "_lutron._tcp.local.",
+      "properties": { "SYSTYPE": "radiora3*" }
+    },
+    {
+      "type": "_lutron._tcp.local.",
+      "properties": { "SYSTYPE": "smartbridge*" }
+    },
+    {
+      "type": "_lutron._tcp.local.",
+      "properties": { "SYSTYPE": "ra2select*" }
+    }
+  ],
   "homekit": {
     "models": ["Smart Bridge"]
   },
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index 18ac7112beb..fc0c3ea5fa7 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -242,14 +242,29 @@ ZEROCONF = {
             "name": "gateway*",
         },
     ],
-    "_leap._tcp.local.": [
+    "_lookin._tcp.local.": [
         {
-            "domain": "lutron_caseta",
+            "domain": "lookin",
         },
     ],
-    "_lookin._tcp.local.": [
+    "_lutron._tcp.local.": [
         {
-            "domain": "lookin",
+            "domain": "lutron_caseta",
+            "properties": {
+                "SYSTYPE": "radiora3*",
+            },
+        },
+        {
+            "domain": "lutron_caseta",
+            "properties": {
+                "SYSTYPE": "smartbridge*",
+            },
+        },
+        {
+            "domain": "lutron_caseta",
+            "properties": {
+                "SYSTYPE": "ra2select*",
+            },
         },
     ],
     "_mediaremotetv._tcp.local.": [
-- 
GitLab


From 7d78728a2ffda127624e8373097c9725d5a9ef2b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 23 Oct 2022 18:53:18 -0500
Subject: [PATCH 730/985] Fix whitespace in oncue manifest (#80859)

---
 homeassistant/components/oncue/manifest.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json
index 26a55cd0a96..d9dd247ded9 100644
--- a/homeassistant/components/oncue/manifest.json
+++ b/homeassistant/components/oncue/manifest.json
@@ -10,7 +10,7 @@
   ],
   "documentation": "https://www.home-assistant.io/integrations/oncue",
   "requirements": ["aiooncue==0.3.4"],
-  "codeowners": ["@bdraco","@peterager"],
+  "codeowners": ["@bdraco", "@peterager"],
   "iot_class": "cloud_polling",
   "loggers": ["aiooncue"]
 }
-- 
GitLab


From 073951177b31f90be7232b03df0fd4db77cb3089 Mon Sep 17 00:00:00 2001
From: Garrett <7310260+G-Two@users.noreply.github.com>
Date: Sun, 23 Oct 2022 19:54:22 -0400
Subject: [PATCH 731/985] Code quality update for Subaru sensors (#79482)

* Use distance device class for sensors

* Change sensor name casing and unique_id

* Migrate sensor entity unique_id

* Match title-cased unique_id when migrating

* Remove unneeded regex to find '_' delimited id suffix

* Incorporate PR review comments

* Add check to prevent extra odometer entity migration
---
 homeassistant/components/subaru/sensor.py | 75 ++++++++++++++++----
 tests/components/subaru/conftest.py       | 54 +++++++-------
 tests/components/subaru/test_init.py      | 48 ++++++++-----
 tests/components/subaru/test_sensor.py    | 85 +++++++++++++++++++++++
 4 files changed, 205 insertions(+), 57 deletions(-)

diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py
index bb467fc77de..cae5a7b14a4 100644
--- a/homeassistant/components/subaru/sensor.py
+++ b/homeassistant/components/subaru/sensor.py
@@ -23,7 +23,8 @@ from homeassistant.const import (
     VOLUME_GALLONS,
     VOLUME_LITERS,
 )
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import entity_registry as er
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.update_coordinator import (
     CoordinatorEntity,
@@ -51,6 +52,7 @@ from .const import (
 
 _LOGGER = logging.getLogger(__name__)
 
+
 # Fuel consumption units
 FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS = "L/100km"
 FUEL_CONSUMPTION_MILES_PER_GALLON = "mi/gal"
@@ -62,6 +64,7 @@ KM_PER_MI = DistanceConverter.convert(1, LENGTH_MILES, LENGTH_KILOMETERS)
 SAFETY_SENSORS = [
     SensorEntityDescription(
         key=sc.ODOMETER,
+        device_class=SensorDeviceClass.DISTANCE,
         icon="mdi:road-variant",
         name="Odometer",
         native_unit_of_measurement=LENGTH_KILOMETERS,
@@ -74,12 +77,13 @@ API_GEN_2_SENSORS = [
     SensorEntityDescription(
         key=sc.AVG_FUEL_CONSUMPTION,
         icon="mdi:leaf",
-        name="Avg Fuel Consumption",
+        name="Avg fuel consumption",
         native_unit_of_measurement=FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
         key=sc.DIST_TO_EMPTY,
+        device_class=SensorDeviceClass.DISTANCE,
         icon="mdi:gas-station",
         name="Range",
         native_unit_of_measurement=LENGTH_KILOMETERS,
@@ -88,42 +92,42 @@ API_GEN_2_SENSORS = [
     SensorEntityDescription(
         key=sc.TIRE_PRESSURE_FL,
         device_class=SensorDeviceClass.PRESSURE,
-        name="Tire Pressure FL",
+        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",
+        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",
+        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",
+        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",
+        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",
+        name="12V battery voltage",
         native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
         state_class=SensorStateClass.MEASUREMENT,
     ),
@@ -133,22 +137,23 @@ API_GEN_2_SENSORS = [
 EV_SENSORS = [
     SensorEntityDescription(
         key=sc.EV_DISTANCE_TO_EMPTY,
+        device_class=SensorDeviceClass.DISTANCE,
         icon="mdi:ev-station",
-        name="EV Range",
+        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",
+        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",
+        name="EV time to full charge",
         state_class=SensorStateClass.MEASUREMENT,
     ),
 ]
@@ -164,6 +169,7 @@ async def async_setup_entry(
     coordinator = entry[ENTRY_COORDINATOR]
     vehicle_info = entry[ENTRY_VEHICLES]
     entities = []
+    await _async_migrate_entries(hass, config_entry)
     for info in vehicle_info.values():
         entities.extend(create_vehicle_sensors(info, coordinator))
     async_add_entities(entities)
@@ -211,7 +217,7 @@ class SubaruSensor(
         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}"
+        self._attr_unique_id = f"{self.vin}_{description.key}"
 
     @property
     def native_value(self) -> None | int | float:
@@ -273,3 +279,48 @@ class SubaruSensor(
         if last_update_success and self.vin not in self.coordinator.data:
             return False
         return last_update_success
+
+
+async def _async_migrate_entries(
+    hass: HomeAssistant, config_entry: ConfigEntry
+) -> None:
+    """Migrate sensor entries from HA<=2022.10 to use preferred unique_id."""
+    entity_registry = er.async_get(hass)
+
+    all_sensors = []
+    all_sensors.extend(EV_SENSORS)
+    all_sensors.extend(API_GEN_2_SENSORS)
+    all_sensors.extend(SAFETY_SENSORS)
+
+    # Old unique_id is (previously title-cased) sensor name (e.g. "VIN_Avg Fuel Consumption")
+    replacements = {str(s.name).upper(): s.key for s in all_sensors}
+
+    @callback
+    def update_unique_id(entry: er.RegistryEntry) -> dict[str, Any] | None:
+        id_split = entry.unique_id.split("_")
+        key = id_split[1].upper() if len(id_split) == 2 else None
+
+        if key not in replacements or id_split[1] == replacements[key]:
+            return None
+
+        new_unique_id = entry.unique_id.replace(id_split[1], replacements[key])
+        _LOGGER.debug(
+            "Migrating entity '%s' unique_id from '%s' to '%s'",
+            entry.entity_id,
+            entry.unique_id,
+            new_unique_id,
+        )
+        if existing_entity_id := entity_registry.async_get_entity_id(
+            entry.domain, entry.platform, new_unique_id
+        ):
+            _LOGGER.debug(
+                "Cannot migrate to unique_id '%s', already exists for '%s'",
+                new_unique_id,
+                existing_entity_id,
+            )
+            return None
+        return {
+            "new_unique_id": new_unique_id,
+        }
+
+    await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py
index 492ec06c1e8..20d70a7d496 100644
--- a/tests/components/subaru/conftest.py
+++ b/tests/components/subaru/conftest.py
@@ -5,6 +5,7 @@ from unittest.mock import patch
 import pytest
 from subarulink.const import COUNTRY_USA
 
+from homeassistant import config_entries
 from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN
 from homeassistant.components.subaru.const import (
     CONF_COUNTRY,
@@ -71,6 +72,15 @@ TEST_OPTIONS = {
     CONF_UPDATE_ENABLED: True,
 }
 
+TEST_CONFIG_ENTRY = {
+    "entry_id": "1",
+    "domain": DOMAIN,
+    "title": TEST_CONFIG[CONF_USERNAME],
+    "data": TEST_CONFIG,
+    "options": TEST_OPTIONS,
+    "source": config_entries.SOURCE_USER,
+}
+
 TEST_DEVICE_NAME = "test_vehicle_2"
 TEST_ENTITY_ID = f"sensor.{TEST_DEVICE_NAME}_odometer"
 
@@ -81,26 +91,16 @@ def advance_time_to_next_fetch(hass):
     async_fire_time_changed(hass, future)
 
 
-async def setup_subaru_integration(
+async def setup_subaru_config_entry(
     hass,
-    vehicle_list=None,
-    vehicle_data=None,
-    vehicle_status=None,
+    config_entry,
+    vehicle_list=[TEST_VIN_2_EV],
+    vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
+    vehicle_status=VEHICLE_STATUS_EV,
     connect_effect=None,
     fetch_effect=None,
 ):
-    """Create Subaru entry."""
-    assert await async_setup_component(hass, HA_DOMAIN, {})
-    assert await async_setup_component(hass, DOMAIN, {})
-
-    config_entry = MockConfigEntry(
-        domain=DOMAIN,
-        data=TEST_CONFIG,
-        options=TEST_OPTIONS,
-        entry_id=1,
-    )
-    config_entry.add_to_hass(hass)
-
+    """Run async_setup with API mocks in place."""
     with patch(
         MOCK_API_CONNECT,
         return_value=connect_effect is None,
@@ -134,20 +134,22 @@ async def setup_subaru_integration(
         await hass.config_entries.async_setup(config_entry.entry_id)
         await hass.async_block_till_done()
 
+
+@pytest.fixture
+async def subaru_config_entry(hass):
+    """Create a Subaru config entry prior to setup."""
+    await async_setup_component(hass, HA_DOMAIN, {})
+    config_entry = MockConfigEntry(**TEST_CONFIG_ENTRY)
+    config_entry.add_to_hass(hass)
     return config_entry
 
 
 @pytest.fixture
-async def ev_entry(hass):
+async def ev_entry(hass, subaru_config_entry):
     """Create a Subaru entry representing an EV vehicle with full STARLINK subscription."""
-    entry = await setup_subaru_integration(
-        hass,
-        vehicle_list=[TEST_VIN_2_EV],
-        vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
-        vehicle_status=VEHICLE_STATUS_EV,
-    )
+    await setup_subaru_config_entry(hass, subaru_config_entry)
     assert DOMAIN in hass.config_entries.async_domains()
     assert len(hass.config_entries.async_entries(DOMAIN)) == 1
-    assert hass.config_entries.async_get_entry(entry.entry_id)
-    assert entry.state is ConfigEntryState.LOADED
-    return entry
+    assert hass.config_entries.async_get_entry(subaru_config_entry.entry_id)
+    assert subaru_config_entry.state is ConfigEntryState.LOADED
+    return subaru_config_entry
diff --git a/tests/components/subaru/test_init.py b/tests/components/subaru/test_init.py
index cd87ed40315..46a8b2e103b 100644
--- a/tests/components/subaru/test_init.py
+++ b/tests/components/subaru/test_init.py
@@ -24,7 +24,7 @@ from .conftest import (
     MOCK_API_FETCH,
     MOCK_API_UPDATE,
     TEST_ENTITY_ID,
-    setup_subaru_integration,
+    setup_subaru_config_entry,
 )
 
 
@@ -42,61 +42,70 @@ async def test_setup_ev(hass, ev_entry):
     assert check_entry.state is ConfigEntryState.LOADED
 
 
-async def test_setup_g2(hass):
+async def test_setup_g2(hass, subaru_config_entry):
     """Test setup with a G2 vehcile ."""
-    entry = await setup_subaru_integration(
+    await setup_subaru_config_entry(
         hass,
+        subaru_config_entry,
         vehicle_list=[TEST_VIN_3_G2],
         vehicle_data=VEHICLE_DATA[TEST_VIN_3_G2],
         vehicle_status=VEHICLE_STATUS_G2,
     )
-    check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+    check_entry = hass.config_entries.async_get_entry(subaru_config_entry.entry_id)
     assert check_entry
     assert check_entry.state is ConfigEntryState.LOADED
 
 
-async def test_setup_g1(hass):
+async def test_setup_g1(hass, subaru_config_entry):
     """Test setup with a G1 vehicle."""
-    entry = await setup_subaru_integration(
-        hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1]
+    await setup_subaru_config_entry(
+        hass,
+        subaru_config_entry,
+        vehicle_list=[TEST_VIN_1_G1],
+        vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1],
     )
-    check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+    check_entry = hass.config_entries.async_get_entry(subaru_config_entry.entry_id)
     assert check_entry
     assert check_entry.state is ConfigEntryState.LOADED
 
 
-async def test_unsuccessful_connect(hass):
+async def test_unsuccessful_connect(hass, subaru_config_entry):
     """Test unsuccessful connect due to connectivity."""
-    entry = await setup_subaru_integration(
+    await setup_subaru_config_entry(
         hass,
+        subaru_config_entry,
         connect_effect=SubaruException("Service Unavailable"),
         vehicle_list=[TEST_VIN_2_EV],
         vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
         vehicle_status=VEHICLE_STATUS_EV,
     )
-    check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+    check_entry = hass.config_entries.async_get_entry(subaru_config_entry.entry_id)
     assert check_entry
     assert check_entry.state is ConfigEntryState.SETUP_RETRY
 
 
-async def test_invalid_credentials(hass):
+async def test_invalid_credentials(hass, subaru_config_entry):
     """Test invalid credentials."""
-    entry = await setup_subaru_integration(
+    await setup_subaru_config_entry(
         hass,
+        subaru_config_entry,
         connect_effect=InvalidCredentials("Invalid Credentials"),
         vehicle_list=[TEST_VIN_2_EV],
         vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
         vehicle_status=VEHICLE_STATUS_EV,
     )
-    check_entry = hass.config_entries.async_get_entry(entry.entry_id)
+    check_entry = hass.config_entries.async_get_entry(subaru_config_entry.entry_id)
     assert check_entry
     assert check_entry.state is ConfigEntryState.SETUP_ERROR
 
 
-async def test_update_skip_unsubscribed(hass):
+async def test_update_skip_unsubscribed(hass, subaru_config_entry):
     """Test update function skips vehicles without subscription."""
-    await setup_subaru_integration(
-        hass, vehicle_list=[TEST_VIN_1_G1], vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1]
+    await setup_subaru_config_entry(
+        hass,
+        subaru_config_entry,
+        vehicle_list=[TEST_VIN_1_G1],
+        vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1],
     )
 
     with patch(MOCK_API_FETCH) as mock_fetch:
@@ -126,10 +135,11 @@ async def test_update_disabled(hass, ev_entry):
         mock_update.assert_not_called()
 
 
-async def test_fetch_failed(hass):
+async def test_fetch_failed(hass, subaru_config_entry):
     """Tests when fetch fails."""
-    await setup_subaru_integration(
+    await setup_subaru_config_entry(
         hass,
+        subaru_config_entry,
         vehicle_list=[TEST_VIN_2_EV],
         vehicle_data=VEHICLE_DATA[TEST_VIN_2_EV],
         vehicle_status=VEHICLE_STATUS_EV,
diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py
index aec9e6ede7a..caec43d36e8 100644
--- a/tests/components/subaru/test_sensor.py
+++ b/tests/components/subaru/test_sensor.py
@@ -1,11 +1,16 @@
 """Test Subaru sensors."""
 from unittest.mock import patch
 
+import pytest
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
 from homeassistant.components.subaru.sensor import (
     API_GEN_2_SENSORS,
+    DOMAIN as SUBARU_DOMAIN,
     EV_SENSORS,
     SAFETY_SENSORS,
 )
+from homeassistant.helpers import entity_registry as er
 from homeassistant.util import slugify
 from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
@@ -13,6 +18,7 @@ from .api_responses import (
     EXPECTED_STATE_EV_IMPERIAL,
     EXPECTED_STATE_EV_METRIC,
     EXPECTED_STATE_EV_UNAVAILABLE,
+    TEST_VIN_2_EV,
     VEHICLE_STATUS_EV,
 )
 from .conftest import (
@@ -20,6 +26,7 @@ from .conftest import (
     MOCK_API_GET_DATA,
     TEST_DEVICE_NAME,
     advance_time_to_next_fetch,
+    setup_subaru_config_entry,
 )
 
 
@@ -50,6 +57,84 @@ async def test_sensors_missing_vin_data(hass, ev_entry):
     _assert_data(hass, EXPECTED_STATE_EV_UNAVAILABLE)
 
 
+@pytest.mark.parametrize(
+    "entitydata,old_unique_id,new_unique_id",
+    [
+        (
+            {
+                "domain": SENSOR_DOMAIN,
+                "platform": SUBARU_DOMAIN,
+                "unique_id": f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}",
+            },
+            f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}",
+            f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].key}",
+        ),
+    ],
+)
+async def test_sensor_migrate_unique_ids(
+    hass, entitydata, old_unique_id, new_unique_id, subaru_config_entry
+) -> None:
+    """Test successful migration of entity unique_ids."""
+    entity_registry = er.async_get(hass)
+    entity: er.RegistryEntry = entity_registry.async_get_or_create(
+        **entitydata,
+        config_entry=subaru_config_entry,
+    )
+    assert entity.unique_id == old_unique_id
+
+    await setup_subaru_config_entry(hass, subaru_config_entry)
+
+    entity_migrated = entity_registry.async_get(entity.entity_id)
+    assert entity_migrated
+    assert entity_migrated.unique_id == new_unique_id
+
+
+@pytest.mark.parametrize(
+    "entitydata,old_unique_id,new_unique_id",
+    [
+        (
+            {
+                "domain": SENSOR_DOMAIN,
+                "platform": SUBARU_DOMAIN,
+                "unique_id": f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}",
+            },
+            f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].name}",
+            f"{TEST_VIN_2_EV}_{API_GEN_2_SENSORS[0].key}",
+        )
+    ],
+)
+async def test_sensor_migrate_unique_ids_duplicate(
+    hass, entitydata, old_unique_id, new_unique_id, subaru_config_entry
+) -> None:
+    """Test unsuccessful migration of entity unique_ids due to duplicate."""
+    entity_registry = er.async_get(hass)
+    entity: er.RegistryEntry = entity_registry.async_get_or_create(
+        **entitydata,
+        config_entry=subaru_config_entry,
+    )
+    assert entity.unique_id == old_unique_id
+
+    # create existing entry with new_unique_id that conflicts with migrate
+    existing_entity = entity_registry.async_get_or_create(
+        SENSOR_DOMAIN,
+        SUBARU_DOMAIN,
+        unique_id=new_unique_id,
+        config_entry=subaru_config_entry,
+    )
+
+    await setup_subaru_config_entry(hass, subaru_config_entry)
+
+    entity_migrated = entity_registry.async_get(entity.entity_id)
+    assert entity_migrated
+    assert entity_migrated.unique_id == old_unique_id
+
+    entity_not_changed = entity_registry.async_get(existing_entity.entity_id)
+    assert entity_not_changed
+    assert entity_not_changed.unique_id == new_unique_id
+
+    assert entity_migrated != entity_not_changed
+
+
 def _assert_data(hass, expected_state):
     sensor_list = EV_SENSORS
     sensor_list.extend(API_GEN_2_SENSORS)
-- 
GitLab


From 712b984833f7c3446c7ca1b8c6818063be52005a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= <oyvind@wergeland.org>
Date: Mon, 24 Oct 2022 01:55:22 +0200
Subject: [PATCH 732/985] =?UTF-8?q?Support=20Nob=C3=B8=20Switch=20as=20tem?=
 =?UTF-8?q?perature=20sensor=20(#78480)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Expose Nobø Switch as temperatur sensor.

* - Review
- Hub may report current temperature as None

* Avoid update during entity addition, and fix race condition

* Update pynobo to 1.6.0
Use new method to fix potential race condition.

* Use generator expressions
---
 .coveragerc                                   |  1 +
 homeassistant/components/nobo_hub/__init__.py | 10 +-
 homeassistant/components/nobo_hub/climate.py  |  6 +-
 homeassistant/components/nobo_hub/const.py    |  1 +
 .../components/nobo_hub/manifest.json         |  2 +-
 homeassistant/components/nobo_hub/sensor.py   | 95 +++++++++++++++++++
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 8 files changed, 106 insertions(+), 13 deletions(-)
 create mode 100644 homeassistant/components/nobo_hub/sensor.py

diff --git a/.coveragerc b/.coveragerc
index 62083486c03..08607ace1f7 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -860,6 +860,7 @@ omit =
     homeassistant/components/noaa_tides/sensor.py
     homeassistant/components/nobo_hub/__init__.py
     homeassistant/components/nobo_hub/climate.py
+    homeassistant/components/nobo_hub/sensor.py
     homeassistant/components/norway_air/air_quality.py
     homeassistant/components/notify_events/notify.py
     homeassistant/components/notion/__init__.py
diff --git a/homeassistant/components/nobo_hub/__init__.py b/homeassistant/components/nobo_hub/__init__.py
index 7db9eb96f7e..d828fb78b78 100644
--- a/homeassistant/components/nobo_hub/__init__.py
+++ b/homeassistant/components/nobo_hub/__init__.py
@@ -1,8 +1,6 @@
 """The Nobø Ecohub integration."""
 from __future__ import annotations
 
-import logging
-
 from pynobo import nobo
 
 from homeassistant.config_entries import ConfigEntry
@@ -25,9 +23,7 @@ from .const import (
     NOBO_MANUFACTURER,
 )
 
-PLATFORMS = [Platform.CLIMATE]
-
-_LOGGER = logging.getLogger(__name__)
+PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
 
 
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -37,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     discover = entry.data[CONF_AUTO_DISCOVERED]
     ip_address = None if discover else entry.data[CONF_IP_ADDRESS]
     hub = nobo(serial=serial, ip=ip_address, discover=discover, synchronous=False)
-    await hub.start()
+    await hub.connect()
 
     hass.data.setdefault(DOMAIN, {})
 
@@ -65,6 +61,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
     entry.async_on_unload(entry.add_update_listener(options_update_listener))
 
+    await hub.start()
+
     return True
 
 
diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py
index ba38e0b1530..d138788fba0 100644
--- a/homeassistant/components/nobo_hub/climate.py
+++ b/homeassistant/components/nobo_hub/climate.py
@@ -69,10 +69,7 @@ async def async_setup_entry(
     )
 
     # Add zones as entities
-    async_add_entities(
-        [NoboZone(zone_id, hub, override_type) for zone_id in hub.zones],
-        True,
-    )
+    async_add_entities(NoboZone(zone_id, hub, override_type) for zone_id in hub.zones)
 
 
 class NoboZone(ClimateEntity):
@@ -107,6 +104,7 @@ class NoboZone(ClimateEntity):
             ATTR_VIA_DEVICE: (DOMAIN, hub.hub_info[ATTR_SERIAL]),
             ATTR_SUGGESTED_AREA: hub.zones[zone_id][ATTR_NAME],
         }
+        self._read_state()
 
     async def async_added_to_hass(self) -> None:
         """Register callback from hub."""
diff --git a/homeassistant/components/nobo_hub/const.py b/homeassistant/components/nobo_hub/const.py
index 320c2f43c07..ff0f25cfec3 100644
--- a/homeassistant/components/nobo_hub/const.py
+++ b/homeassistant/components/nobo_hub/const.py
@@ -17,3 +17,4 @@ ATTR_TEMP_ECO_C = "temp_eco_c"
 ATTR_OVERRIDE_ALLOWED = "override_allowed"
 ATTR_TARGET_TYPE = "target_type"
 ATTR_TARGET_ID = "target_id"
+ATTR_ZONE_ID = "zone_id"
diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json
index 14e10a1ffaf..0df92c4c5ae 100644
--- a/homeassistant/components/nobo_hub/manifest.json
+++ b/homeassistant/components/nobo_hub/manifest.json
@@ -3,7 +3,7 @@
   "name": "Nob\u00f8 Ecohub",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/nobo_hub",
-  "requirements": ["pynobo==1.4.0"],
+  "requirements": ["pynobo==1.6.0"],
   "codeowners": ["@echoromeo", "@oyvindwe"],
   "iot_class": "local_push"
 }
diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py
new file mode 100644
index 00000000000..fe33c6ee83e
--- /dev/null
+++ b/homeassistant/components/nobo_hub/sensor.py
@@ -0,0 +1,95 @@
+"""Python Control of Nobø Hub - Nobø Energy Control."""
+from __future__ import annotations
+
+from pynobo import nobo
+
+from homeassistant.components.sensor import (
+    SensorDeviceClass,
+    SensorEntity,
+    SensorStateClass,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+    ATTR_IDENTIFIERS,
+    ATTR_MANUFACTURER,
+    ATTR_MODEL,
+    ATTR_NAME,
+    ATTR_SUGGESTED_AREA,
+    ATTR_VIA_DEVICE,
+    TEMP_CELSIUS,
+)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.typing import StateType
+
+from .const import ATTR_SERIAL, ATTR_ZONE_ID, DOMAIN, NOBO_MANUFACTURER
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up any temperature sensors connected to the Nobø Ecohub."""
+
+    # Setup connection with hub
+    hub: nobo = hass.data[DOMAIN][config_entry.entry_id]
+
+    async_add_entities(
+        NoboTemperatureSensor(component["serial"], hub)
+        for component in hub.components.values()
+        if component[ATTR_MODEL].has_temp_sensor
+    )
+
+
+class NoboTemperatureSensor(SensorEntity):
+    """A Nobø device with a temperature sensor."""
+
+    _attr_device_class = SensorDeviceClass.TEMPERATURE
+    _attr_native_unit_of_measurement = TEMP_CELSIUS
+    _attr_state_class = SensorStateClass.MEASUREMENT
+    _attr_should_poll = False
+
+    def __init__(self, serial: str, hub: nobo) -> None:
+        """Initialize the temperature sensor."""
+        self._temperature: StateType = None
+        self._id = serial
+        self._nobo = hub
+        component = hub.components[self._id]
+        self._attr_unique_id = component[ATTR_SERIAL]
+        self._attr_name = "Temperature"
+        self._attr_has_entity_name = True
+        self._attr_device_info: DeviceInfo = {
+            ATTR_IDENTIFIERS: {(DOMAIN, component[ATTR_SERIAL])},
+            ATTR_NAME: component[ATTR_NAME],
+            ATTR_MANUFACTURER: NOBO_MANUFACTURER,
+            ATTR_MODEL: component[ATTR_MODEL].name,
+            ATTR_VIA_DEVICE: (DOMAIN, hub.hub_info[ATTR_SERIAL]),
+        }
+        zone_id = component[ATTR_ZONE_ID]
+        if zone_id != "-1":
+            self._attr_device_info[ATTR_SUGGESTED_AREA] = hub.zones[zone_id][ATTR_NAME]
+        self._read_state()
+
+    async def async_added_to_hass(self) -> None:
+        """Register callback from hub."""
+        self._nobo.register_callback(self._after_update)
+
+    async def async_will_remove_from_hass(self) -> None:
+        """Deregister callback from hub."""
+        self._nobo.deregister_callback(self._after_update)
+
+    @callback
+    def _read_state(self) -> None:
+        """Read the current state from the hub. This is a local call."""
+        value = self._nobo.get_current_component_temperature(self._id)
+        if value is None:
+            self._attr_native_value = None
+        else:
+            self._attr_native_value = round(float(value), 1)
+
+    @callback
+    def _after_update(self, hub) -> None:
+        self._read_state()
+        self.async_write_ha_state()
diff --git a/requirements_all.txt b/requirements_all.txt
index dd1a8e53270..25bedebc6fb 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1742,7 +1742,7 @@ pynetio==0.1.9.1
 pynina==0.1.8
 
 # homeassistant.components.nobo_hub
-pynobo==1.4.0
+pynobo==1.6.0
 
 # homeassistant.components.nuki
 pynuki==1.5.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 72f67faf438..403cd562bce 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1228,7 +1228,7 @@ pynetgear==0.10.8
 pynina==0.1.8
 
 # homeassistant.components.nobo_hub
-pynobo==1.4.0
+pynobo==1.6.0
 
 # homeassistant.components.nuki
 pynuki==1.5.2
-- 
GitLab


From 6502248b415abae2f2a9f39e65e2a6a3e898ef35 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Mon, 24 Oct 2022 00:38:21 +0000
Subject: [PATCH 733/985] [ci skip] Translation update

---
 homeassistant/components/binary_sensor/translations/hu.json | 2 +-
 homeassistant/components/sensibo/translations/es.json       | 4 ++--
 homeassistant/components/sensibo/translations/fr.json       | 4 ++--
 homeassistant/components/sensibo/translations/hu.json       | 2 +-
 homeassistant/components/sensibo/translations/pt-BR.json    | 4 ++--
 homeassistant/components/xiaomi_miio/translations/fr.json   | 3 ++-
 6 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json
index ad5d5d93254..5d30570a8aa 100644
--- a/homeassistant/components/binary_sensor/translations/hu.json
+++ b/homeassistant/components/binary_sensor/translations/hu.json
@@ -194,7 +194,7 @@
         },
         "presence": {
             "off": "T\u00e1vol",
-            "on": "Jelen"
+            "on": "Otthon"
         },
         "problem": {
             "off": "OK",
diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json
index 8c2c6bea7f3..3bc6910024d 100644
--- a/homeassistant/components/sensibo/translations/es.json
+++ b/homeassistant/components/sensibo/translations/es.json
@@ -17,7 +17,7 @@
                     "api_key": "Clave API"
                 },
                 "data_description": {
-                    "api_key": "Sigue la documentaci\u00f3n para obtener una nueva clave API."
+                    "api_key": "Sigue la documentaci\u00f3n para obtener tu clave API"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "Clave API"
                 },
                 "data_description": {
-                    "api_key": "Sigue la documentaci\u00f3n para obtener tu clave API."
+                    "api_key": "Sigue la documentaci\u00f3n para obtener tu clave API"
                 }
             }
         }
diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json
index 5d53f8daa37..3209ba2f2c8 100644
--- a/homeassistant/components/sensibo/translations/fr.json
+++ b/homeassistant/components/sensibo/translations/fr.json
@@ -17,7 +17,7 @@
                     "api_key": "Cl\u00e9 d'API"
                 },
                 "data_description": {
-                    "api_key": "Consultez la documentation pour obtenir une nouvelle cl\u00e9 d'API."
+                    "api_key": "Consultez la documentation pour obtenir votre cl\u00e9 d'API"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "Cl\u00e9 d'API"
                 },
                 "data_description": {
-                    "api_key": "Consultez la documentation pour obtenir votre cl\u00e9 d'API."
+                    "api_key": "Consultez la documentation pour obtenir votre cl\u00e9 d'API"
                 }
             }
         }
diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json
index 66d277eecf5..575fc6b7128 100644
--- a/homeassistant/components/sensibo/translations/hu.json
+++ b/homeassistant/components/sensibo/translations/hu.json
@@ -17,7 +17,7 @@
                     "api_key": "API kulcs"
                 },
                 "data_description": {
-                    "api_key": "K\u00f6vesse a dokument\u00e1ci\u00f3t az \u00faj API-kulcs beszerz\u00e9s\u00e9hez."
+                    "api_key": "K\u00f6vesse a dokument\u00e1ci\u00f3t az API-kulcs beszerz\u00e9s\u00e9hez."
                 }
             },
             "user": {
diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json
index 591424dcaf6..0429945137c 100644
--- a/homeassistant/components/sensibo/translations/pt-BR.json
+++ b/homeassistant/components/sensibo/translations/pt-BR.json
@@ -17,7 +17,7 @@
                     "api_key": "Chave da API"
                 },
                 "data_description": {
-                    "api_key": "Siga a documenta\u00e7\u00e3o para obter uma nova chave de API."
+                    "api_key": "Siga a documenta\u00e7\u00e3o para obter sua chave de API"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "Chave da API"
                 },
                 "data_description": {
-                    "api_key": "Siga a documenta\u00e7\u00e3o para obter sua chave de API."
+                    "api_key": "Siga a documenta\u00e7\u00e3o para obter sua chave de API"
                 }
             }
         }
diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json
index 91c6b745816..1ed0609da1b 100644
--- a/homeassistant/components/xiaomi_miio/translations/fr.json
+++ b/homeassistant/components/xiaomi_miio/translations/fr.json
@@ -5,7 +5,8 @@
             "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours",
             "incomplete_info": "Informations incompl\u00e8tes pour configurer l'appareil, aucun h\u00f4te ou jeton fourni.",
             "not_xiaomi_miio": "L'appareil n'est pas (encore) pris en charge par Xiaomi Miio.",
-            "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
+            "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi",
+            "unknown": "Erreur inattendue"
         },
         "error": {
             "cannot_connect": "\u00c9chec de connexion",
-- 
GitLab


From 82eb17e12a4643930963120caec32ea8f98f978d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Sun, 23 Oct 2022 20:29:20 -0500
Subject: [PATCH 734/985] Bump aiohomekit to 2.2.2 (#80857)

* Bump aiohomekit to 2.2.2

changelog: https://github.com/Jc2k/aiohomekit/compare/2.2.1...2.2.2

* ci bump
---
 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 e6182ce1f68..f3ca06c851f 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.1"],
+  "requirements": ["aiohomekit==2.2.2"],
   "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 25bedebc6fb..3f49f19937b 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.1
+aiohomekit==2.2.2
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 403cd562bce..498627170ad 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.1
+aiohomekit==2.2.2
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From 3c170f7627a6326be773770778c40bcf68a9e4b0 Mon Sep 17 00:00:00 2001
From: Oliver <10700296+ol-iver@users.noreply.github.com>
Date: Mon, 24 Oct 2022 04:15:04 +0200
Subject: [PATCH 735/985] Update denonavr to version 0.10.12 (#80861)

---
 homeassistant/components/denonavr/manifest.json | 2 +-
 requirements_all.txt                            | 2 +-
 requirements_test_all.txt                       | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json
index bb6e59053fb..f7212801174 100644
--- a/homeassistant/components/denonavr/manifest.json
+++ b/homeassistant/components/denonavr/manifest.json
@@ -3,7 +3,7 @@
   "name": "Denon AVR Network Receivers",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/denonavr",
-  "requirements": ["denonavr==0.10.11"],
+  "requirements": ["denonavr==0.10.12"],
   "codeowners": ["@ol-iver", "@starkillerOG"],
   "ssdp": [
     {
diff --git a/requirements_all.txt b/requirements_all.txt
index 3f49f19937b..c92822fea11 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -563,7 +563,7 @@ deluge-client==1.7.1
 demetriek==0.4.0
 
 # homeassistant.components.denonavr
-denonavr==0.10.11
+denonavr==0.10.12
 
 # homeassistant.components.devolo_home_control
 devolo-home-control-api==0.18.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 498627170ad..d32a5d687d8 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -437,7 +437,7 @@ deluge-client==1.7.1
 demetriek==0.4.0
 
 # homeassistant.components.denonavr
-denonavr==0.10.11
+denonavr==0.10.12
 
 # homeassistant.components.devolo_home_control
 devolo-home-control-api==0.18.2
-- 
GitLab


From 6e453ae471b9aa6eaf6f29634292bd6cf7eb79e1 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
Date: Mon, 24 Oct 2022 14:49:18 +1100
Subject: [PATCH 736/985] Add device-specific diagnostics to the LIFX
 integration (#79964)

---
 homeassistant/components/lifx/const.py       |   1 +
 homeassistant/components/lifx/coordinator.py |  38 ++
 homeassistant/components/lifx/diagnostics.py |  28 ++
 tests/components/lifx/__init__.py            |   3 +
 tests/components/lifx/test_diagnostics.py    | 385 +++++++++++++++++++
 5 files changed, 455 insertions(+)
 create mode 100644 homeassistant/components/lifx/diagnostics.py
 create mode 100644 tests/components/lifx/test_diagnostics.py

diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py
index a81cd7d59be..1502c51204b 100644
--- a/homeassistant/components/lifx/const.py
+++ b/homeassistant/components/lifx/const.py
@@ -12,6 +12,7 @@ MESSAGE_RETRIES = 5
 OVERALL_TIMEOUT = 9
 UNAVAILABLE_GRACE = 90
 
+CONF_LABEL = "label"
 CONF_SERIAL = "serial"
 
 IDENTIFY_WAVEFORM = {
diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py
index b89fefb35fd..2ec3a6d3745 100644
--- a/homeassistant/components/lifx/coordinator.py
+++ b/homeassistant/components/lifx/coordinator.py
@@ -117,6 +117,44 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
         """Return the current infrared brightness as a string."""
         return infrared_brightness_value_to_option(self.device.infrared_brightness)
 
+    async def diagnostics(self) -> dict[str, Any]:
+        """Return diagnostic information about the device."""
+        features = lifx_features(self.device)
+        device_data = {
+            "firmware": self.device.host_firmware_version,
+            "vendor": self.device.vendor,
+            "product_id": self.device.product,
+            "features": features,
+            "hue": self.device.color[0],
+            "saturation": self.device.color[1],
+            "brightness": self.device.color[2],
+            "kelvin": self.device.color[3],
+            "power": self.device.power_level,
+        }
+
+        if features["multizone"] is True:
+            zones = {"count": self.device.zones_count, "state": {}}
+            for index, zone_color in enumerate(self.device.color_zones):
+                zones["state"][index] = {
+                    "hue": zone_color[0],
+                    "saturation": zone_color[1],
+                    "brightness": zone_color[2],
+                    "kelvin": zone_color[3],
+                }
+            device_data["zones"] = zones
+
+        if features["hev"] is True:
+            device_data["hev"] = {
+                "hev_cycle": self.device.hev_cycle,
+                "hev_config": self.device.hev_cycle_configuration,
+                "last_result": self.device.last_hev_cycle_result,
+            }
+
+        if features["infrared"] is True:
+            device_data["infrared"] = {"brightness": self.device.infrared_brightness}
+
+        return device_data
+
     def async_get_entity_id(self, platform: Platform, key: str) -> str | None:
         """Return the entity_id from the platform and key provided."""
         ent_reg = er.async_get(self.hass)
diff --git a/homeassistant/components/lifx/diagnostics.py b/homeassistant/components/lifx/diagnostics.py
new file mode 100644
index 00000000000..abe13cd1a50
--- /dev/null
+++ b/homeassistant/components/lifx/diagnostics.py
@@ -0,0 +1,28 @@
+"""Diagnostics support for LIFX."""
+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_HOST, CONF_IP_ADDRESS, CONF_MAC
+from homeassistant.core import HomeAssistant
+
+from .const import CONF_LABEL, DOMAIN
+from .coordinator import LIFXUpdateCoordinator
+
+TO_REDACT = [CONF_LABEL, CONF_HOST, CONF_IP_ADDRESS, CONF_MAC]
+
+
+async def async_get_config_entry_diagnostics(
+    hass: HomeAssistant, entry: ConfigEntry
+) -> dict[str, Any]:
+    """Return diagnostics for a LIFX config entry."""
+    coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+    return {
+        "entry": {
+            "title": entry.title,
+            "data": async_redact_data(dict(entry.data), TO_REDACT),
+        },
+        "data": async_redact_data(await coordinator.diagnostics(), TO_REDACT),
+    }
diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py
index df3c41ccaca..543f2f7d7a2 100644
--- a/tests/components/lifx/__init__.py
+++ b/tests/components/lifx/__init__.py
@@ -123,12 +123,15 @@ def _mocked_clean_bulb() -> Light:
     bulb = _mocked_bulb()
     bulb.get_hev_cycle = MockLifxCommand(bulb)
     bulb.set_hev_cycle = MockLifxCommand(bulb)
+    bulb.get_hev_configuration = MockLifxCommand(bulb)
+    bulb.get_last_hev_cycle_result = MockLifxCommand(bulb)
     bulb.hev_cycle_configuration = {"duration": 7200, "indication": False}
     bulb.hev_cycle = {
         "duration": 7200,
         "remaining": 30,
         "last_power": False,
     }
+    bulb.last_hev_cycle_result = 0
     bulb.product = 90
     return bulb
 
diff --git a/tests/components/lifx/test_diagnostics.py b/tests/components/lifx/test_diagnostics.py
new file mode 100644
index 00000000000..4eccd19634d
--- /dev/null
+++ b/tests/components/lifx/test_diagnostics.py
@@ -0,0 +1,385 @@
+"""Test LIFX diagnostics."""
+from homeassistant.components import lifx
+from homeassistant.const import CONF_HOST
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from . import (
+    DEFAULT_ENTRY_TITLE,
+    IP_ADDRESS,
+    MAC_ADDRESS,
+    _mocked_bulb,
+    _mocked_clean_bulb,
+    _mocked_infrared_bulb,
+    _mocked_light_strip,
+    _patch_config_flow_try_connect,
+    _patch_device,
+    _patch_discovery,
+)
+
+from tests.common import MockConfigEntry
+from tests.components.diagnostics import get_diagnostics_for_config_entry
+
+
+async def test_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None:
+    """Test diagnostics for a standard bulb."""
+    config_entry = MockConfigEntry(
+        domain=lifx.DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_bulb()
+    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()
+
+    diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
+    assert diag == {
+        "data": {
+            "brightness": 3,
+            "features": {
+                "buttons": False,
+                "chain": False,
+                "color": True,
+                "extended_multizone": False,
+                "hev": False,
+                "infrared": False,
+                "matrix": False,
+                "max_kelvin": 9000,
+                "min_kelvin": 2500,
+                "multizone": False,
+                "relays": False,
+            },
+            "firmware": "3.00",
+            "hue": 1,
+            "kelvin": 4,
+            "power": 0,
+            "product_id": 1,
+            "saturation": 2,
+            "vendor": None,
+        },
+        "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"},
+    }
+
+
+async def test_clean_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None:
+    """Test diagnostics for a standard bulb."""
+    config_entry = MockConfigEntry(
+        domain=lifx.DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_clean_bulb()
+    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()
+
+    diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
+    assert diag == {
+        "data": {
+            "brightness": 3,
+            "features": {
+                "buttons": False,
+                "chain": False,
+                "color": True,
+                "extended_multizone": False,
+                "hev": True,
+                "infrared": False,
+                "matrix": False,
+                "max_kelvin": 9000,
+                "min_kelvin": 1500,
+                "multizone": False,
+                "relays": False,
+            },
+            "firmware": "3.00",
+            "hev": {
+                "hev_config": {"duration": 7200, "indication": False},
+                "hev_cycle": {"duration": 7200, "last_power": False, "remaining": 30},
+                "last_result": 0,
+            },
+            "hue": 1,
+            "kelvin": 4,
+            "power": 0,
+            "product_id": 90,
+            "saturation": 2,
+            "vendor": None,
+        },
+        "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"},
+    }
+
+
+async def test_infrared_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None:
+    """Test diagnostics for a standard bulb."""
+    config_entry = MockConfigEntry(
+        domain=lifx.DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_infrared_bulb()
+    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()
+
+    diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
+    assert diag == {
+        "data": {
+            "brightness": 3,
+            "features": {
+                "buttons": False,
+                "chain": False,
+                "color": True,
+                "extended_multizone": False,
+                "hev": False,
+                "infrared": True,
+                "matrix": False,
+                "max_kelvin": 9000,
+                "min_kelvin": 1500,
+                "multizone": False,
+                "relays": False,
+            },
+            "firmware": "3.00",
+            "hue": 1,
+            "infrared": {"brightness": 65535},
+            "kelvin": 4,
+            "power": 0,
+            "product_id": 29,
+            "saturation": 2,
+            "vendor": None,
+        },
+        "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"},
+    }
+
+
+async def test_legacy_multizone_bulb_diagnostics(
+    hass: HomeAssistant, hass_client
+) -> None:
+    """Test diagnostics for a standard bulb."""
+    config_entry = MockConfigEntry(
+        domain=lifx.DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_light_strip()
+    bulb.zones_count = 8
+    bulb.color_zones = [
+        (54612, 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),
+    ]
+    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()
+
+    diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
+    assert diag == {
+        "data": {
+            "brightness": 3,
+            "features": {
+                "buttons": False,
+                "chain": False,
+                "color": True,
+                "extended_multizone": False,
+                "hev": False,
+                "infrared": False,
+                "matrix": False,
+                "max_kelvin": 9000,
+                "min_kelvin": 2500,
+                "multizone": True,
+                "relays": False,
+            },
+            "firmware": "3.00",
+            "hue": 1,
+            "kelvin": 4,
+            "power": 0,
+            "product_id": 31,
+            "saturation": 2,
+            "vendor": None,
+            "zones": {
+                "count": 8,
+                "state": {
+                    "0": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "1": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "2": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "3": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "4": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "5": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "6": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "7": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                },
+            },
+        },
+        "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"},
+    }
+
+
+async def test_multizone_bulb_diagnostics(hass: HomeAssistant, hass_client) -> None:
+    """Test diagnostics for a standard bulb."""
+    config_entry = MockConfigEntry(
+        domain=lifx.DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_light_strip()
+    bulb.product = 38
+    bulb.zones_count = 8
+    bulb.color_zones = [
+        (54612, 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),
+    ]
+    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()
+
+    diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
+    assert diag == {
+        "data": {
+            "brightness": 3,
+            "features": {
+                "buttons": False,
+                "chain": False,
+                "color": True,
+                "extended_multizone": True,
+                "hev": False,
+                "infrared": False,
+                "matrix": False,
+                "max_kelvin": 9000,
+                "min_ext_mz_firmware": 1532997580,
+                "min_ext_mz_firmware_components": [2, 77],
+                "min_kelvin": 1500,
+                "multizone": True,
+                "relays": False,
+            },
+            "firmware": "3.00",
+            "hue": 1,
+            "kelvin": 4,
+            "power": 0,
+            "product_id": 38,
+            "saturation": 2,
+            "vendor": None,
+            "zones": {
+                "count": 8,
+                "state": {
+                    "0": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "1": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "2": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "3": {
+                        "brightness": 65535,
+                        "hue": 54612,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "4": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "5": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "6": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                    "7": {
+                        "brightness": 65535,
+                        "hue": 46420,
+                        "kelvin": 3500,
+                        "saturation": 65535,
+                    },
+                },
+            },
+        },
+        "entry": {"data": {"host": "**REDACTED**"}, "title": "My Bulb"},
+    }
-- 
GitLab


From 91e6ee5da53375e29fda59c947731be29fa82a01 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Sun, 23 Oct 2022 23:20:55 -0700
Subject: [PATCH 737/985] Bump gcal_sync to 2.2.0 (#80863)

---
 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 1f1a6c32481..ce95e3112ee 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==1.1.0", "oauth2client==4.1.3"],
+  "requirements": ["gcal-sync==2.2.0", "oauth2client==4.1.3"],
   "codeowners": ["@allenporter"],
   "iot_class": "cloud_polling",
   "loggers": ["googleapiclient"]
diff --git a/requirements_all.txt b/requirements_all.txt
index c92822fea11..b0470e124f9 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==1.1.0
+gcal-sync==2.2.0
 
 # homeassistant.components.geniushub
 geniushub-client==0.6.30
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index d32a5d687d8..fac773bf328 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==1.1.0
+gcal-sync==2.2.0
 
 # homeassistant.components.geocaching
 geocachingapi==0.2.1
-- 
GitLab


From b1202ab31f714e64e052f4ef419847c6a49a253b Mon Sep 17 00:00:00 2001
From: Quentame <polletquentin74@me.com>
Date: Mon, 24 Oct 2022 09:33:59 +0200
Subject: [PATCH 738/985] Add Freebox button to mark calls as read (#80609)

* Add Freebox button to mark calls as read

* Bump Freebox to 1.0.1

* Fix black/flake8

* Add entity_category + fix reboot button name

* Add has_entity_name to reboot

* Remove 'missed' as it put all calls as read

* base unique_id on key and not name

* unique_id base on key later with migration step

* Keep the same name

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
---
 homeassistant/components/freebox/button.py     | 9 ++++++++-
 homeassistant/components/freebox/manifest.json | 2 +-
 homeassistant/components/freebox/router.py     | 6 ++++++
 requirements_all.txt                           | 2 +-
 requirements_test_all.txt                      | 2 +-
 5 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/freebox/button.py b/homeassistant/components/freebox/button.py
index b3313bba9dd..69fa10ff268 100644
--- a/homeassistant/components/freebox/button.py
+++ b/homeassistant/components/freebox/button.py
@@ -11,7 +11,7 @@ from homeassistant.components.button import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity import DeviceInfo, EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import DOMAIN
@@ -37,8 +37,15 @@ BUTTON_DESCRIPTIONS: tuple[FreeboxButtonEntityDescription, ...] = (
         key="reboot",
         name="Reboot Freebox",
         device_class=ButtonDeviceClass.RESTART,
+        entity_category=EntityCategory.CONFIG,
         async_press=lambda router: router.reboot(),
     ),
+    FreeboxButtonEntityDescription(
+        key="mark_calls_as_read",
+        name="Mark calls as read",
+        entity_category=EntityCategory.DIAGNOSTIC,
+        async_press=lambda router: router.call.mark_calls_log_as_read(),
+    ),
 )
 
 
diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json
index a6d21bb635f..44d9b47557c 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==1.0.0"],
+  "requirements": ["freebox-api==1.0.1"],
   "zeroconf": ["_fbx-api._tcp.local."],
   "codeowners": ["@hacf-fr", "@Quentame"],
   "iot_class": "local_polling",
diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py
index ce4d03aae7a..0fb0f10a27d 100644
--- a/homeassistant/components/freebox/router.py
+++ b/homeassistant/components/freebox/router.py
@@ -9,6 +9,7 @@ from pathlib import Path
 from typing import Any
 
 from freebox_api import Freepybox
+from freebox_api.api.call import Call
 from freebox_api.api.wifi import Wifi
 from freebox_api.exceptions import NotOpenError
 
@@ -186,6 +187,11 @@ class FreeboxRouter:
         """Return sensors."""
         return {**self.sensors_temperature, **self.sensors_connection}
 
+    @property
+    def call(self) -> Call:
+        """Return the call."""
+        return self._api.call
+
     @property
     def wifi(self) -> Wifi:
         """Return the wifi."""
diff --git a/requirements_all.txt b/requirements_all.txt
index b0470e124f9..ba49cdaa59e 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -709,7 +709,7 @@ forecast_solar==2.2.0
 fortiosapi==1.0.5
 
 # homeassistant.components.freebox
-freebox-api==1.0.0
+freebox-api==1.0.1
 
 # homeassistant.components.free_mobile
 freesms==0.2.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index fac773bf328..169574d8d0e 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -528,7 +528,7 @@ foobot_async==1.0.0
 forecast_solar==2.2.0
 
 # homeassistant.components.freebox
-freebox-api==1.0.0
+freebox-api==1.0.1
 
 # homeassistant.components.fritz
 # homeassistant.components.fritzbox_callmonitor
-- 
GitLab


From a8bf8d449b4edf3ed0ce5ebdeb60138a1c74ea43 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 24 Oct 2022 09:50:56 +0200
Subject: [PATCH 739/985] Bump actions/upload-artifact from 3.1.0 to 3.1.1
 (#80867)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3.1.0...v3.1.1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/ci.yaml    | 4 ++--
 .github/workflows/wheels.yml | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 36003cac2f3..1651795c633 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -853,7 +853,7 @@ jobs:
             -p no:sugar \
             tests/components/${{ matrix.group }}
       - name: Upload coverage artifact
-        uses: actions/upload-artifact@v3.1.0
+        uses: actions/upload-artifact@v3.1.1
         with:
           name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
           path: coverage.xml
@@ -956,7 +956,7 @@ jobs:
             --dburl=mysql://root:password@127.0.0.1/homeassistant-test \
             tests/components/recorder
       - name: Upload coverage artifact
-        uses: actions/upload-artifact@v3.1.0
+        uses: actions/upload-artifact@v3.1.1
         with:
           name: coverage-${{ matrix.python-version }}-mariadb
           path: coverage.xml
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index aa578c8b115..672cf4fe4cb 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -57,13 +57,13 @@ jobs:
           ) > .env_file
 
       - name: Upload env_file
-        uses: actions/upload-artifact@v3.1.0
+        uses: actions/upload-artifact@v3.1.1
         with:
           name: env_file
           path: ./.env_file
 
       - name: Upload requirements_diff
-        uses: actions/upload-artifact@v3.1.0
+        uses: actions/upload-artifact@v3.1.1
         with:
           name: requirements_diff
           path: ./requirements_diff.txt
-- 
GitLab


From 5e7f571f019c0b992b9cb8ffa545c12e8169d395 Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Mon, 24 Oct 2022 09:58:23 +0200
Subject: [PATCH 740/985] Move advanced MQTT options to entry (#79351)

* Move advanced broker settings to entry

* Add repair issue for deprecated settings

* Split CONFIG_SCHEMA

* Do not store certificate UI flags in entry

* Keep entered password in next dialog

* Do not process yaml config in flow

* Correct typo
---
 homeassistant/components/mqtt/__init__.py     |  66 ++-
 homeassistant/components/mqtt/client.py       |  10 +-
 homeassistant/components/mqtt/config_flow.py  | 355 +++++++++--
 .../components/mqtt/config_integration.py     |  60 +-
 homeassistant/components/mqtt/const.py        |  13 +-
 homeassistant/components/mqtt/manifest.json   |   2 +-
 homeassistant/components/mqtt/strings.json    |  50 +-
 .../components/mqtt/translations/en.json      |  50 +-
 homeassistant/components/mqtt/util.py         |  64 ++
 tests/components/mqtt/test_config_flow.py     | 550 ++++++++++++++++--
 tests/components/mqtt/test_init.py            |   6 +-
 tests/components/mqtt/test_util.py            |  49 ++
 12 files changed, 1159 insertions(+), 116 deletions(-)
 create mode 100644 tests/components/mqtt/test_util.py

diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 5d26c78da99..0b46ef0a32b 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -14,10 +14,12 @@ from homeassistant import config as conf_util, config_entries
 from homeassistant.components import websocket_api
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
+    CONF_CLIENT_ID,
     CONF_DISCOVERY,
     CONF_PASSWORD,
     CONF_PAYLOAD,
     CONF_PORT,
+    CONF_PROTOCOL,
     CONF_USERNAME,
     SERVICE_RELOAD,
 )
@@ -32,6 +34,7 @@ from homeassistant.helpers import (
 from homeassistant.helpers.device_registry import DeviceEntry
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity_platform import async_get_platforms
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.helpers.reload import (
     async_integration_yaml_config,
     async_reload_integration_platforms,
@@ -50,7 +53,9 @@ from .client import (  # noqa: F401
 )
 from .config_integration import (
     CONFIG_SCHEMA_BASE,
+    CONFIG_SCHEMA_ENTRY,
     DEFAULT_VALUES,
+    DEPRECATED_CERTIFICATE_CONFIG_KEYS,
     DEPRECATED_CONFIG_KEYS,
 )
 from .const import (  # noqa: F401
@@ -60,10 +65,15 @@ from .const import (  # noqa: F401
     ATTR_TOPIC,
     CONF_BIRTH_MESSAGE,
     CONF_BROKER,
+    CONF_CERTIFICATE,
+    CONF_CLIENT_CERT,
+    CONF_CLIENT_KEY,
     CONF_COMMAND_TOPIC,
     CONF_DISCOVERY_PREFIX,
+    CONF_KEEPALIVE,
     CONF_QOS,
     CONF_STATE_TOPIC,
+    CONF_TLS_INSECURE,
     CONF_TLS_VERSION,
     CONF_TOPIC,
     CONF_WILL_MESSAGE,
@@ -86,7 +96,9 @@ from .models import (  # noqa: F401
 )
 from .util import (
     _VALID_QOS_SCHEMA,
+    async_create_certificate_temp_files,
     get_mqtt_data,
+    migrate_certificate_file_to_content,
     mqtt_config_entry_enabled,
     valid_publish_topic,
     valid_subscribe_topic,
@@ -97,7 +109,7 @@ _LOGGER = logging.getLogger(__name__)
 SERVICE_PUBLISH = "publish"
 SERVICE_DUMP = "dump"
 
-MANDATORY_DEFAULT_VALUES = (CONF_PORT,)
+MANDATORY_DEFAULT_VALUES = (CONF_PORT, CONF_DISCOVERY_PREFIX)
 
 ATTR_TOPIC_TEMPLATE = "topic_template"
 ATTR_PAYLOAD_TEMPLATE = "payload_template"
@@ -111,9 +123,17 @@ CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable"
 CONFIG_ENTRY_CONFIG_KEYS = [
     CONF_BIRTH_MESSAGE,
     CONF_BROKER,
+    CONF_CERTIFICATE,
+    CONF_CLIENT_ID,
+    CONF_CLIENT_CERT,
+    CONF_CLIENT_KEY,
     CONF_DISCOVERY,
+    CONF_DISCOVERY_PREFIX,
+    CONF_KEEPALIVE,
     CONF_PASSWORD,
     CONF_PORT,
+    CONF_PROTOCOL,
+    CONF_TLS_INSECURE,
     CONF_USERNAME,
     CONF_WILL_MESSAGE,
 ]
@@ -123,9 +143,17 @@ CONFIG_SCHEMA = vol.Schema(
         DOMAIN: vol.All(
             cv.deprecated(CONF_BIRTH_MESSAGE),  # Deprecated in HA Core 2022.3
             cv.deprecated(CONF_BROKER),  # Deprecated in HA Core 2022.3
+            cv.deprecated(CONF_CERTIFICATE),  # Deprecated in HA Core 2022.11
+            cv.deprecated(CONF_CLIENT_ID),  # Deprecated in HA Core 2022.11
+            cv.deprecated(CONF_CLIENT_CERT),  # Deprecated in HA Core 2022.11
+            cv.deprecated(CONF_CLIENT_KEY),  # Deprecated in HA Core 2022.11
             cv.deprecated(CONF_DISCOVERY),  # Deprecated in HA Core 2022.3
+            cv.deprecated(CONF_DISCOVERY_PREFIX),  # Deprecated in HA Core 2022.11
+            cv.deprecated(CONF_KEEPALIVE),  # Deprecated in HA Core 2022.11
             cv.deprecated(CONF_PASSWORD),  # Deprecated in HA Core 2022.3
             cv.deprecated(CONF_PORT),  # Deprecated in HA Core 2022.3
+            cv.deprecated(CONF_PROTOCOL),  # Deprecated in HA Core 2022.11
+            cv.deprecated(CONF_TLS_INSECURE),  # Deprecated in HA Core 2022.11
             cv.deprecated(CONF_TLS_VERSION),  # Deprecated June 2020
             cv.deprecated(CONF_USERNAME),  # Deprecated in HA Core 2022.3
             cv.deprecated(CONF_WILL_MESSAGE),  # Deprecated in HA Core 2022.3
@@ -207,22 +235,31 @@ def _filter_entry_config(hass: HomeAssistant, entry: ConfigEntry) -> None:
     if entry.data.keys() != filtered_data.keys():
         _LOGGER.warning(
             "The following unsupported configuration options were removed from the "
-            "MQTT config entry: %s. Add them to configuration.yaml if they are needed",
+            "MQTT config entry: %s",
             entry.data.keys() - filtered_data.keys(),
         )
         hass.config_entries.async_update_entry(entry, data=filtered_data)
 
 
-def _merge_basic_config(
+async def _async_merge_basic_config(
     hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any]
 ) -> None:
     """Merge basic options in configuration.yaml config with config entry.
 
     This mends incomplete migration from old version of HA Core.
     """
-
     entry_updated = False
     entry_config = {**entry.data}
+    for key in DEPRECATED_CERTIFICATE_CONFIG_KEYS:
+        if key in yaml_config and key not in entry_config:
+            if (
+                content := await hass.async_add_executor_job(
+                    migrate_certificate_file_to_content, yaml_config[key]
+                )
+            ) is not None:
+                entry_config[key] = content
+                entry_updated = True
+
     for key in DEPRECATED_CONFIG_KEYS:
         if key in yaml_config and key not in entry_config:
             entry_config[key] = yaml_config[key]
@@ -265,7 +302,7 @@ async def async_fetch_config(
     _filter_entry_config(hass, entry)
 
     # Merge basic configuration, and add missing defaults for basic options
-    _merge_basic_config(hass, entry, mqtt_data.config or {})
+    await _async_merge_basic_config(hass, entry, mqtt_data.config or {})
     # Bail out if broker setting is missing
     if CONF_BROKER not in entry.data:
         _LOGGER.error("MQTT broker is not configured, please configure it")
@@ -274,7 +311,7 @@ async def async_fetch_config(
     # If user doesn't have configuration.yaml config, generate default values
     # for options not in config entry data
     if (conf := mqtt_data.config) is None:
-        conf = CONFIG_SCHEMA_BASE(dict(entry.data))
+        conf = CONFIG_SCHEMA_ENTRY(dict(entry.data))
 
     # User has configuration.yaml config, warn about config entry overrides
     elif any(key in conf for key in entry.data):
@@ -282,12 +319,28 @@ async def async_fetch_config(
         override = {k: entry.data[k] for k in shared_keys if conf[k] != entry.data[k]}
         if CONF_PASSWORD in override:
             override[CONF_PASSWORD] = "********"
+        if CONF_CLIENT_KEY in override:
+            override[CONF_CLIENT_KEY] = "-----PRIVATE KEY-----"
         if override:
             _LOGGER.warning(
                 "Deprecated configuration settings found in configuration.yaml. "
                 "These settings from your configuration entry will override: %s",
                 override,
             )
+        # Register a repair issue
+        async_create_issue(
+            hass,
+            DOMAIN,
+            "deprecated_yaml_broker_settings",
+            breaks_in_ha_version="2023.4.0",  # Warning first added in 2022.11.0
+            is_fixable=False,
+            severity=IssueSeverity.WARNING,
+            translation_key="deprecated_yaml_broker_settings",
+            translation_placeholders={
+                "more_info_url": "https://www.home-assistant.io/integrations/mqtt/",
+                "deprecated_settings": str(shared_keys)[1:-1],
+            },
+        )
 
     # Merge advanced configuration values from configuration.yaml
     conf = _merge_extended_config(entry, conf)
@@ -302,6 +355,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     if (conf := await async_fetch_config(hass, entry)) is None:
         # Bail out
         return False
+    await async_create_certificate_temp_files(hass, dict(entry.data))
     mqtt_data.client = MQTT(hass, entry, conf)
     # Restore saved subscriptions
     if mqtt_data.subscriptions_to_restore:
diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py
index bf3f24c950e..041e3cfa374 100644
--- a/homeassistant/components/mqtt/client.py
+++ b/homeassistant/components/mqtt/client.py
@@ -68,7 +68,7 @@ from .models import (
     ReceiveMessage,
     ReceivePayloadType,
 )
-from .util import get_mqtt_data, mqtt_config_entry_enabled
+from .util import get_file_path, get_mqtt_data, mqtt_config_entry_enabled
 
 if TYPE_CHECKING:
     # Only import for paho-mqtt type checking here, imports are done locally
@@ -292,11 +292,13 @@ class MqttClientSetup:
         if username is not None:
             self._client.username_pw_set(username, password)
 
-        if (certificate := config.get(CONF_CERTIFICATE)) == "auto":
+        if (
+            certificate := get_file_path(CONF_CERTIFICATE, config.get(CONF_CERTIFICATE))
+        ) == "auto":
             certificate = certifi.where()
 
-        client_key = config.get(CONF_CLIENT_KEY)
-        client_cert = config.get(CONF_CLIENT_CERT)
+        client_key = get_file_path(CONF_CLIENT_KEY, config.get(CONF_CLIENT_KEY))
+        client_cert = get_file_path(CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT))
         tls_insecure = config.get(CONF_TLS_INSECURE)
         if certificate is not None:
             self._client.tls_set(
diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py
index fa3f7d14c31..d94a2648918 100644
--- a/homeassistant/components/mqtt/config_flow.py
+++ b/homeassistant/components/mqtt/config_flow.py
@@ -4,35 +4,47 @@ from __future__ import annotations
 from collections import OrderedDict
 from collections.abc import Callable
 import queue
+from ssl import PROTOCOL_TLS, SSLContext, SSLError
 from types import MappingProxyType
 from typing import Any
 
+from cryptography.hazmat.primitives.serialization import load_pem_private_key
+from cryptography.x509 import load_pem_x509_certificate
 import voluptuous as vol
 
 from homeassistant import config_entries
+from homeassistant.components.file_upload import process_uploaded_file
 from homeassistant.components.hassio import HassioServiceInfo
 from homeassistant.const import (
+    CONF_CLIENT_ID,
     CONF_DISCOVERY,
     CONF_HOST,
     CONF_PASSWORD,
     CONF_PAYLOAD,
     CONF_PORT,
+    CONF_PROTOCOL,
     CONF_USERNAME,
 )
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers.selector import (
     BooleanSelector,
+    FileSelector,
+    FileSelectorConfig,
     NumberSelector,
     NumberSelectorConfig,
     NumberSelectorMode,
+    SelectOptionDict,
+    SelectSelector,
+    SelectSelectorConfig,
+    SelectSelectorMode,
     TextSelector,
     TextSelectorConfig,
     TextSelectorType,
 )
-from homeassistant.helpers.typing import ConfigType
 
 from .client import MqttClientSetup
+from .config_integration import CONFIG_SCHEMA_ENTRY
 from .const import (
     ATTR_PAYLOAD,
     ATTR_QOS,
@@ -40,17 +52,37 @@ from .const import (
     ATTR_TOPIC,
     CONF_BIRTH_MESSAGE,
     CONF_BROKER,
+    CONF_CERTIFICATE,
+    CONF_CLIENT_CERT,
+    CONF_CLIENT_KEY,
+    CONF_DISCOVERY_PREFIX,
+    CONF_KEEPALIVE,
+    CONF_TLS_INSECURE,
     CONF_WILL_MESSAGE,
     DEFAULT_BIRTH,
     DEFAULT_DISCOVERY,
+    DEFAULT_ENCODING,
+    DEFAULT_KEEPALIVE,
     DEFAULT_PORT,
+    DEFAULT_PREFIX,
+    DEFAULT_PROTOCOL,
     DEFAULT_WILL,
     DOMAIN,
+    SUPPORTED_PROTOCOLS,
+)
+from .util import (
+    MQTT_WILL_BIRTH_SCHEMA,
+    async_create_certificate_temp_files,
+    get_file_path,
+    valid_publish_topic,
 )
-from .util import MQTT_WILL_BIRTH_SCHEMA, get_mqtt_data
 
 MQTT_TIMEOUT = 5
 
+ADVANCED_OPTIONS = "advanced_options"
+SET_CA_CERT = "set_ca_cert"
+SET_CLIENT_CERT = "set_client_cert"
+
 BOOLEAN_SELECTOR = BooleanSelector()
 TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
 PUBLISH_TOPIC_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
@@ -63,6 +95,40 @@ QOS_SELECTOR = vol.All(
     NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2)),
     vol.Coerce(int),
 )
+KEEPALIVE_SELECTOR = vol.All(
+    NumberSelector(
+        NumberSelectorConfig(
+            mode=NumberSelectorMode.BOX, min=15, step="any", unit_of_measurement="sec"
+        )
+    ),
+    vol.Coerce(int),
+)
+PROTOCOL_SELECTOR = SelectSelector(
+    SelectSelectorConfig(
+        options=SUPPORTED_PROTOCOLS,
+        mode=SelectSelectorMode.DROPDOWN,
+    )
+)
+CA_VERIFICATION_MODES = [
+    SelectOptionDict(value="off", label="Off"),
+    SelectOptionDict(value="auto", label="Auto"),
+    SelectOptionDict(value="custom", label="Custom"),
+]
+BROKER_VERIFICATION_SELECTOR = SelectSelector(
+    SelectSelectorConfig(
+        options=CA_VERIFICATION_MODES,
+        mode=SelectSelectorMode.DROPDOWN,
+    )
+)
+
+# mime configuration from https://pki-tutorial.readthedocs.io/en/latest/mime.html
+CA_CERT_UPLOAD_SELECTOR = FileSelector(
+    FileSelectorConfig(accept=".crt,application/x-x509-ca-cert")
+)
+CERT_UPLOAD_SELECTOR = FileSelector(
+    FileSelectorConfig(accept=".crt,application/x-x509-user-cert")
+)
+KEY_UPLOAD_SELECTOR = FileSelector(FileSelectorConfig(accept=".key,application/pkcs8"))
 
 
 class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@@ -93,24 +159,20 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
         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: dict[str, Any] = {}
         if await async_get_broker_settings(
             self.hass,
             fields,
-            yaml_config,
             None,
             user_input,
             validated_user_input,
             errors,
         ):
-            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,
-                test_config,
+                validated_user_input,
             )
 
             if can_connect:
@@ -177,7 +239,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
         """Initialize MQTT options flow."""
         self.config_entry = config_entry
         self.broker_config: dict[str, str | int] = {}
-        self.options = dict(config_entry.options)
+        self.options = config_entry.options
 
     async def async_step_init(self, user_input: None = None) -> FlowResult:
         """Manage the MQTT options."""
@@ -188,23 +250,19 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
     ) -> FlowResult:
         """Manage the MQTT broker configuration."""
         errors: dict[str, str] = {}
-        yaml_config: ConfigType = get_mqtt_data(self.hass, True).config or {}
         fields: OrderedDict[Any, Any] = OrderedDict()
         validated_user_input: dict[str, Any] = {}
         if await async_get_broker_settings(
             self.hass,
             fields,
-            yaml_config,
             self.config_entry.data,
             user_input,
             validated_user_input,
             errors,
         ):
-            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,
-                test_config,
+                validated_user_input,
             )
 
             if can_connect:
@@ -226,7 +284,6 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
         """Manage the MQTT options."""
         errors = {}
         current_config = self.config_entry.data
-        yaml_config = get_mqtt_data(self.hass, True).config or {}
         options_config: dict[str, Any] = {}
         bad_input: bool = False
 
@@ -255,6 +312,12 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
         if user_input is not None:
             # validate input
             options_config[CONF_DISCOVERY] = user_input[CONF_DISCOVERY]
+            _validate(
+                CONF_DISCOVERY_PREFIX,
+                user_input[CONF_DISCOVERY_PREFIX],
+                "bad_discovery_prefix",
+                valid_publish_topic,
+            )
             if "birth_topic" in user_input:
                 _validate(
                     CONF_BIRTH_MESSAGE,
@@ -279,6 +342,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
                 updated_config = {}
                 updated_config.update(self.broker_config)
                 updated_config.update(options_config)
+                CONFIG_SCHEMA_ENTRY(updated_config)
                 self.hass.config_entries.async_update_entry(
                     self.config_entry,
                     data=updated_config,
@@ -288,23 +352,21 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
 
         birth = {
             **DEFAULT_BIRTH,
-            **current_config.get(
-                CONF_BIRTH_MESSAGE, yaml_config.get(CONF_BIRTH_MESSAGE, {})
-            ),
+            **current_config.get(CONF_BIRTH_MESSAGE, {}),
         }
         will = {
             **DEFAULT_WILL,
-            **current_config.get(
-                CONF_WILL_MESSAGE, yaml_config.get(CONF_WILL_MESSAGE, {})
-            ),
+            **current_config.get(CONF_WILL_MESSAGE, {}),
         }
-        discovery = current_config.get(
-            CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
-        )
+        discovery = current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
+        discovery_prefix = current_config.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX)
 
         # build form
         fields: OrderedDict[vol.Marker, Any] = OrderedDict()
         fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = BOOLEAN_SELECTOR
+        fields[
+            vol.Optional(CONF_DISCOVERY_PREFIX, default=discovery_prefix)
+        ] = PUBLISH_TOPIC_SELECTOR
 
         # Birth message is disabled if CONF_BIRTH_MESSAGE = {}
         fields[
@@ -363,7 +425,6 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
 async def async_get_broker_settings(
     hass: HomeAssistant,
     fields: OrderedDict[Any, Any],
-    yaml_config: ConfigType,
     entry_config: MappingProxyType[str, Any] | None,
     user_input: dict[str, Any] | None,
     validated_user_input: dict[str, Any],
@@ -371,25 +432,144 @@ async def async_get_broker_settings(
 ) -> bool:
     """Build the config flow schema to collect the broker settings.
 
+    Shows advanced options if one or more are configured
+    or when the advanced_broker_options checkbox was selected.
     Returns True when settings are collected successfully.
     """
+    advanced_broker_options: bool = False
     user_input_basic: dict[str, Any] = {}
-    current_config = entry_config.copy() if entry_config is not None else {}
+    current_config: dict[str, Any] = (
+        entry_config.copy() if entry_config is not None else {}
+    )
+
+    async def _async_validate_broker_settings(
+        config: dict[str, Any],
+        user_input: dict[str, Any],
+        validated_user_input: dict[str, Any],
+        errors: dict[str, str],
+    ) -> bool:
+        """Additional validation on broker settings for better error messages."""
+
+        # Get current certificate settings from config entry
+        certificate: str | None = (
+            "auto"
+            if user_input.get(SET_CA_CERT, "off") == "auto"
+            else config.get(CONF_CERTIFICATE)
+            if user_input.get(SET_CA_CERT, "off") == "custom"
+            else None
+        )
+        client_certificate: str | None = (
+            config.get(CONF_CLIENT_CERT) if user_input.get(SET_CLIENT_CERT) else None
+        )
+        client_key: str | None = (
+            config.get(CONF_CLIENT_KEY) if user_input.get(SET_CLIENT_CERT) else None
+        )
 
-    if user_input is not None:
+        # Prepare entry update with uploaded files
         validated_user_input.update(user_input)
+        client_certificate_id: str | None = user_input.get(CONF_CLIENT_CERT)
+        client_key_id: str | None = user_input.get(CONF_CLIENT_KEY)
+        if (
+            client_certificate_id
+            and not client_key_id
+            or not client_certificate_id
+            and client_key_id
+        ):
+            errors["base"] = "invalid_inclusion"
+            return False
+        certificate_id: str | None = user_input.get(CONF_CERTIFICATE)
+        if certificate_id:
+            with process_uploaded_file(hass, certificate_id) as certiticate_file:
+                certificate = certiticate_file.read_text(encoding=DEFAULT_ENCODING)
+
+        # Return to form for file upload CA cert or client cert and key
+        if (
+            not client_certificate
+            and user_input.get(SET_CLIENT_CERT)
+            and not client_certificate_id
+            or not certificate
+            and user_input.get(SET_CA_CERT, "off") == "custom"
+            and not certificate_id
+        ):
+            return False
+
+        if client_certificate_id:
+            with process_uploaded_file(
+                hass, client_certificate_id
+            ) as client_certiticate_file:
+                client_certificate = client_certiticate_file.read_text(
+                    encoding=DEFAULT_ENCODING
+                )
+        if client_key_id:
+            with process_uploaded_file(hass, client_key_id) as key_file:
+                client_key = key_file.read_text(encoding=DEFAULT_ENCODING)
+
+        certificate_data: dict[str, Any] = {}
+        if certificate:
+            certificate_data[CONF_CERTIFICATE] = certificate
+        if client_certificate:
+            certificate_data[CONF_CLIENT_CERT] = client_certificate
+            certificate_data[CONF_CLIENT_KEY] = client_key
+
+        validated_user_input.update(certificate_data)
+        await async_create_certificate_temp_files(hass, certificate_data)
+        if error := await hass.async_add_executor_job(
+            check_certicate_chain,
+        ):
+            errors["base"] = error
+            return False
+
+        if SET_CA_CERT in validated_user_input:
+            del validated_user_input[SET_CA_CERT]
+        if SET_CLIENT_CERT in validated_user_input:
+            del validated_user_input[SET_CLIENT_CERT]
         return True
 
-    # Update the current settings the the new posted data to fill the defaults
+    if user_input:
+        user_input_basic = user_input.copy()
+        advanced_broker_options = user_input_basic.get(ADVANCED_OPTIONS, False)
+        if ADVANCED_OPTIONS not in user_input or advanced_broker_options is False:
+            if await _async_validate_broker_settings(
+                current_config,
+                user_input_basic,
+                validated_user_input,
+                errors,
+            ):
+                return True
+        # Get defaults settings from previous post
+        current_broker = user_input_basic.get(CONF_BROKER)
+        current_port = user_input_basic.get(CONF_PORT, DEFAULT_PORT)
+        current_user = user_input_basic.get(CONF_USERNAME)
+        current_pass = user_input_basic.get(CONF_PASSWORD)
+    else:
+        # Get default settings from entry or yaml (if any)
+        current_broker = current_config.get(CONF_BROKER)
+        current_port = current_config.get(CONF_PORT, DEFAULT_PORT)
+        current_user = current_config.get(CONF_USERNAME)
+        current_pass = current_config.get(CONF_PASSWORD)
+
+    # Treat the previous post as an update of the current settings (if there was a basic broker setup step)
     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)
+    # Get default settings for advanced broker options
+    current_client_id = current_config.get(CONF_CLIENT_ID)
+    current_keepalive = current_config.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE)
+    current_ca_certificate = current_config.get(CONF_CERTIFICATE)
+    current_client_certificate = current_config.get(CONF_CLIENT_CERT)
+    current_client_key = current_config.get(CONF_CLIENT_KEY)
+    current_tls_insecure = current_config.get(CONF_TLS_INSECURE, False)
+    current_protocol = current_config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL)
+    advanced_broker_options |= bool(
+        current_client_id
+        or current_keepalive != DEFAULT_KEEPALIVE
+        or current_ca_certificate
+        or current_client_certificate
+        or current_client_key
+        or current_tls_insecure
+        or current_protocol != DEFAULT_PROTOCOL
+        or current_config.get(SET_CA_CERT, "off") != "off"
+        or current_config.get(SET_CLIENT_CERT)
     )
-    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)] = TEXT_SELECTOR
@@ -406,6 +586,82 @@ async def async_get_broker_settings(
             description={"suggested_value": current_pass},
         )
     ] = PASSWORD_SELECTOR
+    # show advanced options checkbox if requested
+    # or when the defaults of advanced options are overridden
+    if not advanced_broker_options:
+        fields[
+            vol.Optional(
+                ADVANCED_OPTIONS,
+            )
+        ] = BOOLEAN_SELECTOR
+        return False
+    fields[
+        vol.Optional(
+            CONF_CLIENT_ID,
+            description={"suggested_value": current_client_id},
+        )
+    ] = TEXT_SELECTOR
+    fields[
+        vol.Optional(
+            CONF_KEEPALIVE,
+            description={"suggested_value": current_keepalive},
+        )
+    ] = KEEPALIVE_SELECTOR
+    fields[
+        vol.Optional(
+            SET_CLIENT_CERT,
+            default=current_client_certificate is not None
+            or current_config.get(SET_CLIENT_CERT) is True,
+        )
+    ] = BOOLEAN_SELECTOR
+    if (
+        current_client_certificate is not None
+        or current_config.get(SET_CLIENT_CERT) is True
+    ):
+        fields[
+            vol.Optional(
+                CONF_CLIENT_CERT,
+                description={"suggested_value": user_input_basic.get(CONF_CLIENT_CERT)},
+            )
+        ] = CERT_UPLOAD_SELECTOR
+        fields[
+            vol.Optional(
+                CONF_CLIENT_KEY,
+                description={"suggested_value": user_input_basic.get(CONF_CLIENT_KEY)},
+            )
+        ] = KEY_UPLOAD_SELECTOR
+    verification_mode = current_config.get(SET_CA_CERT) or (
+        "off"
+        if current_ca_certificate is None
+        else "auto"
+        if current_ca_certificate == "auto"
+        else "custom"
+    )
+    fields[
+        vol.Optional(
+            SET_CA_CERT,
+            default=verification_mode,
+        )
+    ] = BROKER_VERIFICATION_SELECTOR
+    if current_ca_certificate is not None or verification_mode == "custom":
+        fields[
+            vol.Optional(
+                CONF_CERTIFICATE,
+                user_input_basic.get(CONF_CERTIFICATE),
+            )
+        ] = CA_CERT_UPLOAD_SELECTOR
+    fields[
+        vol.Optional(
+            CONF_TLS_INSECURE,
+            description={"suggested_value": current_tls_insecure},
+        )
+    ] = BOOLEAN_SELECTOR
+    fields[
+        vol.Optional(
+            CONF_PROTOCOL,
+            description={"suggested_value": current_protocol},
+        )
+    ] = PROTOCOL_SELECTOR
 
     # Show form
     return False
@@ -439,3 +695,36 @@ def try_connection(
     finally:
         client.disconnect()
         client.loop_stop()
+
+
+def check_certicate_chain() -> str | None:
+    """Check the MQTT certificates."""
+    if client_certiticate := get_file_path(CONF_CLIENT_CERT):
+        try:
+            with open(client_certiticate, "rb") as client_certiticate_file:
+                load_pem_x509_certificate(client_certiticate_file.read())
+        except ValueError:
+            return "bad_client_cert"
+    # Check we can serialize the private key file
+    if private_key := get_file_path(CONF_CLIENT_KEY):
+        try:
+            with open(private_key, "rb") as client_key_file:
+                load_pem_private_key(client_key_file.read(), password=None)
+        except (TypeError, ValueError):
+            return "bad_client_key"
+    # Check the certificate chain
+    context = SSLContext(PROTOCOL_TLS)
+    if client_certiticate and private_key:
+        try:
+            context.load_cert_chain(client_certiticate, private_key)
+        except SSLError:
+            return "bad_client_cert_key"
+    # try to load the custom CA file
+    if (ca_cert := get_file_path(CONF_CERTIFICATE)) is None:
+        return None
+
+    try:
+        context.load_verify_locations(ca_cert)
+    except SSLError:
+        return "bad_certificate"
+    return None
diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py
index ab685a63802..2b07d6f9db4 100644
--- a/homeassistant/components/mqtt/config_integration.py
+++ b/homeassistant/components/mqtt/config_integration.py
@@ -51,26 +51,28 @@ from .const import (
     CONF_WILL_MESSAGE,
     DEFAULT_BIRTH,
     DEFAULT_DISCOVERY,
+    DEFAULT_KEEPALIVE,
+    DEFAULT_PORT,
     DEFAULT_PREFIX,
+    DEFAULT_PROTOCOL,
     DEFAULT_QOS,
     DEFAULT_RETAIN,
     DEFAULT_WILL,
-    PROTOCOL_31,
-    PROTOCOL_311,
+    SUPPORTED_PROTOCOLS,
 )
 from .util import _VALID_QOS_SCHEMA, valid_publish_topic
 
-DEFAULT_PORT = 1883
-DEFAULT_KEEPALIVE = 60
-DEFAULT_PROTOCOL = PROTOCOL_311
 DEFAULT_TLS_PROTOCOL = "auto"
 
 DEFAULT_VALUES = {
     CONF_BIRTH_MESSAGE: DEFAULT_BIRTH,
     CONF_DISCOVERY: DEFAULT_DISCOVERY,
+    CONF_DISCOVERY_PREFIX: DEFAULT_PREFIX,
     CONF_PORT: DEFAULT_PORT,
+    CONF_PROTOCOL: DEFAULT_PROTOCOL,
     CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL,
     CONF_WILL_MESSAGE: DEFAULT_WILL,
+    CONF_KEEPALIVE: DEFAULT_KEEPALIVE,
 }
 
 PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema(
@@ -148,12 +150,35 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema(
     required=True,
 )
 
+CONFIG_SCHEMA_ENTRY = vol.Schema(
+    {
+        vol.Optional(CONF_CLIENT_ID): cv.string,
+        vol.Optional(CONF_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)),
+        vol.Optional(CONF_BROKER): cv.string,
+        vol.Optional(CONF_PORT): cv.port,
+        vol.Optional(CONF_USERNAME): cv.string,
+        vol.Optional(CONF_PASSWORD): cv.string,
+        vol.Optional(CONF_CERTIFICATE): str,
+        vol.Inclusive(CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG): str,
+        vol.Inclusive(
+            CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG
+        ): str,
+        vol.Optional(CONF_TLS_INSECURE): cv.boolean,
+        vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"),
+        vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)),
+        vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
+        vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
+        vol.Optional(CONF_DISCOVERY): cv.boolean,
+        # discovery_prefix must be a valid publish topic because if no
+        # state topic is specified, it will be created with the given prefix.
+        vol.Optional(CONF_DISCOVERY_PREFIX): valid_publish_topic,
+    }
+)
+
 CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend(
     {
         vol.Optional(CONF_CLIENT_ID): cv.string,
-        vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(
-            vol.Coerce(int), vol.Range(min=15)
-        ),
+        vol.Optional(CONF_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)),
         vol.Optional(CONF_BROKER): cv.string,
         vol.Optional(CONF_PORT): cv.port,
         vol.Optional(CONF_USERNAME): cv.string,
@@ -167,27 +192,34 @@ CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend(
         ): cv.isfile,
         vol.Optional(CONF_TLS_INSECURE): cv.boolean,
         vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"),
-        vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(
-            cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])
-        ),
+        vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)),
         vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
         vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
         vol.Optional(CONF_DISCOVERY): cv.boolean,
         # discovery_prefix must be a valid publish topic because if no
         # state topic is specified, it will be created with the given prefix.
-        vol.Optional(
-            CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX
-        ): valid_publish_topic,
+        vol.Optional(CONF_DISCOVERY_PREFIX): valid_publish_topic,
     }
 )
 
 DEPRECATED_CONFIG_KEYS = [
     CONF_BIRTH_MESSAGE,
     CONF_BROKER,
+    CONF_CLIENT_ID,
     CONF_DISCOVERY,
+    CONF_DISCOVERY_PREFIX,
+    CONF_KEEPALIVE,
     CONF_PASSWORD,
     CONF_PORT,
+    CONF_PROTOCOL,
+    CONF_TLS_INSECURE,
     CONF_TLS_VERSION,
     CONF_USERNAME,
     CONF_WILL_MESSAGE,
 ]
+
+DEPRECATED_CERTIFICATE_CONFIG_KEYS = [
+    CONF_CERTIFICATE,
+    CONF_CLIENT_CERT,
+    CONF_CLIENT_KEY,
+]
diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py
index d266ed231ba..ff9776a01f7 100644
--- a/homeassistant/components/mqtt/const.py
+++ b/homeassistant/components/mqtt/const.py
@@ -43,6 +43,14 @@ DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline"
 DEFAULT_PORT = 1883
 DEFAULT_RETAIN = False
 
+PROTOCOL_31 = "3.1"
+PROTOCOL_311 = "3.1.1"
+SUPPORTED_PROTOCOLS = [PROTOCOL_31, PROTOCOL_311]
+
+DEFAULT_PORT = 1883
+DEFAULT_KEEPALIVE = 60
+DEFAULT_PROTOCOL = PROTOCOL_311
+
 DEFAULT_BIRTH = {
     ATTR_TOPIC: DEFAULT_BIRTH_WILL_TOPIC,
     CONF_PAYLOAD: DEFAULT_PAYLOAD_AVAILABLE,
@@ -65,11 +73,6 @@ MQTT_DISCONNECTED = "mqtt_disconnected"
 PAYLOAD_EMPTY_JSON = "{}"
 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/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json
index 6fb81deeb4d..d37b15769ad 100644
--- a/homeassistant/components/mqtt/manifest.json
+++ b/homeassistant/components/mqtt/manifest.json
@@ -4,7 +4,7 @@
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/mqtt",
   "requirements": ["paho-mqtt==1.6.1"],
-  "dependencies": ["http"],
+  "dependencies": ["file_upload", "http"],
   "codeowners": ["@emontnemery"],
   "iot_class": "local_push"
 }
diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json
index e11f0a685e7..4799b45e631 100644
--- a/homeassistant/components/mqtt/strings.json
+++ b/homeassistant/components/mqtt/strings.json
@@ -3,6 +3,10 @@
     "deprecated_yaml": {
       "title": "Your manually configured MQTT {platform}(s) needs attention",
       "description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information."
+    },
+    "deprecated_yaml_broker_settings": {
+      "title": "Deprecated MQTT settings found in `configuration.yaml`",
+      "description": "The following settings found in `configuration.yaml` were migrated to MQTT config entry and will now override the settings in `configuration.yaml`:\n`{deprecated_settings}`\n\nPlease remove these settings from `configuration.yaml` and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information."
     }
   },
   "config": {
@@ -14,7 +18,16 @@
           "port": "[%key:common::config_flow::data::port%]",
           "username": "[%key:common::config_flow::data::username%]",
           "password": "[%key:common::config_flow::data::password%]",
-          "discovery": "Enable discovery"
+          "advanced_options": "Advanced options",
+          "certificate": "Path to custom CA certificate file",
+          "client_id": "Client ID (leave empty to randomly generated one)",
+          "client_cert": "Path to a client certificate file",
+          "client_key": "Path to a private key file",
+          "keepalive": "The time between sending keep alive messages",
+          "tls_insecure": "Ignore broker certificate validation",
+          "protocol": "MQTT protocol",
+          "set_ca_cert": "Broker certificate validation",
+          "set_client_cert": "Use a client certificate"
         }
       },
       "hassio_confirm": {
@@ -30,7 +43,15 @@
       "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
     },
     "error": {
-      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+      "bad_birth": "Invalid birth topic",
+      "bad_will": "Invalid will topic",
+      "bad_discovery_prefix": "Invalid discovery prefix",
+      "bad_certificate": "The CA certificate is invalid",
+      "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
+      "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
+      "bad_client_cert_key": "Client certificate and private are no valid pair",
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "invalid_inclusion": "The client certificate and private key must be configurered together"
     }
   },
   "device_automation": {
@@ -64,14 +85,25 @@
           "broker": "Broker",
           "port": "[%key:common::config_flow::data::port%]",
           "username": "[%key:common::config_flow::data::username%]",
-          "password": "[%key:common::config_flow::data::password%]"
+          "password": "[%key:common::config_flow::data::password%]",
+          "advanced_options": "Advanced options",
+          "certificate": "Upload custom CA certificate file",
+          "client_id": "Client ID (leave empty to randomly generated one)",
+          "client_cert": "Upload client certificate file",
+          "client_key": "Upload private key file",
+          "keepalive": "The time between sending keep alive messages",
+          "tls_insecure": "Ignore broker certificate validation",
+          "protocol": "MQTT protocol",
+          "set_ca_cert": "Broker certificate validation",
+          "set_client_cert": "Use a client certificate"
         }
       },
       "options": {
         "title": "MQTT options",
-        "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
+        "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nDiscovery prefix - The prefix a configuration topic for automatic discovery must start with.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
         "data": {
           "discovery": "Enable discovery",
+          "discovery_prefix": "Discovery prefix",
           "birth_enable": "Enable birth message",
           "birth_topic": "Birth message topic",
           "birth_payload": "Birth message payload",
@@ -86,9 +118,15 @@
       }
     },
     "error": {
+      "bad_birth": "Invalid birth topic",
+      "bad_will": "Invalid will topic",
+      "bad_discovery_prefix": "Invalid discovery prefix",
+      "bad_certificate": "The CA certificate is invalid",
+      "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
+      "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
+      "bad_client_cert_key": "Client certificate and private are no valid pair",
       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
-      "bad_birth": "Invalid birth topic.",
-      "bad_will": "Invalid will topic."
+      "invalid_inclusion": "The client certificate and private key must be configured together"
     }
   }
 }
diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json
index f495c4eea2b..92ab99d90dc 100644
--- a/homeassistant/components/mqtt/translations/en.json
+++ b/homeassistant/components/mqtt/translations/en.json
@@ -5,15 +5,32 @@
             "single_instance_allowed": "Already configured. Only a single configuration possible."
         },
         "error": {
-            "cannot_connect": "Failed to connect"
+            "bad_birth": "Invalid birth topic",
+            "bad_discovery_prefix": "Invalid discovery prefix",
+            "bad_certificate": "The CA certificate is invalid",
+            "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
+            "bad_client_cert_key": "Client certificate and private are no valid pair",
+            "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
+            "bad_will": "Invalid will topic",
+            "cannot_connect": "Failed to connect",
+            "invalid_inclusion": "The client certificate and private key must be configurered together"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Advanced options",
                     "broker": "Broker",
-                    "discovery": "Enable discovery",
+                    "certificate": "Path to custom CA certificate file",
+                    "client_id": "Client ID (leave empty to randomly generated one)",
+                    "client_cert": "Path to a client certificate file",
+                    "client_key": "Path to a private key file",
+                    "keepalive": "The time between sending keep alive messages",
                     "password": "Password",
                     "port": "Port",
+                    "protocol": "MQTT protocol",
+                    "set_ca_cert": "Broker certificate validation",
+                    "set_client_cert": "Use a client certificate",
+                    "tls_insecure": "Ignore broker certificate validation",
                     "username": "Username"
                 },
                 "description": "Please enter the connection information of your MQTT broker."
@@ -53,20 +70,40 @@
         "deprecated_yaml": {
             "description": "Manually configured MQTT {platform}(s) found under platform key `{platform}`.\n\nPlease move the configuration to the `mqtt` integration key and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information.",
             "title": "Your manually configured MQTT {platform}(s) needs attention"
+        },
+        "deprecated_yaml_broker_settings": {
+            "title": "Deprecated MQTT settings found in `configuration.yaml`",
+            "description": "The following settings found in `configuration.yaml` were migrated to MQTT config entry and will now override the settings in `configuration.yaml`:\n`{deprecated_settings}`\n\nPlease remove these settings from `configuration.yaml` and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information."
         }
     },
     "options": {
         "error": {
-            "bad_birth": "Invalid birth topic.",
-            "bad_will": "Invalid will topic.",
-            "cannot_connect": "Failed to connect"
+            "bad_birth": "Invalid birth topic",
+            "bad_discovery_prefix": "Invalid discovery prefix",
+            "bad_certificate": "The CA certificate is invalid",
+            "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
+            "bad_client_cert_key": "Client certificate and private are no valid pair",
+            "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
+            "bad_will": "Invalid will topic",
+            "cannot_connect": "Failed to connect",
+            "invalid_inclusion": "The client certificate and private key must be configured together"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Advanced options",
                     "broker": "Broker",
+                    "certificate": "Upload custom CA certificate file",
+                    "client_id": "Client ID (leave empty to randomly generated one)",
+                    "client_cert": "Upload client certificate file",
+                    "client_key": "Upload private key file",
+                    "keepalive": "The time between sending keep alive messages",
                     "password": "Password",
                     "port": "Port",
+                    "protocol": "MQTT protocol",
+                    "set_ca_cert": "Broker certificate validation",
+                    "set_client_cert": "Use a client certificate",
+                    "tls_insecure": "Ignore broker certificate validation",
                     "username": "Username"
                 },
                 "description": "Please enter the connection information of your MQTT broker.",
@@ -80,13 +117,14 @@
                     "birth_retain": "Birth message retain",
                     "birth_topic": "Birth message topic",
                     "discovery": "Enable discovery",
+                    "discovery_prefix": "Discovery prefix",
                     "will_enable": "Enable will message",
                     "will_payload": "Will message payload",
                     "will_qos": "Will message QoS",
                     "will_retain": "Will message retain",
                     "will_topic": "Will message topic"
                 },
-                "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
+                "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nDiscovery prefix - The prefix a configuration topic for automatic discovery must start with.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.",
                 "title": "MQTT options"
             }
         }
diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py
index 43734872e14..7e23b6c01f1 100644
--- a/homeassistant/components/mqtt/util.py
+++ b/homeassistant/components/mqtt/util.py
@@ -2,6 +2,9 @@
 
 from __future__ import annotations
 
+import os
+from pathlib import Path
+import tempfile
 from typing import Any
 
 import voluptuous as vol
@@ -9,19 +12,26 @@ import voluptuous as vol
 from homeassistant.const import CONF_PAYLOAD
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import config_validation as cv, template
+from homeassistant.helpers.typing import ConfigType
 
 from .const import (
     ATTR_PAYLOAD,
     ATTR_QOS,
     ATTR_RETAIN,
     ATTR_TOPIC,
+    CONF_CERTIFICATE,
+    CONF_CLIENT_CERT,
+    CONF_CLIENT_KEY,
     DATA_MQTT,
+    DEFAULT_ENCODING,
     DEFAULT_QOS,
     DEFAULT_RETAIN,
     DOMAIN,
 )
 from .models import MqttData
 
+TEMP_DIR_NAME = f"home-assistant-{DOMAIN}"
+
 
 def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None:
     """Return true when the MQTT config entry is enabled."""
@@ -120,3 +130,57 @@ def get_mqtt_data(hass: HomeAssistant, ensure_exists: bool = False) -> MqttData:
     if ensure_exists:
         return hass.data.setdefault(DATA_MQTT, MqttData())
     return hass.data[DATA_MQTT]
+
+
+async def async_create_certificate_temp_files(
+    hass: HomeAssistant, config: ConfigType
+) -> None:
+    """Create certificate temporary files for the MQTT client."""
+
+    def _create_temp_file(temp_file: Path, data: str | None) -> None:
+        if data is None or data == "auto":
+            if temp_file.exists():
+                os.remove(Path(temp_file))
+            return
+        temp_file.write_text(data)
+
+    def _create_temp_dir_and_files() -> None:
+        """Create temporary directory."""
+        temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
+
+        if (
+            config.get(CONF_CERTIFICATE)
+            or config.get(CONF_CLIENT_CERT)
+            or config.get(CONF_CLIENT_KEY)
+        ) and not temp_dir.exists():
+            temp_dir.mkdir(0o700)
+
+        _create_temp_file(temp_dir / CONF_CERTIFICATE, config.get(CONF_CERTIFICATE))
+        _create_temp_file(temp_dir / CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT))
+        _create_temp_file(temp_dir / CONF_CLIENT_KEY, config.get(CONF_CLIENT_KEY))
+
+    await hass.async_add_executor_job(_create_temp_dir_and_files)
+
+
+def get_file_path(option: str, default: str | None = None) -> Path | str | None:
+    """Get file path of a certificate file."""
+    temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME
+    if not temp_dir.exists():
+        return default
+
+    file_path: Path = temp_dir / option
+    if not file_path.exists():
+        return default
+
+    return temp_dir / option
+
+
+def migrate_certificate_file_to_content(file_name_or_auto: str) -> str | None:
+    """Convert certificate file or setting to config entry setting."""
+    if file_name_or_auto == "auto":
+        return "auto"
+    try:
+        with open(file_name_or_auto, encoding=DEFAULT_ENCODING) as certiticate_file:
+            return certiticate_file.read()
+    except OSError:
+        return None
diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py
index c60df7089ad..eef728664aa 100644
--- a/tests/components/mqtt/test_config_flow.py
+++ b/tests/components/mqtt/test_config_flow.py
@@ -1,5 +1,8 @@
 """Test config flow."""
+from random import getrandbits
+from ssl import SSLError
 from unittest.mock import AsyncMock, patch
+from uuid import uuid4
 
 import pytest
 import voluptuous as vol
@@ -13,6 +16,9 @@ from homeassistant.setup import async_setup_component
 
 from tests.common import MockConfigEntry
 
+MOCK_CLIENT_CERT = b"## mock client certificate file ##"
+MOCK_CLIENT_KEY = b"## mock key file ##"
+
 
 @pytest.fixture(autouse=True)
 def mock_finish_setup():
@@ -23,6 +29,43 @@ def mock_finish_setup():
         yield mock_finish
 
 
+@pytest.fixture
+def mock_client_cert_check_fail():
+    """Mock the client certificate check."""
+    with patch(
+        "homeassistant.components.mqtt.config_flow.load_pem_x509_certificate",
+        side_effect=ValueError,
+    ) as mock_cert_check:
+        yield mock_cert_check
+
+
+@pytest.fixture
+def mock_client_key_check_fail():
+    """Mock the client key file check."""
+    with patch(
+        "homeassistant.components.mqtt.config_flow.load_pem_private_key",
+        side_effect=ValueError,
+    ) as mock_key_check:
+        yield mock_key_check
+
+
+@pytest.fixture
+def mock_ssl_context():
+    """Mock the SSL context used to load the cert chain and to load verify locations."""
+    with patch(
+        "homeassistant.components.mqtt.config_flow.SSLContext"
+    ) as mock_context, patch(
+        "homeassistant.components.mqtt.config_flow.load_pem_private_key"
+    ) as mock_key_check, patch(
+        "homeassistant.components.mqtt.config_flow.load_pem_x509_certificate"
+    ) as mock_cert_check:
+        yield {
+            "context": mock_context,
+            "load_pem_x509_certificate": mock_cert_check,
+            "load_pem_private_key": mock_key_check,
+        }
+
+
 @pytest.fixture
 def mock_reload_after_entry_update():
     """Mock out the reload after updating the entry."""
@@ -84,6 +127,45 @@ def mock_try_connection_time_out():
         yield mock_client()
 
 
+@pytest.fixture
+def mock_process_uploaded_file(tmp_path):
+    """Mock upload certificate files."""
+    file_id_ca = str(uuid4())
+    file_id_cert = str(uuid4())
+    file_id_key = str(uuid4())
+
+    def _mock_process_uploaded_file(hass, file_id):
+        if file_id == file_id_ca:
+            with open(tmp_path / "ca.crt", "wb") as cafile:
+                cafile.write(b"## mock CA certificate file ##")
+            return tmp_path / "ca.crt"
+        elif file_id == file_id_cert:
+            with open(tmp_path / "client.crt", "wb") as certfile:
+                certfile.write(b"## mock client certificate file ##")
+            return tmp_path / "client.crt"
+        elif file_id == file_id_key:
+            with open(tmp_path / "client.key", "wb") as keyfile:
+                keyfile.write(b"## mock key file ##")
+            return tmp_path / "client.key"
+        else:
+            assert False
+
+    with patch(
+        "homeassistant.components.mqtt.config_flow.process_uploaded_file",
+        side_effect=_mock_process_uploaded_file,
+    ) as mock_upload, patch(
+        # Patch temp dir name to avoid tests fail running in parallel
+        "homeassistant.components.mqtt.util.TEMP_DIR_NAME",
+        "home-assistant-mqtt" + f"-{getrandbits(10):03x}",
+    ):
+        mock_upload.file_id = {
+            mqtt.CONF_CERTIFICATE: file_id_ca,
+            mqtt.CONF_CLIENT_CERT: file_id_cert,
+            mqtt.CONF_CLIENT_KEY: file_id_key,
+        }
+        yield mock_upload
+
+
 async def test_user_connection_works(
     hass, mock_try_connection, mock_finish_setup, mqtt_client_mock
 ):
@@ -96,7 +178,7 @@ async def test_user_connection_works(
     assert result["type"] == "form"
 
     result = await hass.config_entries.flow.async_configure(
-        result["flow_id"], {"broker": "127.0.0.1"}
+        result["flow_id"], {"broker": "127.0.0.1", "advanced_options": False}
     )
 
     assert result["type"] == "create_entry"
@@ -104,6 +186,7 @@ async def test_user_connection_works(
         "broker": "127.0.0.1",
         "port": 1883,
         "discovery": True,
+        "discovery_prefix": "homeassistant",
     }
     # Check we tried the connection
     assert len(mock_try_connection.mock_calls) == 1
@@ -184,15 +267,14 @@ async def test_manual_config_set(
         "broker": "127.0.0.1",
         "port": 1883,
         "discovery": True,
+        "discovery_prefix": "homeassistant",
     }
     # Check we tried the connection, with precedence for config entry settings
     mock_try_connection.assert_called_once_with(
         {
             "broker": "127.0.0.1",
-            "protocol": "3.1.1",
-            "keepalive": 60,
-            "discovery_prefix": "homeassistant",
             "port": 1883,
+            "discovery": True,
         },
     )
     # Check config entry got setup
@@ -285,6 +367,7 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set
         "username": "mock-user",
         "password": "mock-pass",
         "discovery": True,
+        "discovery_prefix": "homeassistant",
     }
     # Check we tried the connection
     assert len(mock_try_connection_success.mock_calls)
@@ -376,6 +459,7 @@ async def test_option_flow(
         result["flow_id"],
         user_input={
             mqtt.CONF_DISCOVERY: True,
+            "discovery_prefix": "homeassistant",
             "birth_enable": True,
             "birth_topic": "ha_state/online",
             "birth_payload": "online",
@@ -396,6 +480,7 @@ async def test_option_flow(
         mqtt.CONF_USERNAME: "user",
         mqtt.CONF_PASSWORD: "pass",
         mqtt.CONF_DISCOVERY: True,
+        mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
         mqtt.CONF_BIRTH_MESSAGE: {
             mqtt.ATTR_TOPIC: "ha_state/online",
             mqtt.ATTR_PAYLOAD: "online",
@@ -419,6 +504,160 @@ async def test_option_flow(
     )
 
 
+@pytest.mark.parametrize(
+    "test_error",
+    [
+        "bad_certificate",
+        "bad_client_cert",
+        "bad_client_key",
+        "bad_client_cert_key",
+        "invalid_inclusion",
+        None,
+    ],
+)
+async def test_bad_certificate(
+    hass,
+    mqtt_mock_entry_no_yaml_config,
+    mock_try_connection_success,
+    tmp_path,
+    mock_ssl_context,
+    test_error,
+    mock_process_uploaded_file,
+):
+    """Test bad certificate tests."""
+    # Mock certificate files
+    file_id = mock_process_uploaded_file.file_id
+    test_input = {
+        mqtt.CONF_BROKER: "another-broker",
+        mqtt.CONF_PORT: 2345,
+        mqtt.CONF_CERTIFICATE: file_id[mqtt.CONF_CERTIFICATE],
+        mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT],
+        mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY],
+        "set_ca_cert": True,
+        "set_client_cert": True,
+    }
+    set_client_cert = True
+    set_ca_cert = "custom"
+    tls_insecure = False
+    if test_error == "bad_certificate":
+        # CA chain is not loading
+        mock_ssl_context["context"]().load_verify_locations.side_effect = SSLError
+    elif test_error == "bad_client_cert":
+        # Client certificate is invalid
+        mock_ssl_context["load_pem_x509_certificate"].side_effect = ValueError
+    elif test_error == "bad_client_key":
+        # Client key file is invalid
+        mock_ssl_context["load_pem_private_key"].side_effect = ValueError
+    elif test_error == "bad_client_cert_key":
+        # Client key file file and certificate do not pair
+        mock_ssl_context["context"]().load_cert_chain.side_effect = SSLError
+    elif test_error == "invalid_inclusion":
+        # Client key file without client cert, client cert without key file
+        test_input.pop(mqtt.CONF_CLIENT_KEY)
+
+    mqtt_mock = await mqtt_mock_entry_no_yaml_config()
+    mock_try_connection.return_value = True
+    config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
+    # Add at least one advanced option to get the full form
+    config_entry.data = {
+        mqtt.CONF_BROKER: "test-broker",
+        mqtt.CONF_PORT: 1234,
+        mqtt.CONF_CLIENT_ID: "custom1234",
+        mqtt.CONF_KEEPALIVE: 60,
+        mqtt.CONF_TLS_INSECURE: False,
+        mqtt.CONF_PROTOCOL: "3.1.1",
+    }
+
+    mqtt_mock.async_connect.reset_mock()
+
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["step_id"] == "broker"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            mqtt.CONF_BROKER: "another-broker",
+            mqtt.CONF_PORT: 2345,
+            mqtt.CONF_KEEPALIVE: 60,
+            "set_client_cert": set_client_cert,
+            "set_ca_cert": set_ca_cert,
+            mqtt.CONF_TLS_INSECURE: tls_insecure,
+            mqtt.CONF_PROTOCOL: "3.1.1",
+            mqtt.CONF_CLIENT_ID: "custom1234",
+        },
+    )
+    test_input["set_client_cert"] = set_client_cert
+    test_input["set_ca_cert"] = set_ca_cert
+    test_input["tls_insecure"] = tls_insecure
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input=test_input,
+    )
+    if test_error is not None:
+        assert result["errors"]["base"] == test_error
+        return
+    assert result["errors"] == {}
+
+
+@pytest.mark.parametrize(
+    "input_value, error",
+    [
+        ("", True),
+        ("-10", True),
+        ("10", True),
+        ("15", False),
+        ("26", False),
+        ("100", False),
+    ],
+)
+async def test_keepalive_validation(
+    hass,
+    mqtt_mock_entry_no_yaml_config,
+    mock_try_connection,
+    mock_reload_after_entry_update,
+    input_value,
+    error,
+):
+    """Test validation of the keep alive option."""
+
+    test_input = {
+        mqtt.CONF_BROKER: "another-broker",
+        mqtt.CONF_PORT: 2345,
+        mqtt.CONF_KEEPALIVE: input_value,
+    }
+
+    mqtt_mock = await mqtt_mock_entry_no_yaml_config()
+    mock_try_connection.return_value = True
+    config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
+    # Add at least one advanced option to get the full form
+    config_entry.data = {
+        mqtt.CONF_BROKER: "test-broker",
+        mqtt.CONF_PORT: 1234,
+        mqtt.CONF_CLIENT_ID: "custom1234",
+    }
+
+    mqtt_mock.async_connect.reset_mock()
+
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["step_id"] == "broker"
+
+    if error:
+        with pytest.raises(vol.MultipleInvalid):
+            result = await hass.config_entries.options.async_configure(
+                result["flow_id"],
+                user_input=test_input,
+            )
+        return
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input=test_input,
+    )
+    assert not result["errors"]
+
+
 async def test_disable_birth_will(
     hass,
     mqtt_mock_entry_no_yaml_config,
@@ -459,6 +698,7 @@ async def test_disable_birth_will(
         result["flow_id"],
         user_input={
             mqtt.CONF_DISCOVERY: True,
+            mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
             "birth_enable": False,
             "birth_topic": "ha_state/online",
             "birth_payload": "online",
@@ -479,6 +719,7 @@ async def test_disable_birth_will(
         mqtt.CONF_USERNAME: "user",
         mqtt.CONF_PASSWORD: "pass",
         mqtt.CONF_DISCOVERY: True,
+        mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
         mqtt.CONF_BIRTH_MESSAGE: {},
         mqtt.CONF_WILL_MESSAGE: {},
     }
@@ -488,6 +729,64 @@ async def test_disable_birth_will(
     assert mock_reload_after_entry_update.call_count == 1
 
 
+async def test_invalid_discovery_prefix(
+    hass,
+    mqtt_mock_entry_no_yaml_config,
+    mock_try_connection,
+    mock_reload_after_entry_update,
+):
+    """Test setting an invalid discovery prefix."""
+    mqtt_mock = await mqtt_mock_entry_no_yaml_config()
+    mock_try_connection.return_value = True
+    config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
+    config_entry.data = {
+        mqtt.CONF_BROKER: "test-broker",
+        mqtt.CONF_PORT: 1234,
+        mqtt.CONF_DISCOVERY: True,
+        mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
+    }
+
+    mqtt_mock.async_connect.reset_mock()
+
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["step_id"] == "broker"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            mqtt.CONF_BROKER: "another-broker",
+            mqtt.CONF_PORT: 2345,
+        },
+    )
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["step_id"] == "options"
+
+    await hass.async_block_till_done()
+    assert mqtt_mock.async_connect.call_count == 0
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            mqtt.CONF_DISCOVERY: True,
+            mqtt.CONF_DISCOVERY_PREFIX: "homeassistant#invalid",
+        },
+    )
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["step_id"] == "options"
+    assert result["errors"]["base"] == "bad_discovery_prefix"
+    assert config_entry.data == {
+        mqtt.CONF_BROKER: "test-broker",
+        mqtt.CONF_PORT: 1234,
+        mqtt.CONF_DISCOVERY: True,
+        mqtt.CONF_DISCOVERY_PREFIX: "homeassistant",
+    }
+
+    await hass.async_block_till_done()
+    # assert that the entry was not reloaded with the new config
+    assert mock_reload_after_entry_update.call_count == 0
+
+
 def get_default(schema, key):
     """Get default value for key in voluptuous schema."""
     for k in schema.keys():
@@ -658,6 +957,47 @@ async def test_option_flow_default_suggested_values(
     await hass.async_block_till_done()
 
 
+@pytest.mark.parametrize(
+    "advanced_options, step_id", [(False, "options"), (True, "broker")]
+)
+async def test_skipping_advanced_options(
+    hass,
+    mqtt_mock_entry_no_yaml_config,
+    mock_try_connection,
+    mock_reload_after_entry_update,
+    advanced_options,
+    step_id,
+):
+    """Test advanced options option."""
+
+    test_input = {
+        mqtt.CONF_BROKER: "another-broker",
+        mqtt.CONF_PORT: 2345,
+        "advanced_options": advanced_options,
+    }
+
+    mqtt_mock = await mqtt_mock_entry_no_yaml_config()
+    mock_try_connection.return_value = True
+    config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
+    # Initiate with a basic setup
+    config_entry.data = {
+        mqtt.CONF_BROKER: "test-broker",
+        mqtt.CONF_PORT: 1234,
+    }
+
+    mqtt_mock.async_connect.reset_mock()
+
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert result["type"] == data_entry_flow.FlowResultType.FORM
+    assert result["step_id"] == "broker"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input=test_input,
+    )
+    assert result["step_id"] == step_id
+
+
 async def test_options_user_connection_fails(hass, mock_try_connection_time_out):
     """Test if connection cannot be made."""
     config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
@@ -760,50 +1100,57 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection):
 
 
 async def test_try_connection_with_advanced_parameters(
-    hass, mock_try_connection_success, tmp_path
+    hass,
+    mqtt_mock_entry_with_yaml_config,
+    mock_try_connection_success,
+    tmp_path,
+    mock_ssl_context,
+    mock_process_uploaded_file,
 ):
     """Test config flow with advanced parameters from config."""
-    # Mock certificate files
-    certfile = tmp_path / "cert.pem"
-    certfile.write_text("## mock certificate file ##")
-    keyfile = tmp_path / "key.pem"
-    keyfile.write_text("## mock key file ##")
+
+    with open(tmp_path / "client.crt", "wb") as certfile:
+        certfile.write(MOCK_CLIENT_CERT)
+    with open(tmp_path / "client.key", "wb") as keyfile:
+        keyfile.write(MOCK_CLIENT_KEY)
+
     config = {
         "certificate": "auto",
         "tls_insecure": True,
-        "client_cert": certfile,
-        "client_key": keyfile,
+        "client_cert": str(tmp_path / "client.crt"),
+        "client_key": str(tmp_path / "client.key"),
     }
     new_yaml_config_file = tmp_path / "configuration.yaml"
     new_yaml_config = yaml.dump({mqtt.DOMAIN: config})
     new_yaml_config_file.write_text(new_yaml_config)
     assert new_yaml_config_file.read_text() == new_yaml_config
 
+    config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
+    config_entry.add_to_hass(hass)
+    config_entry.data = {
+        mqtt.CONF_BROKER: "test-broker",
+        mqtt.CONF_PORT: 1234,
+        mqtt.CONF_USERNAME: "user",
+        mqtt.CONF_PASSWORD: "pass",
+        mqtt.CONF_KEEPALIVE: 30,
+        mqtt.CONF_DISCOVERY: True,
+        mqtt.CONF_BIRTH_MESSAGE: {
+            mqtt.ATTR_TOPIC: "ha_state/online",
+            mqtt.ATTR_PAYLOAD: "online",
+            mqtt.ATTR_QOS: 1,
+            mqtt.ATTR_RETAIN: True,
+        },
+        mqtt.CONF_WILL_MESSAGE: {
+            mqtt.ATTR_TOPIC: "ha_state/offline",
+            mqtt.ATTR_PAYLOAD: "offline",
+            mqtt.ATTR_QOS: 2,
+            mqtt.ATTR_RETAIN: False,
+        },
+    }
+
     with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file):
         await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config})
         await hass.async_block_till_done()
-        config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
-        config_entry.add_to_hass(hass)
-        config_entry.data = {
-            mqtt.CONF_BROKER: "test-broker",
-            mqtt.CONF_PORT: 1234,
-            mqtt.CONF_USERNAME: "user",
-            mqtt.CONF_PASSWORD: "pass",
-            mqtt.CONF_DISCOVERY: True,
-            mqtt.CONF_BIRTH_MESSAGE: {
-                mqtt.ATTR_TOPIC: "ha_state/online",
-                mqtt.ATTR_PAYLOAD: "online",
-                mqtt.ATTR_QOS: 1,
-                mqtt.ATTR_RETAIN: True,
-            },
-            mqtt.CONF_WILL_MESSAGE: {
-                mqtt.ATTR_TOPIC: "ha_state/offline",
-                mqtt.ATTR_PAYLOAD: "offline",
-                mqtt.ATTR_QOS: 2,
-                mqtt.ATTR_RETAIN: False,
-            },
-        }
-
         # Test default/suggested values from config
         result = await hass.config_entries.options.async_init(config_entry.entry_id)
         assert result["type"] == data_entry_flow.FlowResultType.FORM
@@ -811,16 +1158,32 @@ async def test_try_connection_with_advanced_parameters(
         defaults = {
             mqtt.CONF_BROKER: "test-broker",
             mqtt.CONF_PORT: 1234,
+            "set_client_cert": True,
+            "set_ca_cert": "auto",
         }
         suggested = {
             mqtt.CONF_USERNAME: "user",
             mqtt.CONF_PASSWORD: "pass",
+            mqtt.CONF_TLS_INSECURE: True,
+            mqtt.CONF_PROTOCOL: "3.1.1",
         }
         for k, v in defaults.items():
             assert get_default(result["data_schema"].schema, k) == v
         for k, v in suggested.items():
             assert get_suggested(result["data_schema"].schema, k) == v
 
+        # test the client cert and key were migrated to the entry
+        assert config_entry.data[mqtt.CONF_CLIENT_CERT] == MOCK_CLIENT_CERT.decode(
+            "utf-8"
+        )
+        assert config_entry.data[mqtt.CONF_CLIENT_KEY] == MOCK_CLIENT_KEY.decode(
+            "utf-8"
+        )
+        assert config_entry.data[mqtt.CONF_CERTIFICATE] == "auto"
+
+        # test we can chante username and password
+        # as it was configured as auto in configuration.yaml is is migrated now
+        mock_try_connection_success.reset_mock()
         result = await hass.config_entries.options.async_configure(
             result["flow_id"],
             user_input={
@@ -828,24 +1191,135 @@ async def test_try_connection_with_advanced_parameters(
                 mqtt.CONF_PORT: 2345,
                 mqtt.CONF_USERNAME: "us3r",
                 mqtt.CONF_PASSWORD: "p4ss",
+                "set_ca_cert": "auto",
+                "set_client_cert": True,
+                mqtt.CONF_TLS_INSECURE: True,
             },
         )
         assert result["type"] == data_entry_flow.FlowResultType.FORM
+        assert result["errors"] == {}
         assert result["step_id"] == "options"
+        await hass.async_block_till_done()
 
         # check if the username and password was set from config flow and not from configuration.yaml
         assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == (
             "us3r",
             "p4ss",
         )
-
         # check if tls_insecure_set is called
         assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,)
 
-        # check if the certificate settings were set from configuration.yaml
+        # check if the ca certificate settings were not set during connection test
         assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
             "certfile"
-        ] == str(certfile)
+        ] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_CERT)
         assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[
             "keyfile"
-        ] == str(keyfile)
+        ] == mqtt.util.get_file_path(mqtt.CONF_CLIENT_KEY)
+
+        # Accept default option
+        result = await hass.config_entries.options.async_configure(
+            result["flow_id"],
+            user_input={},
+        )
+        assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+        await hass.async_block_till_done()
+
+
+async def test_setup_with_advanced_settings(
+    hass, mock_try_connection, tmp_path, mock_ssl_context, mock_process_uploaded_file
+):
+    """Test config flow setup with advanced parameters."""
+    file_id = mock_process_uploaded_file.file_id
+
+    config_entry = MockConfigEntry(domain=mqtt.DOMAIN)
+    config_entry.add_to_hass(hass)
+    config_entry.data = {
+        mqtt.CONF_BROKER: "test-broker",
+        mqtt.CONF_PORT: 1234,
+    }
+
+    mock_try_connection.return_value = True
+
+    result = await hass.config_entries.options.async_init(config_entry.entry_id)
+    assert result["type"] == "form"
+    assert result["step_id"] == "broker"
+    assert result["data_schema"].schema["advanced_options"]
+
+    # first iteration, basic settings
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            mqtt.CONF_BROKER: "test-broker",
+            mqtt.CONF_PORT: 2345,
+            mqtt.CONF_USERNAME: "user",
+            mqtt.CONF_PASSWORD: "secret",
+            "advanced_options": True,
+        },
+    )
+    assert result["type"] == "form"
+    assert result["step_id"] == "broker"
+    assert "advanced_options" not in result["data_schema"].schema
+    assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID]
+    assert result["data_schema"].schema[mqtt.CONF_KEEPALIVE]
+    assert result["data_schema"].schema["set_client_cert"]
+    assert result["data_schema"].schema["set_ca_cert"]
+    assert result["data_schema"].schema[mqtt.CONF_TLS_INSECURE]
+    assert result["data_schema"].schema[mqtt.CONF_PROTOCOL]
+    assert mqtt.CONF_CLIENT_CERT not in result["data_schema"].schema
+    assert mqtt.CONF_CLIENT_KEY not in result["data_schema"].schema
+
+    # second iteration, advanced settings with request for client cert
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            mqtt.CONF_BROKER: "test-broker",
+            mqtt.CONF_PORT: 2345,
+            mqtt.CONF_USERNAME: "user",
+            mqtt.CONF_PASSWORD: "secret",
+            mqtt.CONF_KEEPALIVE: 30,
+            "set_ca_cert": "auto",
+            "set_client_cert": True,
+            mqtt.CONF_TLS_INSECURE: True,
+        },
+    )
+    assert result["type"] == "form"
+    assert result["step_id"] == "broker"
+    assert "advanced_options" not in result["data_schema"].schema
+    assert result["data_schema"].schema[mqtt.CONF_CLIENT_ID]
+    assert result["data_schema"].schema[mqtt.CONF_KEEPALIVE]
+    assert result["data_schema"].schema["set_client_cert"]
+    assert result["data_schema"].schema["set_ca_cert"]
+    assert result["data_schema"].schema[mqtt.CONF_TLS_INSECURE]
+    assert result["data_schema"].schema[mqtt.CONF_PROTOCOL]
+    assert result["data_schema"].schema[mqtt.CONF_CLIENT_CERT]
+    assert result["data_schema"].schema[mqtt.CONF_CLIENT_KEY]
+
+    # third iteration, advanced settings with client cert and key set
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            mqtt.CONF_BROKER: "test-broker",
+            mqtt.CONF_PORT: 2345,
+            mqtt.CONF_USERNAME: "user",
+            mqtt.CONF_PASSWORD: "secret",
+            mqtt.CONF_KEEPALIVE: 30,
+            "set_ca_cert": "auto",
+            "set_client_cert": True,
+            mqtt.CONF_CLIENT_CERT: file_id[mqtt.CONF_CLIENT_CERT],
+            mqtt.CONF_CLIENT_KEY: file_id[mqtt.CONF_CLIENT_KEY],
+            mqtt.CONF_TLS_INSECURE: True,
+        },
+    )
+
+    assert result["type"] == "form"
+    assert result["step_id"] == "options"
+
+    result = await hass.config_entries.options.async_configure(
+        result["flow_id"],
+        user_input={
+            mqtt.CONF_DISCOVERY: True,
+            mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test",
+        },
+    )
+    assert result["type"] == "create_entry"
diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py
index 90df45b65a1..426ccb5806f 100644
--- a/tests/components/mqtt/test_init.py
+++ b/tests/components/mqtt/test_init.py
@@ -1940,6 +1940,7 @@ async def test_update_incomplete_entry(
     # Config entry data should now be updated
     assert entry.data == {
         "port": 1234,
+        "discovery_prefix": "homeassistant",
         "broker": "yaml_broker",
     }
     # Warnings about broker deprecated, but not about other keys with default values
@@ -2969,7 +2970,7 @@ async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog)
     mqtt_config_entry_data = {
         mqtt.CONF_BROKER: "mock-broker",
         mqtt.CONF_BIRTH_MESSAGE: {},
-        mqtt.client.CONF_PROTOCOL: mqtt.const.PROTOCOL_311,
+        "old_option": "old_value",
     }
 
     entry = MockConfigEntry(
@@ -2985,8 +2986,7 @@ async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog)
     assert mqtt.client.CONF_PROTOCOL not in entry.data
     assert (
         "The following unsupported configuration options were removed from the "
-        "MQTT config entry: {'protocol'}. Add them to configuration.yaml if they "
-        "are needed"
+        "MQTT config entry: {'old_option'}"
     ) in caplog.text
 
 
diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py
new file mode 100644
index 00000000000..a8eaba0421f
--- /dev/null
+++ b/tests/components/mqtt/test_util.py
@@ -0,0 +1,49 @@
+"""Test MQTT utils."""
+
+from random import getrandbits
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.components import mqtt
+
+
+@pytest.fixture(autouse=True)
+def mock_temp_dir():
+    """Mock the certificate temp directory."""
+    with patch(
+        # Patch temp dir name to avoid tests fail running in parallel
+        "homeassistant.components.mqtt.util.TEMP_DIR_NAME",
+        "home-assistant-mqtt" + f"-{getrandbits(10):03x}",
+    ) as mocked_temp_dir:
+        yield mocked_temp_dir
+
+
+@pytest.mark.parametrize(
+    "option,content,file_created",
+    [
+        (mqtt.CONF_CERTIFICATE, "auto", False),
+        (mqtt.CONF_CERTIFICATE, "### CA CERTIFICATE ###", True),
+        (mqtt.CONF_CLIENT_CERT, "### CLIENT CERTIFICATE ###", True),
+        (mqtt.CONF_CLIENT_KEY, "### PRIVATE KEY ###", True),
+    ],
+)
+async def test_async_create_certificate_temp_files(
+    hass, mock_temp_dir, option, content, file_created
+):
+    """Test creating and reading certificate files."""
+    config = {option: content}
+    await mqtt.util.async_create_certificate_temp_files(hass, config)
+
+    file_path = mqtt.util.get_file_path(option)
+    assert bool(file_path) is file_created
+    assert (
+        mqtt.util.migrate_certificate_file_to_content(file_path or content) == content
+    )
+
+
+async def test_reading_non_exitisting_certificate_file():
+    """Test reading a non existing certificate file."""
+    assert (
+        mqtt.util.migrate_certificate_file_to_content("/home/file_not_exists") is None
+    )
-- 
GitLab


From 26d3c34838892c246e4bcc1816959d614c2b1c04 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 24 Oct 2022 10:45:16 +0200
Subject: [PATCH 741/985] Remove precipitation note from length units (#80677)

---
 homeassistant/const.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 49749c1b5e5..90a2dd34550 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -525,15 +525,11 @@ TIME_YEARS: Final = "y"
 
 # Length units
 LENGTH_MILLIMETERS: Final = "mm"
-"""Note: for precipitation, please use PRECIPITATION_MILLIMETERS"""
-
 LENGTH_CENTIMETERS: Final = "cm"
 LENGTH_METERS: Final = "m"
 LENGTH_KILOMETERS: Final = "km"
 
 LENGTH_INCHES: Final = "in"
-"""Note: for precipitation, please use PRECIPITATION_INCHES"""
-
 LENGTH_FEET: Final = "ft"
 LENGTH_YARD: Final = "yd"
 LENGTH_MILES: Final = "mi"
-- 
GitLab


From 1f0cb73ad0e49ba4c3ebf6e98d7b554f579e4300 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
Date: Mon, 24 Oct 2022 11:47:45 +0200
Subject: [PATCH 742/985] Add `update` platform to MQTT integration (#80659)

---
 .../components/mqtt/abbreviations.py          |   3 +
 .../components/mqtt/config_integration.py     |   4 +
 homeassistant/components/mqtt/const.py        |   2 +
 homeassistant/components/mqtt/discovery.py    |   1 +
 homeassistant/components/mqtt/update.py       | 199 +++++++++
 tests/components/mqtt/test_update.py          | 393 ++++++++++++++++++
 6 files changed, 602 insertions(+)
 create mode 100644 homeassistant/components/mqtt/update.py
 create mode 100644 tests/components/mqtt/test_update.py

diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py
index 758e978bb46..67fffec1106 100644
--- a/homeassistant/components/mqtt/abbreviations.py
+++ b/homeassistant/components/mqtt/abbreviations.py
@@ -255,6 +255,9 @@ ABBREVIATIONS = {
     "xy_cmd_t": "xy_command_topic",
     "xy_stat_t": "xy_state_topic",
     "xy_val_tpl": "xy_value_template",
+    "l_ver_t": "latest_version_topic",
+    "l_ver_tpl": "latest_version_template",
+    "pl_inst": "payload_install",
 }
 
 DEVICE_ABBREVIATIONS = {
diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py
index 2b07d6f9db4..2be125c2c12 100644
--- a/homeassistant/components/mqtt/config_integration.py
+++ b/homeassistant/components/mqtt/config_integration.py
@@ -32,6 +32,7 @@ from . import (
     sensor as sensor_platform,
     siren as siren_platform,
     switch as switch_platform,
+    update as update_platform,
     vacuum as vacuum_platform,
 )
 from .const import (
@@ -128,6 +129,9 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema(
         Platform.SWITCH.value: vol.All(
             cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN]  # type: ignore[has-type]
         ),
+        Platform.UPDATE.value: vol.All(
+            cv.ensure_list, [update_platform.PLATFORM_SCHEMA_MODERN]  # type: ignore[has-type]
+        ),
         Platform.VACUUM.value: vol.All(
             cv.ensure_list, [vacuum_platform.PLATFORM_SCHEMA_MODERN]  # type: ignore[has-type]
         ),
diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py
index ff9776a01f7..1dc25c1e78c 100644
--- a/homeassistant/components/mqtt/const.py
+++ b/homeassistant/components/mqtt/const.py
@@ -91,6 +91,7 @@ PLATFORMS = [
     Platform.SENSOR,
     Platform.SIREN,
     Platform.SWITCH,
+    Platform.UPDATE,
     Platform.VACUUM,
 ]
 
@@ -111,5 +112,6 @@ RELOADABLE_PLATFORMS = [
     Platform.SENSOR,
     Platform.SIREN,
     Platform.SWITCH,
+    Platform.UPDATE,
     Platform.VACUUM,
 ]
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index 92ad50b7b4a..0aa288e700a 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -62,6 +62,7 @@ SUPPORTED_COMPONENTS = [
     "sensor",
     "switch",
     "tag",
+    "update",
     "vacuum",
 ]
 
diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py
new file mode 100644
index 00000000000..ac8b5431a59
--- /dev/null
+++ b/homeassistant/components/mqtt/update.py
@@ -0,0 +1,199 @@
+"""Configure update platform in a device through MQTT topic."""
+from __future__ import annotations
+
+from collections.abc import Callable
+import functools
+import logging
+from typing import Any
+
+import voluptuous as vol
+
+from homeassistant.components import update
+from homeassistant.components.update import (
+    DEVICE_CLASSES_SCHEMA,
+    UpdateEntity,
+    UpdateEntityFeature,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+
+from . import subscription
+from .config import DEFAULT_RETAIN, MQTT_RO_SCHEMA
+from .const import (
+    CONF_COMMAND_TOPIC,
+    CONF_ENCODING,
+    CONF_QOS,
+    CONF_RETAIN,
+    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, ReceiveMessage
+from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = "MQTT Update"
+
+CONF_LATEST_VERSION_TEMPLATE = "latest_version_template"
+CONF_LATEST_VERSION_TOPIC = "latest_version_topic"
+CONF_PAYLOAD_INSTALL = "payload_install"
+
+
+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_LATEST_VERSION_TEMPLATE): cv.template,
+        vol.Required(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_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
+    },
+).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
+
+
+DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA))
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up MQTT update through configuration.yaml and dynamically through MQTT discovery."""
+    setup = functools.partial(
+        _async_setup_entity, hass, async_add_entities, config_entry=config_entry
+    )
+    await async_setup_entry_helper(hass, update.DOMAIN, setup, DISCOVERY_SCHEMA)
+
+
+async def _async_setup_entity(
+    hass: HomeAssistant,
+    async_add_entities: AddEntitiesCallback,
+    config: ConfigType,
+    config_entry: ConfigEntry,
+    discovery_data: DiscoveryInfoType | None = None,
+) -> None:
+    """Set up the MQTT update."""
+    async_add_entities([MqttUpdate(hass, config, config_entry, discovery_data)])
+
+
+class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
+    """Representation of the MQTT update entity."""
+
+    _entity_id_format = update.ENTITY_ID_FORMAT
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        config: ConfigType,
+        config_entry: ConfigEntry,
+        discovery_data: DiscoveryInfoType | None = None,
+    ) -> None:
+        """Initialize the MQTT update."""
+        self._config = config
+        self._sub_state = None
+
+        self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
+
+        UpdateEntity.__init__(self)
+        MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
+
+    @staticmethod
+    def config_schema() -> vol.Schema:
+        """Return the config schema."""
+        return DISCOVERY_SCHEMA
+
+    def _setup_from_config(self, config: ConfigType) -> None:
+        """(Re)Setup the entity."""
+        self._templates = {
+            CONF_VALUE_TEMPLATE: MqttValueTemplate(
+                config.get(CONF_VALUE_TEMPLATE),
+                entity=self,
+            ).async_render_with_possible_json_value,
+            CONF_LATEST_VERSION_TEMPLATE: MqttValueTemplate(
+                config.get(CONF_LATEST_VERSION_TEMPLATE),
+                entity=self,
+            ).async_render_with_possible_json_value,
+        }
+
+    def _prepare_subscribe_topics(self) -> None:
+        """(Re)Subscribe to topics."""
+        topics: dict[str, Any] = {}
+
+        def add_subscription(
+            topics: dict[str, Any], topic: str, msg_callback: Callable
+        ) -> None:
+            if self._config.get(topic) is not None:
+                topics[topic] = {
+                    "topic": self._config[topic],
+                    "msg_callback": msg_callback,
+                    "qos": self._config[CONF_QOS],
+                    "encoding": self._config[CONF_ENCODING] or None,
+                }
+
+        @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)
+
+            if isinstance(installed_version, str) and installed_version != "":
+                self._attr_installed_version = installed_version
+                get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
+
+        add_subscription(topics, CONF_STATE_TOPIC, handle_installed_version_received)
+
+        @callback
+        @log_messages(self.hass, self.entity_id)
+        def handle_latest_version_received(msg: ReceiveMessage) -> None:
+            """Handle receiving latest version via MQTT."""
+            latest_version = self._templates[CONF_LATEST_VERSION_TEMPLATE](msg.payload)
+
+            if isinstance(latest_version, str) and latest_version != "":
+                self._attr_latest_version = latest_version
+                get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
+
+        add_subscription(
+            topics, CONF_LATEST_VERSION_TOPIC, handle_latest_version_received
+        )
+
+        self._sub_state = subscription.async_prepare_subscribe_topics(
+            self.hass, self._sub_state, topics
+        )
+
+    async def _subscribe_topics(self) -> None:
+        """(Re)Subscribe to topics."""
+        await subscription.async_subscribe_topics(self.hass, self._sub_state)
+
+    async def async_install(
+        self, version: str | None, backup: bool, **kwargs: Any
+    ) -> None:
+        """Update the current value."""
+        payload = self._config[CONF_PAYLOAD_INSTALL]
+
+        await self.async_publish(
+            self._config[CONF_COMMAND_TOPIC],
+            payload,
+            self._config[CONF_QOS],
+            self._config[CONF_RETAIN],
+            self._config[CONF_ENCODING],
+        )
+
+        get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
+
+    @property
+    def supported_features(self) -> int:
+        """Return the list of supported features."""
+        support = 0
+
+        if self._config.get(CONF_COMMAND_TOPIC) is not None:
+            support |= UpdateEntityFeature.INSTALL
+
+        return support
diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py
new file mode 100644
index 00000000000..9b008f093d0
--- /dev/null
+++ b/tests/components/mqtt/test_update.py
@@ -0,0 +1,393 @@
+"""The tests for mqtt update component."""
+import json
+from unittest.mock import patch
+
+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.setup import async_setup_component
+
+from .test_common import (
+    help_test_availability_when_connection_lost,
+    help_test_availability_without_topic,
+    help_test_custom_availability_payload,
+    help_test_default_availability_payload,
+    help_test_discovery_broken,
+    help_test_discovery_removal,
+    help_test_discovery_update,
+    help_test_discovery_update_attr,
+    help_test_discovery_update_unchanged,
+    help_test_entity_device_info_remove,
+    help_test_entity_device_info_update,
+    help_test_entity_device_info_with_connection,
+    help_test_entity_device_info_with_identifier,
+    help_test_entity_id_update_discovery_update,
+    help_test_setting_attribute_via_mqtt_json_message,
+    help_test_setting_attribute_with_template,
+    help_test_setup_manual_entity_from_yaml,
+    help_test_unique_id,
+    help_test_unload_config_entry_with_platform,
+    help_test_update_with_json_attrs_bad_json,
+    help_test_update_with_json_attrs_not_dict,
+)
+
+from tests.common import async_fire_mqtt_message
+
+DEFAULT_CONFIG = {
+    mqtt.DOMAIN: {
+        update.DOMAIN: {
+            "name": "test",
+            "state_topic": "test-topic",
+            "latest_version_topic": "test-topic",
+            "command_topic": "test-topic",
+            "payload_install": "install",
+        }
+    }
+}
+
+
+@pytest.fixture(autouse=True)
+def update_platform_only():
+    """Only setup the update platform to speed up tests."""
+    with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.UPDATE]):
+        yield
+
+
+async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config):
+    """Test that it fetches the given payload."""
+    installed_version_topic = "test/installed-version"
+    latest_version_topic = "test/latest-version"
+    await async_setup_component(
+        hass,
+        mqtt.DOMAIN,
+        {
+            mqtt.DOMAIN: {
+                update.DOMAIN: {
+                    "state_topic": installed_version_topic,
+                    "latest_version_topic": latest_version_topic,
+                    "name": "Test Update",
+                }
+            }
+        },
+    )
+    await hass.async_block_till_done()
+    await mqtt_mock_entry_with_yaml_config()
+
+    async_fire_mqtt_message(hass, installed_version_topic, "1.9.0")
+    async_fire_mqtt_message(hass, latest_version_topic, "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, latest_version_topic, "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_value_template(hass, mqtt_mock_entry_with_yaml_config):
+    """Test that it fetches the given payload with a template."""
+    installed_version_topic = "test/installed-version"
+    latest_version_topic = "test/latest-version"
+    await async_setup_component(
+        hass,
+        mqtt.DOMAIN,
+        {
+            mqtt.DOMAIN: {
+                update.DOMAIN: {
+                    "state_topic": installed_version_topic,
+                    "value_template": "{{ value_json.installed }}",
+                    "latest_version_topic": latest_version_topic,
+                    "latest_version_template": "{{ value_json.latest }}",
+                    "name": "Test Update",
+                }
+            }
+        },
+    )
+    await hass.async_block_till_done()
+    await mqtt_mock_entry_with_yaml_config()
+
+    async_fire_mqtt_message(hass, installed_version_topic, '{"installed":"1.9.0"}')
+    async_fire_mqtt_message(hass, latest_version_topic, '{"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, latest_version_topic, '{"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"
+    latest_version_topic = "test/latest-version"
+    command_topic = "test/install-command"
+
+    await async_setup_component(
+        hass,
+        mqtt.DOMAIN,
+        {
+            mqtt.DOMAIN: {
+                update.DOMAIN: {
+                    "state_topic": installed_version_topic,
+                    "latest_version_topic": latest_version_topic,
+                    "command_topic": command_topic,
+                    "payload_install": "install",
+                    "name": "Test Update",
+                }
+            }
+        },
+    )
+    await hass.async_block_till_done()
+    mqtt_mock = await mqtt_mock_entry_with_yaml_config()
+
+    async_fire_mqtt_message(hass, installed_version_topic, "1.9.0")
+    async_fire_mqtt_message(hass, latest_version_topic, "2.0.0")
+
+    await hass.async_block_till_done()
+
+    state = hass.states.get("update.test_update")
+    assert state.state == STATE_ON
+
+    await hass.services.async_call(
+        UPDATE_DOMAIN,
+        SERVICE_INSTALL,
+        {ATTR_ENTITY_ID: "update.test_update"},
+        blocking=True,
+    )
+
+    mqtt_mock.async_publish.assert_called_once_with(command_topic, "install", 0, False)
+
+
+async def test_availability_when_connection_lost(
+    hass, mqtt_mock_entry_with_yaml_config
+):
+    """Test availability after MQTT disconnection."""
+    await help_test_availability_when_connection_lost(
+        hass, mqtt_mock_entry_with_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config):
+    """Test availability without defined availability topic."""
+    await help_test_availability_without_topic(
+        hass, mqtt_mock_entry_with_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config):
+    """Test availability by default payload with defined topic."""
+    await help_test_default_availability_payload(
+        hass, mqtt_mock_entry_with_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config):
+    """Test availability by custom payload with defined topic."""
+    await help_test_custom_availability_payload(
+        hass, mqtt_mock_entry_with_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_setting_attribute_via_mqtt_json_message(
+    hass, mqtt_mock_entry_with_yaml_config
+):
+    """Test the setting of attribute via MQTT with JSON payload."""
+    await help_test_setting_attribute_via_mqtt_json_message(
+        hass, mqtt_mock_entry_with_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config):
+    """Test the setting of attribute via MQTT with JSON payload."""
+    await help_test_setting_attribute_with_template(
+        hass, mqtt_mock_entry_with_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_update_with_json_attrs_not_dict(
+    hass, mqtt_mock_entry_with_yaml_config, caplog
+):
+    """Test attributes get extracted from a JSON result."""
+    await help_test_update_with_json_attrs_not_dict(
+        hass,
+        mqtt_mock_entry_with_yaml_config,
+        caplog,
+        update.DOMAIN,
+        DEFAULT_CONFIG,
+    )
+
+
+async def test_update_with_json_attrs_bad_json(
+    hass, mqtt_mock_entry_with_yaml_config, caplog
+):
+    """Test attributes get extracted from a JSON result."""
+    await help_test_update_with_json_attrs_bad_json(
+        hass,
+        mqtt_mock_entry_with_yaml_config,
+        caplog,
+        update.DOMAIN,
+        DEFAULT_CONFIG,
+    )
+
+
+async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog):
+    """Test update of discovered MQTTAttributes."""
+    await help_test_discovery_update_attr(
+        hass,
+        mqtt_mock_entry_no_yaml_config,
+        caplog,
+        update.DOMAIN,
+        DEFAULT_CONFIG,
+    )
+
+
+async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config):
+    """Test unique id option only creates one update per unique_id."""
+    config = {
+        mqtt.DOMAIN: {
+            update.DOMAIN: [
+                {
+                    "name": "Bear",
+                    "state_topic": "installed-topic",
+                    "latest_version_topic": "latest-topic",
+                    "unique_id": "TOTALLY_UNIQUE",
+                },
+                {
+                    "name": "Milk",
+                    "state_topic": "installed-topic",
+                    "latest_version_topic": "latest-topic",
+                    "unique_id": "TOTALLY_UNIQUE",
+                },
+            ]
+        }
+    }
+    await help_test_unique_id(
+        hass, mqtt_mock_entry_with_yaml_config, update.DOMAIN, config
+    )
+
+
+async def test_discovery_removal_update(hass, mqtt_mock_entry_no_yaml_config, caplog):
+    """Test removal of discovered update."""
+    data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][update.DOMAIN])
+    await help_test_discovery_removal(
+        hass, mqtt_mock_entry_no_yaml_config, caplog, update.DOMAIN, data
+    )
+
+
+async def test_discovery_update_update(hass, mqtt_mock_entry_no_yaml_config, caplog):
+    """Test update of discovered update."""
+    config1 = {
+        "name": "Beer",
+        "state_topic": "installed-topic",
+        "latest_version_topic": "latest-topic",
+    }
+    config2 = {
+        "name": "Milk",
+        "state_topic": "installed-topic",
+        "latest_version_topic": "latest-topic",
+    }
+
+    await help_test_discovery_update(
+        hass, mqtt_mock_entry_no_yaml_config, caplog, update.DOMAIN, config1, config2
+    )
+
+
+async def test_discovery_update_unchanged_update(
+    hass, mqtt_mock_entry_no_yaml_config, caplog
+):
+    """Test update of discovered update."""
+    data1 = '{ "name": "Beer", "state_topic": "installed-topic", "latest_version_topic": "latest-topic"}'
+    with patch(
+        "homeassistant.components.mqtt.update.MqttUpdate.discovery_update"
+    ) as discovery_update:
+        await help_test_discovery_update_unchanged(
+            hass,
+            mqtt_mock_entry_no_yaml_config,
+            caplog,
+            update.DOMAIN,
+            data1,
+            discovery_update,
+        )
+
+
+@pytest.mark.no_fail_on_log_exception
+async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog):
+    """Test handling of bad discovery message."""
+    data1 = '{ "name": "Beer" }'
+    data2 = '{ "name": "Milk", "state_topic": "installed-topic", "latest_version_topic": "latest-topic" }'
+
+    await help_test_discovery_broken(
+        hass, mqtt_mock_entry_no_yaml_config, caplog, update.DOMAIN, data1, data2
+    )
+
+
+async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config):
+    """Test MQTT update device registry integration."""
+    await help_test_entity_device_info_with_connection(
+        hass, mqtt_mock_entry_no_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config):
+    """Test MQTT update device registry integration."""
+    await help_test_entity_device_info_with_identifier(
+        hass, mqtt_mock_entry_no_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config):
+    """Test device registry update."""
+    await help_test_entity_device_info_update(
+        hass, mqtt_mock_entry_no_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config):
+    """Test device registry remove."""
+    await help_test_entity_device_info_remove(
+        hass, mqtt_mock_entry_no_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config):
+    """Test MQTT discovery update when entity_id is updated."""
+    await help_test_entity_id_update_discovery_update(
+        hass, mqtt_mock_entry_no_yaml_config, update.DOMAIN, DEFAULT_CONFIG
+    )
+
+
+async def test_setup_manual_entity_from_yaml(hass):
+    """Test setup manual configured MQTT entity."""
+    platform = update.DOMAIN
+    await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG)
+    assert hass.states.get(f"{platform}.test")
+
+
+async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
+    """Test unloading the config entry."""
+    domain = update.DOMAIN
+    config = DEFAULT_CONFIG
+    await help_test_unload_config_entry_with_platform(
+        hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
+    )
-- 
GitLab


From f7982a0db2dd8bde98aec22b440a080a77422902 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 24 Oct 2022 12:18:27 +0200
Subject: [PATCH 743/985] CI: Fix partial coverage (#80877)

* CI: Split coverage job

* Don't wait for pytest-mariadb
---
 .github/workflows/ci.yaml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 1651795c633..fc0ca593fbd 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -970,7 +970,6 @@ jobs:
     needs:
       - info
       - pytest
-      - pytest-mariadb
     steps:
       - name: Check out code from GitHub
         uses: actions/checkout@v3.1.0
-- 
GitLab


From ebfb10c177e8c940b0a51b5a8653c36c7b2d96c4 Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
Date: Mon, 24 Oct 2022 14:17:53 +0200
Subject: [PATCH 744/985] Allow hostname for nibe heatpump (#80793)

Allow hostname for nibe
---
 homeassistant/components/nibe_heatpump/config_flow.py      | 7 +++----
 homeassistant/components/nibe_heatpump/strings.json        | 4 ++--
 .../components/nibe_heatpump/translations/en.json          | 7 ++-----
 tests/components/nibe_heatpump/test_config_flow.py         | 5 +++--
 4 files changed, 10 insertions(+), 13 deletions(-)

diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py
index ba3325d9daf..bd48583547c 100644
--- a/homeassistant/components/nibe_heatpump/config_flow.py
+++ b/homeassistant/components/nibe_heatpump/config_flow.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import errno
+from socket import gaierror
 from typing import Any
 
 from nibe.connection.nibegw import NibeGW
@@ -14,7 +15,6 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL
 from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers import config_validation as cv
-from homeassistant.util.network import is_ipv4_address
 
 from .const import (
     CONF_CONNECTION_TYPE,
@@ -51,9 +51,6 @@ class FieldError(Exception):
 async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
     """Validate the user input allows us to connect."""
 
-    if not is_ipv4_address(data[CONF_IP_ADDRESS]):
-        raise FieldError("Not a valid ipv4 address", CONF_IP_ADDRESS, "address")
-
     heatpump = HeatPump(Model[data[CONF_MODEL]])
     heatpump.initialize()
 
@@ -79,6 +76,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
         coil = await connection.read_coil(coil)
         word_swap = coil.value == "ON"
         coil = await connection.write_coil(coil)
+    except gaierror as exception:
+        raise FieldError(str(exception), "ip_address", "address") from exception
     except CoilNotFoundException as exception:
         raise FieldError(
             "Model selected doesn't seem to support expected coils", "base", "model"
diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json
index 45e55b61083..9bbee6ae2ee 100644
--- a/homeassistant/components/nibe_heatpump/strings.json
+++ b/homeassistant/components/nibe_heatpump/strings.json
@@ -3,7 +3,7 @@
     "step": {
       "user": {
         "data": {
-          "ip_address": "Remote IP address",
+          "ip_address": "Remote address",
           "remote_read_port": "Remote read port",
           "remote_write_port": "Remote write port",
           "listening_port": "Local listening port"
@@ -13,7 +13,7 @@
     "error": {
       "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`.",
       "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.",
-      "address": "Invalid remote IP address specified. Address must be a IPV4 address.",
+      "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.",
       "address_in_use": "The selected listening port is already in use on this system.",
       "model": "The model selected doesn't seem to support modbus40",
       "unknown": "[%key:common::config_flow::error::unknown%]"
diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json
index 74dd8313e95..3837989511f 100644
--- a/homeassistant/components/nibe_heatpump/translations/en.json
+++ b/homeassistant/components/nibe_heatpump/translations/en.json
@@ -1,10 +1,7 @@
 {
     "config": {
-        "abort": {
-            "already_configured": "Device is already configured"
-        },
         "error": {
-            "address": "Invalid remote IP address specified. Address must be a IPV4 address.",
+            "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.",
             "address_in_use": "The selected listening port is already in use on this system.",
             "model": "The model selected doesn't seem to support modbus40",
             "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.",
@@ -14,7 +11,7 @@
         "step": {
             "user": {
                 "data": {
-                    "ip_address": "Remote IP address",
+                    "ip_address": "Remote address",
                     "listening_port": "Local listening port",
                     "remote_read_port": "Remote read port",
                     "remote_write_port": "Remote write port"
diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py
index 2647102ba5a..f7dc08c41bb 100644
--- a/tests/components/nibe_heatpump/test_config_flow.py
+++ b/tests/components/nibe_heatpump/test_config_flow.py
@@ -1,5 +1,6 @@
 """Test the Nibe Heat Pump config flow."""
 import errno
+from socket import gaierror
 from unittest.mock import Mock, patch
 
 from nibe.coil import Coil
@@ -150,13 +151,13 @@ async def test_unexpected_exception(hass: HomeAssistant, mock_connection: Mock)
     assert result2["errors"] == {"base": "unknown"}
 
 
-async def test_invalid_ip(hass: HomeAssistant, mock_connection: Mock) -> None:
+async def test_invalid_host(hass: HomeAssistant, mock_connection: Mock) -> None:
     """Test we handle cannot connect error."""
     result = await hass.config_entries.flow.async_init(
         DOMAIN, context={"source": config_entries.SOURCE_USER}
     )
 
-    mock_connection.return_value.read_coil.side_effect = Exception()
+    mock_connection.return_value.read_coil.side_effect = gaierror()
 
     result2 = await hass.config_entries.flow.async_configure(
         result["flow_id"], {**MOCK_FLOW_USERDATA, "ip_address": "abcd"}
-- 
GitLab


From 64d6d04ade1ae851e1bd1fe63f89ae62c0de55ab Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 24 Oct 2022 14:55:57 +0200
Subject: [PATCH 745/985] Use DataUpdateCoordinator in scrape (#80593)

* Add DataUpdateCoordinator to scrape

* Fix tests
---
 .../components/scrape/coordinator.py          | 36 ++++++++++++++
 homeassistant/components/scrape/sensor.py     | 49 +++++++++----------
 tests/components/scrape/test_sensor.py        | 15 ++++--
 3 files changed, 70 insertions(+), 30 deletions(-)
 create mode 100644 homeassistant/components/scrape/coordinator.py

diff --git a/homeassistant/components/scrape/coordinator.py b/homeassistant/components/scrape/coordinator.py
new file mode 100644
index 00000000000..3e81ba798ae
--- /dev/null
+++ b/homeassistant/components/scrape/coordinator.py
@@ -0,0 +1,36 @@
+"""Coordinator for the scrape component."""
+from __future__ import annotations
+
+from datetime import timedelta
+import logging
+
+from bs4 import BeautifulSoup
+
+from homeassistant.components.rest.data import RestData
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ScrapeCoordinator(DataUpdateCoordinator[BeautifulSoup]):
+    """Scrape Coordinator."""
+
+    def __init__(
+        self, hass: HomeAssistant, rest: RestData, update_interval: timedelta
+    ) -> None:
+        """Initialize Scrape coordinator."""
+        super().__init__(
+            hass,
+            _LOGGER,
+            name="Scrape Coordinator",
+            update_interval=update_interval,
+        )
+        self._rest = rest
+
+    async def _async_update_data(self) -> BeautifulSoup:
+        """Fetch data from Rest."""
+        await self._rest.async_update()
+        if (data := self._rest.data) is None:
+            raise UpdateFailed("REST data is not available")
+        return await self.hass.async_add_executor_job(BeautifulSoup, data, "lxml")
diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index 1f696d73007..c2ba370a660 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -5,7 +5,6 @@ from datetime import timedelta
 import logging
 from typing import Any
 
-from bs4 import BeautifulSoup
 import httpx
 import voluptuous as vol
 
@@ -31,12 +30,15 @@ from homeassistant.const import (
     HTTP_BASIC_AUTHENTICATION,
     HTTP_DIGEST_AUTHENTICATION,
 )
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import PlatformNotReady
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.template import Template
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .coordinator import ScrapeCoordinator
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -105,15 +107,16 @@ async def async_setup_platform(
             auth = (username, password)
 
     rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl)
-    await rest.async_update()
 
-    if rest.data is None:
+    coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL)
+    await coordinator.async_refresh()
+    if coordinator.data is None:
         raise PlatformNotReady
 
     async_add_entities(
         [
             ScrapeSensor(
-                rest,
+                coordinator,
                 name,
                 select,
                 attr,
@@ -124,16 +127,15 @@ async def async_setup_platform(
                 state_class,
             )
         ],
-        True,
     )
 
 
-class ScrapeSensor(SensorEntity):
+class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], SensorEntity):
     """Representation of a web scrape sensor."""
 
     def __init__(
         self,
-        rest: RestData,
+        coordinator: ScrapeCoordinator,
         name: str,
         select: str | None,
         attr: str | None,
@@ -144,7 +146,7 @@ class ScrapeSensor(SensorEntity):
         state_class: str | None,
     ) -> None:
         """Initialize a web scrape sensor."""
-        self.rest = rest
+        super().__init__(coordinator)
         self._attr_native_value = None
         self._select = select
         self._attr = attr
@@ -157,9 +159,8 @@ class ScrapeSensor(SensorEntity):
 
     def _extract_value(self) -> Any:
         """Parse the html extraction in the executor."""
-        raw_data = BeautifulSoup(self.rest.data, "lxml")
-        _LOGGER.debug(raw_data)
-
+        raw_data = self.coordinator.data
+        _LOGGER.debug("Raw beautiful soup: %s", raw_data)
         try:
             if self._attr is not None:
                 value = raw_data.select(self._select)[self._index][self._attr]
@@ -177,25 +178,17 @@ class ScrapeSensor(SensorEntity):
                 "Attribute '%s' not found in %s", self._attr, self.entity_id
             )
             value = None
-        _LOGGER.debug(value)
+        _LOGGER.debug("Parsed value: %s", value)
         return value
 
-    async def async_update(self) -> None:
-        """Get the latest data from the source and updates the state."""
-        await self.rest.async_update()
-        await self._async_update_from_rest_data()
-
     async def async_added_to_hass(self) -> None:
         """Ensure the data from the initial update is reflected in the state."""
-        await self._async_update_from_rest_data()
+        await super().async_added_to_hass()
+        self._async_update_from_rest_data()
 
-    async def _async_update_from_rest_data(self) -> None:
+    def _async_update_from_rest_data(self) -> None:
         """Update state from the rest data."""
-        if self.rest.data is None:
-            _LOGGER.error("Unable to retrieve data for %s", self.name)
-            return
-
-        value = await self.hass.async_add_executor_job(self._extract_value)
+        value = self._extract_value()
 
         if self._value_template is not None:
             self._attr_native_value = (
@@ -203,3 +196,9 @@ class ScrapeSensor(SensorEntity):
             )
         else:
             self._attr_native_value = value
+
+    @callback
+    def _handle_coordinator_update(self) -> None:
+        """Handle updated data from the coordinator."""
+        self._async_update_from_rest_data()
+        super()._handle_coordinator_update()
diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py
index d8da22aada1..3ad19ed25af 100644
--- a/tests/components/scrape/test_sensor.py
+++ b/tests/components/scrape/test_sensor.py
@@ -1,8 +1,10 @@
 """The tests for the Scrape sensor platform."""
 from __future__ import annotations
 
+from datetime import datetime
 from unittest.mock import patch
 
+from homeassistant.components.scrape.sensor import SCAN_INTERVAL
 from homeassistant.components.sensor import (
     CONF_STATE_CLASS,
     SensorDeviceClass,
@@ -11,15 +13,17 @@ from homeassistant.components.sensor import (
 from homeassistant.const import (
     CONF_DEVICE_CLASS,
     CONF_UNIT_OF_MEASUREMENT,
+    STATE_UNAVAILABLE,
     STATE_UNKNOWN,
     TEMP_CELSIUS,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_component import async_update_entity
 from homeassistant.setup import async_setup_component
 
 from . import MockRestData, return_config
 
+from tests.common import async_fire_time_changed
+
 DOMAIN = "scrape"
 
 
@@ -155,12 +159,13 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None:
     assert state
     assert state.state == "Current Version: 2021.12.10"
 
-    mocker.data = None
-    await async_update_entity(hass, "sensor.ha_version")
+    mocker.payload = "test_scrape_sensor_no_data"
+    async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL)
+    await hass.async_block_till_done()
 
-    assert mocker.data is None
+    state = hass.states.get("sensor.ha_version")
     assert state is not None
-    assert state.state == "Current Version: 2021.12.10"
+    assert state.state == STATE_UNAVAILABLE
 
 
 async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None:
-- 
GitLab


From 2f1138562720cd50343d2fedd4981913a9ef6bd9 Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Mon, 24 Oct 2022 15:00:37 +0200
Subject: [PATCH 746/985] Add typing hints for MQTT mixins (#80702)

* Add typing hints for MQTT mixins

* Follow up comments

* config_entry is always set

* typing discovery_data - substate None assignment

* Rename `config[CONF_DEVICE]` -> specifications
---
 homeassistant/components/mqtt/cover.py     |  14 +-
 homeassistant/components/mqtt/discovery.py |  13 +-
 homeassistant/components/mqtt/mixins.py    | 205 ++++++++++++---------
 homeassistant/components/mqtt/models.py    |   4 +-
 homeassistant/components/mqtt/update.py    |   2 -
 5 files changed, 132 insertions(+), 106 deletions(-)

diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py
index 11901f15054..7d7d4f61c4a 100644
--- a/homeassistant/components/mqtt/cover.py
+++ b/homeassistant/components/mqtt/cover.py
@@ -552,7 +552,7 @@ class MqttCover(MqttEntity, CoverEntity):
         This method is a coroutine.
         """
         await self.async_publish(
-            self._config.get(CONF_COMMAND_TOPIC),
+            self._config[CONF_COMMAND_TOPIC],
             self._config[CONF_PAYLOAD_OPEN],
             self._config[CONF_QOS],
             self._config[CONF_RETAIN],
@@ -573,7 +573,7 @@ class MqttCover(MqttEntity, CoverEntity):
         This method is a coroutine.
         """
         await self.async_publish(
-            self._config.get(CONF_COMMAND_TOPIC),
+            self._config[CONF_COMMAND_TOPIC],
             self._config[CONF_PAYLOAD_CLOSE],
             self._config[CONF_QOS],
             self._config[CONF_RETAIN],
@@ -594,7 +594,7 @@ class MqttCover(MqttEntity, CoverEntity):
         This method is a coroutine.
         """
         await self.async_publish(
-            self._config.get(CONF_COMMAND_TOPIC),
+            self._config[CONF_COMMAND_TOPIC],
             self._config[CONF_PAYLOAD_STOP],
             self._config[CONF_QOS],
             self._config[CONF_RETAIN],
@@ -614,7 +614,7 @@ class MqttCover(MqttEntity, CoverEntity):
         }
         tilt_payload = self._set_tilt_template(tilt_open_position, variables=variables)
         await self.async_publish(
-            self._config.get(CONF_TILT_COMMAND_TOPIC),
+            self._config[CONF_TILT_COMMAND_TOPIC],
             tilt_payload,
             self._config[CONF_QOS],
             self._config[CONF_RETAIN],
@@ -641,7 +641,7 @@ class MqttCover(MqttEntity, CoverEntity):
             tilt_closed_position, variables=variables
         )
         await self.async_publish(
-            self._config.get(CONF_TILT_COMMAND_TOPIC),
+            self._config[CONF_TILT_COMMAND_TOPIC],
             tilt_payload,
             self._config[CONF_QOS],
             self._config[CONF_RETAIN],
@@ -670,7 +670,7 @@ class MqttCover(MqttEntity, CoverEntity):
         tilt = self._set_tilt_template(tilt, variables=variables)
 
         await self.async_publish(
-            self._config.get(CONF_TILT_COMMAND_TOPIC),
+            self._config[CONF_TILT_COMMAND_TOPIC],
             tilt,
             self._config[CONF_QOS],
             self._config[CONF_RETAIN],
@@ -697,7 +697,7 @@ class MqttCover(MqttEntity, CoverEntity):
         position = self._set_position_template(position, variables=variables)
 
         await self.async_publish(
-            self._config.get(CONF_SET_POSITION_TOPIC),
+            self._config[CONF_SET_POSITION_TOPIC],
             position,
             self._config[CONF_QOS],
             self._config[CONF_RETAIN],
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index 0aa288e700a..84f14d26146 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -7,6 +7,7 @@ import functools
 import logging
 import re
 import time
+from typing import Any
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
@@ -19,7 +20,7 @@ from homeassistant.helpers.dispatcher import (
 )
 from homeassistant.helpers.json import json_loads
 from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
-from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
+from homeassistant.helpers.typing import DiscoveryInfoType
 from homeassistant.loader import async_get_mqtt
 
 from .. import mqtt
@@ -73,8 +74,8 @@ MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}"
 TOPIC_BASE = "~"
 
 
-class MQTTConfig(dict):
-    """Dummy class to allow adding attributes."""
+class MQTTDiscoveryPayload(dict[str, Any]):
+    """Class to hold and MQTT discovery payload and discovery data."""
 
     discovery_data: DiscoveryInfoType
 
@@ -96,7 +97,7 @@ async def async_start(  # noqa: C901
     mqtt_data = get_mqtt_data(hass)
     mqtt_integrations = {}
 
-    async def async_discovery_message_received(msg):
+    async def async_discovery_message_received(msg) -> None:
         """Process the received message."""
         mqtt_data.last_discovery = time.time()
         payload = msg.payload
@@ -126,7 +127,7 @@ async def async_start(  # noqa: C901
                 _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload)
                 return
 
-        payload = MQTTConfig(payload)
+        payload = MQTTDiscoveryPayload(payload)
 
         for key in list(payload):
             abbreviated_key = key
@@ -195,7 +196,7 @@ async def async_start(  # noqa: C901
         await async_process_discovery_payload(component, discovery_id, payload)
 
     async def async_process_discovery_payload(
-        component: str, discovery_id: str, payload: ConfigType
+        component: str, discovery_id: str, payload: MQTTDiscoveryPayload
     ) -> None:
         """Process the payload of a new discovery."""
 
diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py
index b5c870a196e..7866e3cf6d6 100644
--- a/homeassistant/components/mqtt/mixins.py
+++ b/homeassistant/components/mqtt/mixins.py
@@ -34,7 +34,10 @@ from homeassistant.helpers import (
     device_registry as dr,
     entity_registry as er,
 )
-from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
+from homeassistant.helpers.device_registry import (
+    EVENT_DEVICE_REGISTRY_UPDATED,
+    DeviceEntry,
+)
 from homeassistant.helpers.dispatcher import (
     async_dispatcher_connect,
     async_dispatcher_send,
@@ -50,6 +53,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.event import async_track_entity_registry_updated_event
 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.helpers.json import json_loads
+from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 from . import debug_info, subscription
@@ -74,11 +78,13 @@ from .discovery import (
     MQTT_DISCOVERY_DONE,
     MQTT_DISCOVERY_NEW,
     MQTT_DISCOVERY_UPDATED,
+    MQTTDiscoveryPayload,
     clear_discovery_hash,
     set_discovery_hash,
 )
 from .models import MqttValueTemplate, PublishPayloadType, ReceiveMessage
 from .subscription import (
+    EntitySubscription,
     async_prepare_subscribe_topics,
     async_subscribe_topics,
     async_unsubscribe_topics,
@@ -222,7 +228,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend(
 )
 
 
-def warn_for_legacy_schema(domain: str) -> Callable:
+def warn_for_legacy_schema(domain: str) -> Callable[[ConfigType], ConfigType]:
     """Warn once when a legacy platform schema is used."""
     warned = set()
 
@@ -269,8 +275,8 @@ class SetupEntity(Protocol):
         hass: HomeAssistant,
         async_add_entities: AddEntitiesCallback,
         config: ConfigType,
-        config_entry: ConfigEntry | None = None,
-        discovery_data: dict[str, Any] | None = None,
+        config_entry: ConfigEntry,
+        discovery_data: DiscoveryInfoType | None = None,
     ) -> None:
         """Define setup_entities type."""
 
@@ -294,13 +300,13 @@ async def async_get_platform_config_from_yaml(
 async def async_setup_entry_helper(
     hass: HomeAssistant,
     domain: str,
-    async_setup: partial[Coroutine[HomeAssistant, str, None]],
+    async_setup: partial[Coroutine[Any, Any, None]],
     discovery_schema: vol.Schema,
 ) -> None:
     """Set up entity, automation or tag creation dynamically through MQTT discovery."""
     mqtt_data = get_mqtt_data(hass)
 
-    async def async_discover(discovery_payload):
+    async def async_discover(discovery_payload: MQTTDiscoveryPayload) -> None:
         """Discover and add an MQTT entity, automation or tag."""
         if not mqtt_config_entry_enabled(hass):
             _LOGGER.warning(
@@ -312,10 +318,10 @@ async def async_setup_entry_helper(
             return
         discovery_data = discovery_payload.discovery_data
         try:
-            config = discovery_schema(discovery_payload)
+            config: DiscoveryInfoType = discovery_schema(discovery_payload)
             await async_setup(config, discovery_data=discovery_data)
         except Exception:
-            discovery_hash = discovery_data[ATTR_DISCOVERY_HASH]
+            discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH]
             clear_discovery_hash(hass, discovery_hash)
             async_dispatcher_send(
                 hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None
@@ -357,7 +363,7 @@ async def async_setup_entry_helper(
 async def async_setup_platform_helper(
     hass: HomeAssistant,
     platform_domain: str,
-    config: ConfigType | DiscoveryInfoType,
+    config: ConfigType,
     async_add_entities: AddEntitiesCallback,
     async_setup_entities: SetupEntity,
 ) -> None:
@@ -381,7 +387,9 @@ async def async_setup_platform_helper(
     await async_setup_entities(hass, async_add_entities, config, config_entry)
 
 
-def init_entity_id_from_config(hass, entity, config, entity_id_format):
+def init_entity_id_from_config(
+    hass: HomeAssistant, entity: Entity, config: ConfigType, entity_id_format: str
+) -> None:
     """Set entity_id from object_id if defined in config."""
     if CONF_OBJECT_ID in config:
         entity.entity_id = async_generate_entity_id(
@@ -394,10 +402,10 @@ class MqttAttributes(Entity):
 
     _attributes_extra_blocked: frozenset[str] = frozenset()
 
-    def __init__(self, config: dict) -> None:
+    def __init__(self, config: ConfigType) -> None:
         """Initialize the JSON attributes mixin."""
         self._attributes: dict[str, Any] | None = None
-        self._attributes_sub_state = None
+        self._attributes_sub_state: dict[str, EntitySubscription] = {}
         self._attributes_config = config
 
     async def async_added_to_hass(self) -> None:
@@ -406,16 +414,16 @@ class MqttAttributes(Entity):
         self._attributes_prepare_subscribe_topics()
         await self._attributes_subscribe_topics()
 
-    def attributes_prepare_discovery_update(self, config: dict):
+    def attributes_prepare_discovery_update(self, config: DiscoveryInfoType) -> None:
         """Handle updated discovery message."""
         self._attributes_config = config
         self._attributes_prepare_subscribe_topics()
 
-    async def attributes_discovery_update(self, config: dict):
+    async def attributes_discovery_update(self, config: DiscoveryInfoType) -> None:
         """Handle updated discovery message."""
         await self._attributes_subscribe_topics()
 
-    def _attributes_prepare_subscribe_topics(self):
+    def _attributes_prepare_subscribe_topics(self) -> None:
         """(Re)Subscribe to topics."""
         attr_tpl = MqttValueTemplate(
             self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE), entity=self
@@ -458,11 +466,11 @@ class MqttAttributes(Entity):
             },
         )
 
-    async def _attributes_subscribe_topics(self):
+    async def _attributes_subscribe_topics(self) -> None:
         """(Re)Subscribe to topics."""
         await async_subscribe_topics(self.hass, self._attributes_sub_state)
 
-    async def async_will_remove_from_hass(self):
+    async def async_will_remove_from_hass(self) -> None:
         """Unsubscribe when removed."""
         self._attributes_sub_state = async_unsubscribe_topics(
             self.hass, self._attributes_sub_state
@@ -477,11 +485,11 @@ class MqttAttributes(Entity):
 class MqttAvailability(Entity):
     """Mixin used for platforms that report availability."""
 
-    def __init__(self, config: dict) -> None:
+    def __init__(self, config: ConfigType) -> None:
         """Initialize the availability mixin."""
-        self._availability_sub_state = None
-        self._available: dict = {}
-        self._available_latest = False
+        self._availability_sub_state: dict[str, EntitySubscription] = {}
+        self._available: dict[str, str | bool] = {}
+        self._available_latest: bool = False
         self._availability_setup_from_config(config)
 
     async def async_added_to_hass(self) -> None:
@@ -498,18 +506,18 @@ class MqttAvailability(Entity):
             )
         )
 
-    def availability_prepare_discovery_update(self, config: dict):
+    def availability_prepare_discovery_update(self, config: DiscoveryInfoType) -> None:
         """Handle updated discovery message."""
         self._availability_setup_from_config(config)
         self._availability_prepare_subscribe_topics()
 
-    async def availability_discovery_update(self, config: dict):
+    async def availability_discovery_update(self, config: DiscoveryInfoType) -> None:
         """Handle updated discovery message."""
         await self._availability_subscribe_topics()
 
-    def _availability_setup_from_config(self, config):
+    def _availability_setup_from_config(self, config: ConfigType) -> None:
         """(Re)Setup."""
-        self._avail_topics = {}
+        self._avail_topics: dict[str, dict[str, Any]] = {}
         if CONF_AVAILABILITY_TOPIC in config:
             self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = {
                 CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE],
@@ -518,6 +526,7 @@ class MqttAvailability(Entity):
             }
 
         if CONF_AVAILABILITY in config:
+            avail: dict[str, Any]
             for avail in config[CONF_AVAILABILITY]:
                 self._avail_topics[avail[CONF_TOPIC]] = {
                     CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE],
@@ -533,7 +542,7 @@ class MqttAvailability(Entity):
 
         self._avail_config = config
 
-    def _availability_prepare_subscribe_topics(self):
+    def _availability_prepare_subscribe_topics(self) -> None:
         """(Re)Subscribe to topics."""
 
         @callback
@@ -541,6 +550,7 @@ class MqttAvailability(Entity):
         def availability_message_received(msg: ReceiveMessage) -> None:
             """Handle a new received MQTT availability message."""
             topic = msg.topic
+            payload: ReceivePayloadType
             payload = self._avail_topics[topic][CONF_AVAILABILITY_TEMPLATE](msg.payload)
             if payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]:
                 self._available[topic] = True
@@ -555,7 +565,7 @@ class MqttAvailability(Entity):
             topic: (self._available[topic] if topic in self._available else False)
             for topic in self._avail_topics
         }
-        topics = {
+        topics: dict[str, dict[str, Any]] = {
             f"availability_{topic}": {
                 "topic": topic,
                 "msg_callback": availability_message_received,
@@ -571,17 +581,17 @@ class MqttAvailability(Entity):
             topics,
         )
 
-    async def _availability_subscribe_topics(self):
+    async def _availability_subscribe_topics(self) -> None:
         """(Re)Subscribe to topics."""
         await async_subscribe_topics(self.hass, self._availability_sub_state)
 
     @callback
-    def async_mqtt_connect(self):
+    def async_mqtt_connect(self) -> None:
         """Update state on connection/disconnection to MQTT broker."""
         if not self.hass.is_stopping:
             self.async_write_ha_state()
 
-    async def async_will_remove_from_hass(self):
+    async def async_will_remove_from_hass(self) -> None:
         """Unsubscribe when removed."""
         self._availability_sub_state = async_unsubscribe_topics(
             self.hass, self._availability_sub_state
@@ -628,12 +638,12 @@ async def cleanup_device_registry(
         )
 
 
-def get_discovery_hash(discovery_data: dict) -> tuple[str, str]:
+def get_discovery_hash(discovery_data: DiscoveryInfoType) -> tuple[str, str]:
     """Get the discovery hash from the discovery data."""
     return discovery_data[ATTR_DISCOVERY_HASH]
 
 
-def send_discovery_done(hass: HomeAssistant, discovery_data: dict) -> None:
+def send_discovery_done(hass: HomeAssistant, discovery_data: DiscoveryInfoType) -> None:
     """Acknowledge a discovery message has been handled."""
     discovery_hash = get_discovery_hash(discovery_data)
     async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None)
@@ -641,7 +651,7 @@ def send_discovery_done(hass: HomeAssistant, discovery_data: dict) -> None:
 
 def stop_discovery_updates(
     hass: HomeAssistant,
-    discovery_data: dict,
+    discovery_data: DiscoveryInfoType,
     remove_discovery_updated: Callable[[], None] | None = None,
 ) -> None:
     """Stop discovery updates of being sent."""
@@ -660,7 +670,7 @@ async def async_remove_discovery_payload(hass: HomeAssistant, discovery_data: di
 
 async def async_clear_discovery_topic_if_entity_removed(
     hass: HomeAssistant,
-    discovery_data: dict[str, Any],
+    discovery_data: DiscoveryInfoType,
     event: Event,
 ) -> None:
     """Clear the discovery topic if the entity is removed."""
@@ -675,7 +685,7 @@ class MqttDiscoveryDeviceUpdate:
     def __init__(
         self,
         hass: HomeAssistant,
-        discovery_data: dict,
+        discovery_data: DiscoveryInfoType,
         device_id: str | None,
         config_entry: ConfigEntry,
         log_name: str,
@@ -718,7 +728,7 @@ class MqttDiscoveryDeviceUpdate:
 
     async def async_discovery_update(
         self,
-        discovery_payload: DiscoveryInfoType | None,
+        discovery_payload: MQTTDiscoveryPayload,
     ) -> None:
         """Handle discovery update."""
         discovery_hash = get_discovery_hash(self._discovery_data)
@@ -789,7 +799,7 @@ class MqttDiscoveryDeviceUpdate:
                 self.hass, self._device_id, self._config_entry_id
             )
 
-    async def async_update(self, discovery_data: dict) -> None:
+    async def async_update(self, discovery_data: MQTTDiscoveryPayload) -> None:
         """Handle the update of platform specific parts, extend to the platform."""
 
     @abstractmethod
@@ -803,8 +813,9 @@ class MqttDiscoveryUpdate(Entity):
     def __init__(
         self,
         hass: HomeAssistant,
-        discovery_data: dict | None,
-        discovery_update: Callable | None = None,
+        discovery_data: DiscoveryInfoType | None,
+        discovery_update: Callable[[MQTTDiscoveryPayload], Coroutine[Any, Any, None]]
+        | None = None,
     ) -> None:
         """Initialize the discovery update mixin."""
         self._discovery_data = discovery_data
@@ -823,11 +834,13 @@ class MqttDiscoveryUpdate(Entity):
         """Subscribe to discovery updates."""
         await super().async_added_to_hass()
         self._removed_from_hass = False
-        discovery_hash = (
+        discovery_hash: tuple[str, str] | None = (
             self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None
         )
 
-        async def _async_remove_state_and_registry_entry(self) -> None:
+        async def _async_remove_state_and_registry_entry(
+            self: MqttDiscoveryUpdate,
+        ) -> None:
             """Remove entity's state and entity registry entry.
 
             Remove entity from entity registry if it is registered, this also removes the state.
@@ -842,13 +855,15 @@ class MqttDiscoveryUpdate(Entity):
             else:
                 await self.async_remove(force_remove=True)
 
-        async def discovery_callback(payload):
+        async def discovery_callback(payload: MQTTDiscoveryPayload) -> None:
             """Handle discovery update."""
             _LOGGER.info(
                 "Got update for entity with hash: %s '%s'",
                 discovery_hash,
                 payload,
             )
+            assert self._discovery_data
+            old_payload: DiscoveryInfoType
             old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD]
             debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id)
             if not payload:
@@ -923,39 +938,43 @@ class MqttDiscoveryUpdate(Entity):
             self._removed_from_hass = True
 
 
-def device_info_from_config(config) -> DeviceInfo | None:
+def device_info_from_specifications(
+    specifications: dict[str, Any] | None
+) -> DeviceInfo | None:
     """Return a device description for device registry."""
-    if not config:
+    if not specifications:
         return None
 
     info = DeviceInfo(
-        identifiers={(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]},
-        connections={(conn_[0], conn_[1]) for conn_ in config[CONF_CONNECTIONS]},
+        identifiers={(DOMAIN, id_) for id_ in specifications[CONF_IDENTIFIERS]},
+        connections={
+            (conn_[0], conn_[1]) for conn_ in specifications[CONF_CONNECTIONS]
+        },
     )
 
-    if CONF_MANUFACTURER in config:
-        info[ATTR_MANUFACTURER] = config[CONF_MANUFACTURER]
+    if CONF_MANUFACTURER in specifications:
+        info[ATTR_MANUFACTURER] = specifications[CONF_MANUFACTURER]
 
-    if CONF_MODEL in config:
-        info[ATTR_MODEL] = config[CONF_MODEL]
+    if CONF_MODEL in specifications:
+        info[ATTR_MODEL] = specifications[CONF_MODEL]
 
-    if CONF_NAME in config:
-        info[ATTR_NAME] = config[CONF_NAME]
+    if CONF_NAME in specifications:
+        info[ATTR_NAME] = specifications[CONF_NAME]
 
-    if CONF_HW_VERSION in config:
-        info[ATTR_HW_VERSION] = config[CONF_HW_VERSION]
+    if CONF_HW_VERSION in specifications:
+        info[ATTR_HW_VERSION] = specifications[CONF_HW_VERSION]
 
-    if CONF_SW_VERSION in config:
-        info[ATTR_SW_VERSION] = config[CONF_SW_VERSION]
+    if CONF_SW_VERSION in specifications:
+        info[ATTR_SW_VERSION] = specifications[CONF_SW_VERSION]
 
-    if CONF_VIA_DEVICE in config:
-        info[ATTR_VIA_DEVICE] = (DOMAIN, config[CONF_VIA_DEVICE])
+    if CONF_VIA_DEVICE in specifications:
+        info[ATTR_VIA_DEVICE] = (DOMAIN, specifications[CONF_VIA_DEVICE])
 
-    if CONF_SUGGESTED_AREA in config:
-        info[ATTR_SUGGESTED_AREA] = config[CONF_SUGGESTED_AREA]
+    if CONF_SUGGESTED_AREA in specifications:
+        info[ATTR_SUGGESTED_AREA] = specifications[CONF_SUGGESTED_AREA]
 
-    if CONF_CONFIGURATION_URL in config:
-        info[ATTR_CONFIGURATION_URL] = config[CONF_CONFIGURATION_URL]
+    if CONF_CONFIGURATION_URL in specifications:
+        info[ATTR_CONFIGURATION_URL] = specifications[CONF_CONFIGURATION_URL]
 
     return info
 
@@ -963,19 +982,21 @@ def device_info_from_config(config) -> DeviceInfo | None:
 class MqttEntityDeviceInfo(Entity):
     """Mixin used for mqtt platforms that support the device registry."""
 
-    def __init__(self, device_config: ConfigType | None, config_entry=None) -> None:
+    def __init__(
+        self, specifications: dict[str, Any] | None, config_entry: ConfigEntry
+    ) -> None:
         """Initialize the device mixin."""
-        self._device_config = device_config
+        self._device_specifications = specifications
         self._config_entry = config_entry
 
-    def device_info_discovery_update(self, config: dict):
+    def device_info_discovery_update(self, config: DiscoveryInfoType) -> None:
         """Handle updated discovery message."""
-        self._device_config = config.get(CONF_DEVICE)
+        self._device_specifications = config.get(CONF_DEVICE)
         device_registry = dr.async_get(self.hass)
         config_entry_id = self._config_entry.entry_id
         device_info = self.device_info
 
-        if config_entry_id is not None and device_info is not None:
+        if device_info is not None:
             device_registry.async_get_or_create(
                 config_entry_id=config_entry_id, **device_info
             )
@@ -983,7 +1004,7 @@ class MqttEntityDeviceInfo(Entity):
     @property
     def device_info(self) -> DeviceInfo | None:
         """Return a device description for device registry."""
-        return device_info_from_config(self._device_config)
+        return device_info_from_specifications(self._device_specifications)
 
 
 class MqttEntity(
@@ -997,12 +1018,18 @@ class MqttEntity(
     _attr_should_poll = False
     _entity_id_format: str
 
-    def __init__(self, hass, config, config_entry, discovery_data):
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        config: ConfigType,
+        config_entry: ConfigEntry,
+        discovery_data: DiscoveryInfoType | None,
+    ) -> None:
         """Init the MQTT Entity."""
         self.hass = hass
-        self._config = config
-        self._unique_id = config.get(CONF_UNIQUE_ID)
-        self._sub_state = None
+        self._config: ConfigType = config
+        self._unique_id: str | None = config.get(CONF_UNIQUE_ID)
+        self._sub_state: dict[str, EntitySubscription] = {}
 
         # Load config
         self._setup_from_config(self._config)
@@ -1016,14 +1043,14 @@ class MqttEntity(
         MqttDiscoveryUpdate.__init__(self, hass, discovery_data, self.discovery_update)
         MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry)
 
-    def _init_entity_id(self):
+    def _init_entity_id(self) -> None:
         """Set entity_id from object_id if defined in config."""
         init_entity_id_from_config(
             self.hass, self, self._config, self._entity_id_format
         )
 
     @final
-    async def async_added_to_hass(self):
+    async def async_added_to_hass(self) -> None:
         """Subscribe to MQTT events."""
         await super().async_added_to_hass()
         self._prepare_subscribe_topics()
@@ -1032,15 +1059,15 @@ class MqttEntity(
         if self._discovery_data is not None:
             send_discovery_done(self.hass, self._discovery_data)
 
-    async def mqtt_async_added_to_hass(self):
+    async def mqtt_async_added_to_hass(self) -> None:
         """Call before the discovery message is acknowledged.
 
         To be extended by subclasses.
         """
 
-    async def discovery_update(self, discovery_payload):
+    async def discovery_update(self, discovery_payload: MQTTDiscoveryPayload) -> None:
         """Handle updated discovery message."""
-        config = self.config_schema()(discovery_payload)
+        config: DiscoveryInfoType = self.config_schema()(discovery_payload)
         self._config = config
         self._setup_from_config(self._config)
 
@@ -1056,7 +1083,7 @@ class MqttEntity(
         await self._subscribe_topics()
         self.async_write_ha_state()
 
-    async def async_will_remove_from_hass(self):
+    async def async_will_remove_from_hass(self) -> None:
         """Unsubscribe when removed."""
         self._sub_state = subscription.async_unsubscribe_topics(
             self.hass, self._sub_state
@@ -1073,7 +1100,7 @@ class MqttEntity(
         qos: int = 0,
         retain: bool = False,
         encoding: str = DEFAULT_ENCODING,
-    ):
+    ) -> None:
         """Publish message to an MQTT topic."""
         log_message(self.hass, self.entity_id, topic, payload, qos, retain)
         await async_publish(
@@ -1087,18 +1114,18 @@ class MqttEntity(
 
     @staticmethod
     @abstractmethod
-    def config_schema():
+    def config_schema() -> vol.Schema:
         """Return the config schema."""
 
-    def _setup_from_config(self, config):
+    def _setup_from_config(self, config: ConfigType) -> None:
         """(Re)Setup the entity."""
 
     @abstractmethod
-    def _prepare_subscribe_topics(self):
+    def _prepare_subscribe_topics(self) -> None:
         """(Re)Subscribe to topics."""
 
     @abstractmethod
-    async def _subscribe_topics(self):
+    async def _subscribe_topics(self) -> None:
         """(Re)Subscribe to topics."""
 
     @property
@@ -1112,17 +1139,17 @@ class MqttEntity(
         return self._config.get(CONF_ENTITY_CATEGORY)
 
     @property
-    def icon(self):
+    def icon(self) -> str | None:
         """Return icon of the entity if any."""
         return self._config.get(CONF_ICON)
 
     @property
-    def name(self):
+    def name(self) -> str | None:
         """Return the name of the device if any."""
         return self._config.get(CONF_NAME)
 
     @property
-    def unique_id(self):
+    def unique_id(self) -> str | None:
         """Return a unique ID."""
         return self._unique_id
 
@@ -1136,10 +1163,10 @@ def update_device(
     if CONF_DEVICE not in config:
         return None
 
-    device = None
+    device: DeviceEntry | None = None
     device_registry = dr.async_get(hass)
     config_entry_id = config_entry.entry_id
-    device_info = device_info_from_config(config[CONF_DEVICE])
+    device_info = device_info_from_specifications(config[CONF_DEVICE])
 
     if config_entry_id is not None and device_info is not None:
         update_device_info = cast(dict, device_info)
@@ -1154,7 +1181,7 @@ def async_removed_from_device(
     hass: HomeAssistant, event: Event, mqtt_device_id: str, config_entry_id: str
 ) -> bool:
     """Check if the passed event indicates MQTT was removed from a device."""
-    device_id = event.data["device_id"]
+    device_id: str = event.data["device_id"]
     if event.data["action"] not in ("remove", "update"):
         return False
 
diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py
index f2f30419b4c..363956cc732 100644
--- a/homeassistant/components/mqtt/models.py
+++ b/homeassistant/components/mqtt/models.py
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
     from .client import MQTT, Subscription
     from .debug_info import TimestampedPublishMessage
     from .device_trigger import Trigger
-    from .discovery import MQTTConfig
+    from .discovery import MQTTDiscoveryPayload
     from .tag import MQTTTagScanner
 
 _SENTINEL = object()
@@ -86,7 +86,7 @@ class TriggerDebugInfo(TypedDict):
 class PendingDiscovered(TypedDict):
     """Pending discovered items."""
 
-    pending: deque[MQTTConfig]
+    pending: deque[MQTTDiscoveryPayload]
     unsub: CALLBACK_TYPE
 
 
diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py
index ac8b5431a59..8fdc6393e0b 100644
--- a/homeassistant/components/mqtt/update.py
+++ b/homeassistant/components/mqtt/update.py
@@ -98,8 +98,6 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
     ) -> None:
         """Initialize the MQTT update."""
         self._config = config
-        self._sub_state = None
-
         self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
 
         UpdateEntity.__init__(self)
-- 
GitLab


From ec4b8c49fe772178fdeb72f7950de52fd12a4ac6 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 24 Oct 2022 15:14:43 +0200
Subject: [PATCH 747/985] Add unique_id to scrape (#80581)

* Add unique_id to scrape

* Add tests
---
 homeassistant/components/scrape/sensor.py |  6 +++++
 tests/components/scrape/__init__.py       |  3 +++
 tests/components/scrape/test_sensor.py    | 29 +++++++++++++++++++++++
 3 files changed, 38 insertions(+)

diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index c2ba370a660..b13b5d8463b 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -23,6 +23,7 @@ from homeassistant.const import (
     CONF_NAME,
     CONF_PASSWORD,
     CONF_RESOURCE,
+    CONF_UNIQUE_ID,
     CONF_UNIT_OF_MEASUREMENT,
     CONF_USERNAME,
     CONF_VALUE_TEMPLATE,
@@ -66,6 +67,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
         vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
         vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
         vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
+        vol.Optional(CONF_UNIQUE_ID): cv.string,
         vol.Optional(CONF_USERNAME): cv.string,
         vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
         vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
@@ -92,6 +94,7 @@ async def async_setup_platform(
     unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT)
     device_class: str | None = config.get(CONF_DEVICE_CLASS)
     state_class: str | None = config.get(CONF_STATE_CLASS)
+    unique_id: str | None = config.get(CONF_UNIQUE_ID)
     username: str | None = config.get(CONF_USERNAME)
     password: str | None = config.get(CONF_PASSWORD)
     value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
@@ -117,6 +120,7 @@ async def async_setup_platform(
         [
             ScrapeSensor(
                 coordinator,
+                unique_id,
                 name,
                 select,
                 attr,
@@ -136,6 +140,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], SensorEntity):
     def __init__(
         self,
         coordinator: ScrapeCoordinator,
+        unique_id: str | None,
         name: str,
         select: str | None,
         attr: str | None,
@@ -153,6 +158,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], SensorEntity):
         self._index = index
         self._value_template = value_template
         self._attr_name = name
+        self._attr_unique_id = unique_id
         self._attr_native_unit_of_measurement = unit
         self._attr_device_class = device_class
         self._attr_state_class = state_class
diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py
index 0ba9266a79d..644ea84854a 100644
--- a/tests/components/scrape/__init__.py
+++ b/tests/components/scrape/__init__.py
@@ -18,6 +18,7 @@ def return_config(
     username=None,
     password=None,
     headers=None,
+    unique_id=None,
 ) -> dict[str, dict[str, Any]]:
     """Return config."""
     config = {
@@ -44,6 +45,8 @@ def return_config(
         config["password"] = password
     if headers:
         config["headers"] = headers
+    if unique_id:
+        config["unique_id"] = unique_id
     return config
 
 
diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py
index 3ad19ed25af..aacd89b2eb9 100644
--- a/tests/components/scrape/test_sensor.py
+++ b/tests/components/scrape/test_sensor.py
@@ -18,6 +18,7 @@ from homeassistant.const import (
     TEMP_CELSIUS,
 )
 from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
 from homeassistant.setup import async_setup_component
 
 from . import MockRestData, return_config
@@ -93,6 +94,34 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None:
     assert state.attributes[CONF_STATE_CLASS] == SensorStateClass.MEASUREMENT
 
 
+async def test_scrape_unique_id(hass: HomeAssistant) -> None:
+    """Test Scrape sensor for unique id."""
+    config = {
+        "sensor": return_config(
+            select=".current-temp h3",
+            name="Current Temp",
+            template="{{ value.split(':')[1] }}",
+            unique_id="very_unique_id",
+        )
+    }
+
+    mocker = MockRestData("test_scrape_uom_and_classes")
+    with patch(
+        "homeassistant.components.scrape.sensor.RestData",
+        return_value=mocker,
+    ):
+        assert await async_setup_component(hass, "sensor", config)
+        await hass.async_block_till_done()
+
+    state = hass.states.get("sensor.current_temp")
+    assert state.state == "22.1"
+
+    registry = er.async_get(hass)
+    entry = registry.async_get("sensor.current_temp")
+    assert entry
+    assert entry.unique_id == "very_unique_id"
+
+
 async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None:
     """Test Scrape sensor with authentication."""
     config = {
-- 
GitLab


From 0c79a9a33d653523f9bc656ee27df5a39c0bdbf6 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 24 Oct 2022 15:25:00 +0200
Subject: [PATCH 748/985] Adjust pylint for IMPERIAL_SYSTEM deprecation
 (#80874)

* Adjust pylint for IMPERIAL_SYSTEM deprecation

* Use correct location

* Adjust components
---
 homeassistant/components/google_travel_time/config_flow.py | 4 ++--
 homeassistant/components/waze_travel_time/config_flow.py   | 4 ++--
 pylint/plugins/hass_imports.py                             | 6 ++++++
 3 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py
index 133baddf704..d9218566bcb 100644
--- a/homeassistant/components/google_travel_time/config_flow.py
+++ b/homeassistant/components/google_travel_time/config_flow.py
@@ -8,7 +8,7 @@ from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.data_entry_flow import FlowResult
 import homeassistant.helpers.config_validation as cv
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     ALL_LANGUAGES,
@@ -46,7 +46,7 @@ def default_options(hass: HomeAssistant) -> dict[str, str | None]:
     return {
         CONF_MODE: "driving",
         CONF_UNITS: (
-            UNITS_IMPERIAL if hass.config.units is IMPERIAL_SYSTEM else UNITS_METRIC
+            UNITS_IMPERIAL if hass.config.units is US_CUSTOMARY_SYSTEM else UNITS_METRIC
         ),
     }
 
diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py
index 348b48e3368..fd6747cc1c8 100644
--- a/homeassistant/components/waze_travel_time/config_flow.py
+++ b/homeassistant/components/waze_travel_time/config_flow.py
@@ -8,7 +8,7 @@ from homeassistant.const import CONF_NAME, CONF_REGION
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.data_entry_flow import FlowResult
 import homeassistant.helpers.config_validation as cv
-from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .const import (
     CONF_AVOID_FERRIES,
@@ -35,7 +35,7 @@ from .helpers import is_valid_config_entry
 def default_options(hass: HomeAssistant) -> dict[str, str | bool]:
     """Get the default options."""
     defaults = DEFAULT_OPTIONS.copy()
-    if hass.config.units is IMPERIAL_SYSTEM:
+    if hass.config.units is US_CUSTOMARY_SYSTEM:
         defaults[CONF_UNITS] = IMPERIAL_UNITS
     return defaults
 
diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py
index 3d2f747ca7b..678773abcb9 100644
--- a/pylint/plugins/hass_imports.py
+++ b/pylint/plugins/hass_imports.py
@@ -296,6 +296,12 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
             constant=re.compile(r"^(distance|pressure|speed|temperature|volume)$"),
         ),
     ],
+    "homeassistant.util.unit_system": [
+        ObsoleteImportMatch(
+            reason="replaced by US_CUSTOMARY_SYSTEM",
+            constant=re.compile(r"^IMPERIAL_SYSTEM$"),
+        ),
+    ],
 }
 
 
-- 
GitLab


From 6979cd95b0fe85c3ee8eca3dbc9881b8d05591e8 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Mon, 24 Oct 2022 16:08:02 +0200
Subject: [PATCH 749/985] Add suggested_unit_of_measurement attribute to
 sensors (#80638)

* Add suggested_unit_of_measurement attribute to sensors

* Lazy calculation of initial entity options

* Add type alias for entity options

* Small tweak

* Add tests

* Store suggested_unit_of_measurement in its own option key

* Adapt to renaming of IMPERIAL_SYSTEM

* Fix rebase mistakes

* Apply suggestions from code review

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
---
 homeassistant/components/sensor/__init__.py   |  84 ++++++-
 homeassistant/helpers/entity.py               |  12 +
 homeassistant/helpers/entity_platform.py      |   3 +-
 homeassistant/helpers/entity_registry.py      |  14 +-
 homeassistant/util/unit_system.py             |  20 +-
 tests/components/sensor/test_init.py          | 212 ++++++++++++++++++
 tests/helpers/test_template.py                |   9 +-
 .../custom_components/test/sensor.py          |   5 +
 tests/util/test_unit_system.py                |  63 +++---
 9 files changed, 377 insertions(+), 45 deletions(-)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index c6c5908b5d8..7913dd345db 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -49,6 +49,7 @@ from homeassistant.const import (  # noqa: F401, pylint: disable=[hass-deprecate
     TEMP_KELVIN,
 )
 from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import entity_registry as er
 from homeassistant.helpers.config_validation import (  # noqa: F401
     PLATFORM_SCHEMA,
     PLATFORM_SCHEMA_BASE,
@@ -407,6 +408,7 @@ class SensorEntityDescription(EntityDescription):
     """A class that describes sensor entities."""
 
     device_class: SensorDeviceClass | str | None = None
+    suggested_unit_of_measurement: str | None = None
     last_reset: datetime | None = None
     native_unit_of_measurement: str | None = None
     state_class: SensorStateClass | str | None = None
@@ -423,6 +425,7 @@ class SensorEntity(Entity):
     _attr_native_value: StateType | date | datetime | Decimal = None
     _attr_state_class: SensorStateClass | str | None
     _attr_state: None = None  # Subclasses of SensorEntity should not set this
+    _attr_suggested_unit_of_measurement: str | None
     _attr_unit_of_measurement: None = (
         None  # Subclasses of SensorEntity should not set this
     )
@@ -471,6 +474,30 @@ class SensorEntity(Entity):
 
         return None
 
+    def get_initial_entity_options(self) -> er.EntityOptionsType | None:
+        """Return initial entity options.
+
+        These will be stored in the entity registry the first time the entity is seen,
+        and then never updated.
+        """
+        # Unit suggested by the integration
+        suggested_unit_of_measurement = self.suggested_unit_of_measurement
+
+        if suggested_unit_of_measurement is None:
+            # Fallback to suggested by the unit conversion rules
+            suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
+                self.device_class, self.native_unit_of_measurement
+            )
+
+        if suggested_unit_of_measurement is None:
+            return None
+
+        return {
+            f"{DOMAIN}.private": {
+                "suggested_unit_of_measurement": suggested_unit_of_measurement
+            }
+        }
+
     @final
     @property
     def state_attributes(self) -> dict[str, Any] | None:
@@ -514,13 +541,45 @@ class SensorEntity(Entity):
             return self.entity_description.native_unit_of_measurement
         return None
 
+    @property
+    def suggested_unit_of_measurement(self) -> str | None:
+        """Return the unit which should be used for the sensor's state.
+
+        This can be used by integrations to override automatic unit conversion rules,
+        for example to make a temperature sensor display in °C even if the configured
+        unit system prefers °F.
+
+        For sensors without a `unique_id`, this takes precedence over legacy
+        temperature conversion rules only.
+
+        For sensors with a `unique_id`, this is applied only if the unit is not set by the user,
+        and takes precedence over automatic device-class conversion rules.
+
+        Note:
+            suggested_unit_of_measurement is stored in the entity registry the first time
+            the entity is seen, and then never updated.
+        """
+        if hasattr(self, "_attr_suggested_unit_of_measurement"):
+            return self._attr_suggested_unit_of_measurement
+        if hasattr(self, "entity_description"):
+            return self.entity_description.suggested_unit_of_measurement
+        return None
+
     @final
     @property
     def unit_of_measurement(self) -> str | None:
         """Return the unit of measurement of the entity, after unit conversion."""
+        # Highest priority, for registered entities: unit set by user, with fallback to unit suggested
+        # by integration or secondary fallback to unit conversion rules
         if self._sensor_option_unit_of_measurement:
             return self._sensor_option_unit_of_measurement
 
+        # Second priority, for non registered entities: unit suggested by integration
+        if not self.registry_entry and self.suggested_unit_of_measurement:
+            return self.suggested_unit_of_measurement
+
+        # Third priority: Legacy temperature conversion, which applies
+        # to both registered and non registered entities
         native_unit_of_measurement = self.native_unit_of_measurement
 
         if (
@@ -529,6 +588,7 @@ class SensorEntity(Entity):
         ):
             return self.hass.config.units.temperature_unit
 
+        # Fourth priority: Native unit
         return native_unit_of_measurement
 
     @final
@@ -624,22 +684,30 @@ class SensorEntity(Entity):
 
         return super().__repr__()
 
-    @callback
-    def async_registry_entry_updated(self) -> None:
-        """Run when the entity registry entry has been updated."""
+    def _custom_unit_or_none(self, primary_key: str, secondary_key: str) -> str | None:
+        """Return a custom unit, or None if it's not compatible with the native unit."""
         assert self.registry_entry
         if (
-            (sensor_options := self.registry_entry.options.get(DOMAIN))
-            and (custom_unit := sensor_options.get(CONF_UNIT_OF_MEASUREMENT))
+            (sensor_options := self.registry_entry.options.get(primary_key))
+            and (custom_unit := sensor_options.get(secondary_key))
             and (device_class := self.device_class) in UNIT_CONVERTERS
             and self.native_unit_of_measurement
             in UNIT_CONVERTERS[device_class].VALID_UNITS
             and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
         ):
-            self._sensor_option_unit_of_measurement = custom_unit
-            return
+            return cast(str, custom_unit)
+        return None
 
-        self._sensor_option_unit_of_measurement = None
+    @callback
+    def async_registry_entry_updated(self) -> None:
+        """Run when the entity registry entry has been updated."""
+        self._sensor_option_unit_of_measurement = self._custom_unit_or_none(
+            DOMAIN, CONF_UNIT_OF_MEASUREMENT
+        )
+        if not self._sensor_option_unit_of_measurement:
+            self._sensor_option_unit_of_measurement = self._custom_unit_or_none(
+                f"{DOMAIN}.private", "suggested_unit_of_measurement"
+            )
 
 
 @dataclass
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 2f2588367b6..57cfe362231 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -340,6 +340,18 @@ class Entity(ABC):
         """
         return self._attr_capability_attributes
 
+    def get_initial_entity_options(self) -> er.EntityOptionsType | None:
+        """Return initial entity options.
+
+        These will be stored in the entity registry the first time the entity is seen,
+        and then never updated.
+
+        Implemented by component base class, should not be extended by integrations.
+
+        Note: Not a property to avoid calculating unless needed.
+        """
+        return None
+
     @property
     def state_attributes(self) -> dict[str, Any] | None:
         """Return the state attributes.
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 81487bbb627..9b8e1985930 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -607,9 +607,10 @@ class EntityPlatform:
                 device_id=device_id,
                 disabled_by=disabled_by,
                 entity_category=entity.entity_category,
+                get_initial_options=entity.get_initial_entity_options,
+                has_entity_name=entity.has_entity_name,
                 hidden_by=hidden_by,
                 known_object_ids=self.entities.keys(),
-                has_entity_name=entity.has_entity_name,
                 original_device_class=entity.device_class,
                 original_icon=entity.icon,
                 original_name=entity.name,
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index e58dde19127..77a0b5a0400 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -94,6 +94,9 @@ class RegistryEntryHider(StrEnum):
     USER = "user"
 
 
+EntityOptionsType = Mapping[str, Mapping[str, Any]]
+
+
 @attr.s(slots=True, frozen=True)
 class RegistryEntry:
     """Entity Registry Entry."""
@@ -114,7 +117,7 @@ class RegistryEntry:
     id: str = attr.ib(factory=uuid_util.random_uuid_hex)
     has_entity_name: bool = attr.ib(default=False)
     name: str | None = attr.ib(default=None)
-    options: Mapping[str, Mapping[str, Any]] = attr.ib(
+    options: EntityOptionsType = attr.ib(
         default=None, converter=attr.converters.default_if_none(factory=dict)  # type: ignore[misc]
     )
     # As set by integration
@@ -397,6 +400,8 @@ class EntityRegistry:
         # To disable or hide an entity if it gets created
         disabled_by: RegistryEntryDisabler | None = None,
         hidden_by: RegistryEntryHider | None = None,
+        # Function to generate initial entity options if it gets created
+        get_initial_options: Callable[[], EntityOptionsType | None] | None = None,
         # Data that we want entry to have
         capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED,
         config_entry: ConfigEntry | None | UndefinedType = UNDEFINED,
@@ -465,6 +470,8 @@ class EntityRegistry:
             """Return None if value is UNDEFINED, otherwise return value."""
             return None if value is UNDEFINED else value
 
+        initial_options = get_initial_options() if get_initial_options else None
+
         entry = RegistryEntry(
             capabilities=none_if_undefined(capabilities),
             config_entry_id=none_if_undefined(config_entry_id),
@@ -474,6 +481,7 @@ class EntityRegistry:
             entity_id=entity_id,
             hidden_by=hidden_by,
             has_entity_name=none_if_undefined(has_entity_name) or False,
+            options=initial_options,
             original_device_class=none_if_undefined(original_device_class),
             original_icon=none_if_undefined(original_icon),
             original_name=none_if_undefined(original_name),
@@ -590,7 +598,7 @@ class EntityRegistry:
         supported_features: int | UndefinedType = UNDEFINED,
         unit_of_measurement: str | None | UndefinedType = UNDEFINED,
         platform: str | None | UndefinedType = UNDEFINED,
-        options: Mapping[str, Mapping[str, Any]] | UndefinedType = UNDEFINED,
+        options: EntityOptionsType | UndefinedType = UNDEFINED,
     ) -> RegistryEntry:
         """Private facing update properties method."""
         old = self.entities[entity_id]
@@ -779,7 +787,7 @@ class EntityRegistry:
     ) -> RegistryEntry:
         """Update entity options."""
         old = self.entities[entity_id]
-        new_options: Mapping[str, Mapping[str, Any]] = {**old.options, domain: options}
+        new_options: EntityOptionsType = {**old.options, domain: options}
         return self._async_update_entity(entity_id, options=new_options)
 
     async def async_load(self) -> None:
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index bb2cd0862e1..db0e0e49e17 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -2,7 +2,7 @@
 from __future__ import annotations
 
 from numbers import Number
-from typing import Final
+from typing import TYPE_CHECKING, Final
 
 import voluptuous as vol
 
@@ -42,6 +42,9 @@ from .unit_conversion import (
     VolumeConverter,
 )
 
+if TYPE_CHECKING:
+    from homeassistant.components.sensor import SensorDeviceClass
+
 _CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial"
 _CONF_UNIT_SYSTEM_METRIC: Final = "metric"
 _CONF_UNIT_SYSTEM_US_CUSTOMARY: Final = "us_customary"
@@ -90,6 +93,7 @@ class UnitSystem:
         *,
         accumulated_precipitation: str,
         length: str,
+        length_conversions: dict[str | None, str],
         mass: str,
         pressure: str,
         temperature: str,
@@ -122,6 +126,7 @@ class UnitSystem:
         self.pressure_unit = pressure
         self.volume_unit = volume
         self.wind_speed_unit = wind_speed
+        self._length_conversions = length_conversions
 
     @property
     def name(self) -> str:
@@ -215,6 +220,17 @@ class UnitSystem:
             WIND_SPEED: self.wind_speed_unit,
         }
 
+    def get_converted_unit(
+        self,
+        device_class: SensorDeviceClass | str | None,
+        original_unit: str | None,
+    ) -> str | None:
+        """Return converted unit given a device class or an original unit."""
+        if device_class == "distance":
+            return self._length_conversions.get(original_unit)
+
+        return None
+
 
 def get_unit_system(key: str) -> UnitSystem:
     """Get unit system based on key."""
@@ -244,6 +260,7 @@ METRIC_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_METRIC,
     accumulated_precipitation=PRECIPITATION_MILLIMETERS,
     length=LENGTH_KILOMETERS,
+    length_conversions={LENGTH_MILES: LENGTH_KILOMETERS},
     mass=MASS_GRAMS,
     pressure=PRESSURE_PA,
     temperature=TEMP_CELSIUS,
@@ -255,6 +272,7 @@ US_CUSTOMARY_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_US_CUSTOMARY,
     accumulated_precipitation=PRECIPITATION_INCHES,
     length=LENGTH_MILES,
+    length_conversions={LENGTH_KILOMETERS: LENGTH_MILES},
     mass=MASS_POUNDS,
     pressure=PRESSURE_PSI,
     temperature=TEMP_FAHRENHEIT,
diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py
index 0fe60ef98c7..567d00d653d 100644
--- a/tests/components/sensor/test_init.py
+++ b/tests/components/sensor/test_init.py
@@ -11,7 +11,9 @@ from homeassistant.const import (
     LENGTH_CENTIMETERS,
     LENGTH_INCHES,
     LENGTH_KILOMETERS,
+    LENGTH_METERS,
     LENGTH_MILES,
+    LENGTH_YARD,
     MASS_GRAMS,
     MASS_OUNCES,
     PRESSURE_HPA,
@@ -661,3 +663,213 @@ async def test_custom_unit_change(
     state = hass.states.get(entity0.entity_id)
     assert float(state.state) == approx(float(native_value))
     assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
+
+
+@pytest.mark.parametrize(
+    "unit_system, native_unit, automatic_unit, suggested_unit, custom_unit, native_value, automatic_value, suggested_value, custom_value, device_class",
+    [
+        # Distance
+        (
+            US_CUSTOMARY_SYSTEM,
+            LENGTH_KILOMETERS,
+            LENGTH_MILES,
+            LENGTH_METERS,
+            LENGTH_YARD,
+            1000,
+            621,
+            1000000,
+            1093613,
+            SensorDeviceClass.DISTANCE,
+        ),
+    ],
+)
+async def test_unit_conversion_priority(
+    hass,
+    enable_custom_integrations,
+    unit_system,
+    native_unit,
+    automatic_unit,
+    suggested_unit,
+    custom_unit,
+    native_value,
+    automatic_value,
+    suggested_value,
+    custom_value,
+    device_class,
+):
+    """Test priority of unit conversion."""
+
+    hass.config.units = unit_system
+
+    entity_registry = er.async_get(hass)
+    platform = getattr(hass.components, "test.sensor")
+    platform.init(empty=True)
+
+    platform.ENTITIES["0"] = platform.MockSensor(
+        name="Test",
+        device_class=device_class,
+        native_unit_of_measurement=native_unit,
+        native_value=str(native_value),
+        unique_id="very_unique",
+    )
+    entity0 = platform.ENTITIES["0"]
+
+    platform.ENTITIES["1"] = platform.MockSensor(
+        name="Test",
+        device_class=device_class,
+        native_unit_of_measurement=native_unit,
+        native_value=str(native_value),
+    )
+    entity1 = platform.ENTITIES["1"]
+
+    platform.ENTITIES["2"] = platform.MockSensor(
+        name="Test",
+        device_class=device_class,
+        native_unit_of_measurement=native_unit,
+        native_value=str(native_value),
+        suggested_unit_of_measurement=suggested_unit,
+        unique_id="very_unique_2",
+    )
+    entity2 = platform.ENTITIES["2"]
+
+    platform.ENTITIES["3"] = platform.MockSensor(
+        name="Test",
+        device_class=device_class,
+        native_unit_of_measurement=native_unit,
+        native_value=str(native_value),
+        suggested_unit_of_measurement=suggested_unit,
+    )
+    entity3 = platform.ENTITIES["3"]
+
+    assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+    await hass.async_block_till_done()
+
+    # Registered entity -> Follow automatic unit conversion
+    state = hass.states.get(entity0.entity_id)
+    assert float(state.state) == approx(float(automatic_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit
+    # Assert the automatic unit conversion is stored in the registry
+    entry = entity_registry.async_get(entity0.entity_id)
+    assert entry.options == {
+        "sensor.private": {"suggested_unit_of_measurement": automatic_unit}
+    }
+
+    # Unregistered entity -> Follow native unit
+    state = hass.states.get(entity1.entity_id)
+    assert float(state.state) == approx(float(native_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
+
+    # Registered entity with suggested unit
+    state = hass.states.get(entity2.entity_id)
+    assert float(state.state) == approx(float(suggested_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
+    # Assert the suggested unit is stored in the registry
+    entry = entity_registry.async_get(entity2.entity_id)
+    assert entry.options == {
+        "sensor.private": {"suggested_unit_of_measurement": suggested_unit}
+    }
+
+    # Unregistered entity with suggested unit
+    state = hass.states.get(entity3.entity_id)
+    assert float(state.state) == approx(float(suggested_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit
+
+    # Set a custom unit, this should have priority over the automatic unit conversion
+    entity_registry.async_update_entity_options(
+        entity0.entity_id, "sensor", {"unit_of_measurement": custom_unit}
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get(entity0.entity_id)
+    assert float(state.state) == approx(float(custom_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit
+
+    entity_registry.async_update_entity_options(
+        entity2.entity_id, "sensor", {"unit_of_measurement": custom_unit}
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get(entity2.entity_id)
+    assert float(state.state) == approx(float(custom_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit
+
+
+@pytest.mark.parametrize(
+    "unit_system, native_unit, original_unit, suggested_unit, native_value, original_value, device_class",
+    [
+        # Distance
+        (
+            US_CUSTOMARY_SYSTEM,
+            LENGTH_KILOMETERS,
+            LENGTH_YARD,
+            LENGTH_METERS,
+            1000,
+            1093613,
+            SensorDeviceClass.DISTANCE,
+        ),
+    ],
+)
+async def test_unit_conversion_priority_suggested_unit_change(
+    hass,
+    enable_custom_integrations,
+    unit_system,
+    native_unit,
+    original_unit,
+    suggested_unit,
+    native_value,
+    original_value,
+    device_class,
+):
+    """Test priority of unit conversion."""
+
+    hass.config.units = unit_system
+
+    entity_registry = er.async_get(hass)
+    platform = getattr(hass.components, "test.sensor")
+    platform.init(empty=True)
+
+    # Pre-register entities
+    entry = entity_registry.async_get_or_create("sensor", "test", "very_unique")
+    entity_registry.async_update_entity_options(
+        entry.entity_id,
+        "sensor.private",
+        {"suggested_unit_of_measurement": original_unit},
+    )
+    entry = entity_registry.async_get_or_create("sensor", "test", "very_unique_2")
+    entity_registry.async_update_entity_options(
+        entry.entity_id,
+        "sensor.private",
+        {"suggested_unit_of_measurement": original_unit},
+    )
+
+    platform.ENTITIES["0"] = platform.MockSensor(
+        name="Test",
+        device_class=device_class,
+        native_unit_of_measurement=native_unit,
+        native_value=str(native_value),
+        unique_id="very_unique",
+    )
+    entity0 = platform.ENTITIES["0"]
+
+    platform.ENTITIES["1"] = platform.MockSensor(
+        name="Test",
+        device_class=device_class,
+        native_unit_of_measurement=native_unit,
+        native_value=str(native_value),
+        suggested_unit_of_measurement=suggested_unit,
+        unique_id="very_unique_2",
+    )
+    entity1 = platform.ENTITIES["1"]
+
+    assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+    await hass.async_block_till_done()
+
+    # Registered entity -> Follow automatic unit conversion the first time the entity was seen
+    state = hass.states.get(entity0.entity_id)
+    assert float(state.state) == approx(float(original_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit
+
+    # Registered entity -> Follow suggested unit the first time the entity was seen
+    state = hass.states.get(entity1.entity_id)
+    assert float(state.state) == approx(float(original_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index 7679ddb44fc..2edb592ce39 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -43,13 +43,14 @@ def _set_up_units(hass):
     """Set up the tests."""
     hass.config.units = UnitSystem(
         "custom",
-        temperature=TEMP_CELSIUS,
+        accumulated_precipitation=LENGTH_MILLIMETERS,
         length=LENGTH_METERS,
-        wind_speed=SPEED_KILOMETERS_PER_HOUR,
-        volume=VOLUME_LITERS,
+        length_conversions={},
         mass=MASS_GRAMS,
         pressure=PRESSURE_PA,
-        accumulated_precipitation=LENGTH_MILLIMETERS,
+        temperature=TEMP_CELSIUS,
+        volume=VOLUME_LITERS,
+        wind_speed=SPEED_KILOMETERS_PER_HOUR,
     )
 
 
diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py
index 6404a126807..9584e47ba0b 100644
--- a/tests/testing_config/custom_components/test/sensor.py
+++ b/tests/testing_config/custom_components/test/sensor.py
@@ -112,6 +112,11 @@ class MockSensor(MockEntity, SensorEntity):
         """Return the state class of this sensor."""
         return self._handle("state_class")
 
+    @property
+    def suggested_unit_of_measurement(self):
+        """Return the state class of this sensor."""
+        return self._handle("suggested_unit_of_measurement")
+
 
 class MockRestoreSensor(MockSensor, RestoreSensor):
     """Mock RestoreSensor class."""
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 4aa62417705..6734abba7ac 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -39,85 +39,92 @@ def test_invalid_units():
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            temperature=INVALID_UNIT,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
             length=LENGTH_METERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
-            volume=VOLUME_LITERS,
+            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            temperature=INVALID_UNIT,
+            volume=VOLUME_LITERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            temperature=TEMP_CELSIUS,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
             length=INVALID_UNIT,
-            wind_speed=SPEED_METERS_PER_SECOND,
-            volume=VOLUME_LITERS,
+            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            volume=VOLUME_LITERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            temperature=TEMP_CELSIUS,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
             length=LENGTH_METERS,
-            wind_speed=INVALID_UNIT,
-            volume=VOLUME_LITERS,
+            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            volume=VOLUME_LITERS,
+            wind_speed=INVALID_UNIT,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            temperature=TEMP_CELSIUS,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
             length=LENGTH_METERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
-            volume=INVALID_UNIT,
+            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            volume=INVALID_UNIT,
+            wind_speed=SPEED_METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            temperature=TEMP_CELSIUS,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
             length=LENGTH_METERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
-            volume=VOLUME_LITERS,
+            length_conversions={},
             mass=INVALID_UNIT,
             pressure=PRESSURE_PA,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            volume=VOLUME_LITERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            temperature=TEMP_CELSIUS,
+            accumulated_precipitation=LENGTH_MILLIMETERS,
             length=LENGTH_METERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
-            volume=VOLUME_LITERS,
+            length_conversions={},
             mass=MASS_GRAMS,
             pressure=INVALID_UNIT,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            temperature=TEMP_CELSIUS,
+            volume=VOLUME_LITERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            temperature=TEMP_CELSIUS,
+            accumulated_precipitation=INVALID_UNIT,
             length=LENGTH_METERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
-            volume=VOLUME_LITERS,
+            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
-            accumulated_precipitation=INVALID_UNIT,
+            temperature=TEMP_CELSIUS,
+            volume=VOLUME_LITERS,
+            wind_speed=SPEED_METERS_PER_SECOND,
         )
 
 
-- 
GitLab


From 232041b194cb147bc21c4981fea4c391e1e53598 Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
Date: Mon, 24 Oct 2022 16:18:05 +0200
Subject: [PATCH 750/985] Add field descriptions for nibe heat pump (#80791)

---
 .../components/nibe_heatpump/config_flow.py   | 19 ++++++++++++++-----
 .../components/nibe_heatpump/strings.json     |  7 +++++++
 .../nibe_heatpump/translations/en.json        |  9 ++++++++-
 3 files changed, 29 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py
index bd48583547c..d68def046fd 100644
--- a/homeassistant/components/nibe_heatpump/config_flow.py
+++ b/homeassistant/components/nibe_heatpump/config_flow.py
@@ -14,7 +14,7 @@ from homeassistant import config_entries
 from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL
 from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResult
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import selector
 
 from .const import (
     CONF_CONNECTION_TYPE,
@@ -27,13 +27,22 @@ from .const import (
     LOGGER,
 )
 
+PORT_SELECTOR = vol.All(
+    selector.NumberSelector(
+        selector.NumberSelectorConfig(
+            min=1, step=1, max=65535, mode=selector.NumberSelectorMode.BOX
+        )
+    ),
+    vol.Coerce(int),
+)
+
 STEP_USER_DATA_SCHEMA = vol.Schema(
     {
         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,
-        vol.Required(CONF_REMOTE_WRITE_PORT, default=10000): cv.port,
+        vol.Required(CONF_IP_ADDRESS): selector.TextSelector(),
+        vol.Required(CONF_LISTENING_PORT, default=9999): PORT_SELECTOR,
+        vol.Required(CONF_REMOTE_READ_PORT, default=9999): PORT_SELECTOR,
+        vol.Required(CONF_REMOTE_WRITE_PORT, default=10000): PORT_SELECTOR,
     }
 )
 
diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json
index 9bbee6ae2ee..08a049cb17a 100644
--- a/homeassistant/components/nibe_heatpump/strings.json
+++ b/homeassistant/components/nibe_heatpump/strings.json
@@ -2,11 +2,18 @@
   "config": {
     "step": {
       "user": {
+        "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory.",
         "data": {
           "ip_address": "Remote address",
           "remote_read_port": "Remote read port",
           "remote_write_port": "Remote write port",
           "listening_port": "Local listening port"
+        },
+        "data_description": {
+          "ip_address": "The address of the NibeGW unit. The device should have been configured with a static address.",
+          "remote_read_port": "The port the NibeGW unit is listening for read requests on.",
+          "remote_write_port": "The port the NibeGW unit is listening for write requests on.",
+          "listening_port": "The local port on this system, that the NibeGW unit is configured to send data to."
         }
       }
     },
diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json
index 3837989511f..459b0792df7 100644
--- a/homeassistant/components/nibe_heatpump/translations/en.json
+++ b/homeassistant/components/nibe_heatpump/translations/en.json
@@ -15,7 +15,14 @@
                     "listening_port": "Local listening port",
                     "remote_read_port": "Remote read port",
                     "remote_write_port": "Remote write port"
-                }
+                },
+                "data_description": {
+                    "ip_address": "The address of the NibeGW unit. The device should have been configured with a static address.",
+                    "listening_port": "The local port on this system, that the NibeGW unit is configured to send data to.",
+                    "remote_read_port": "The port the NibeGW unit is listening for read requests on.",
+                    "remote_write_port": "The port the NibeGW unit is listening for write requests on."
+                },
+                "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory."
             }
         }
     }
-- 
GitLab


From 9978296ae2b95dca70faf8063148db824e8ed1b1 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 24 Oct 2022 09:28:59 -0500
Subject: [PATCH 751/985] Bump bleak-retry-connector to 2.4.0 (#80887)

---
 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 a2e2322b4db..2a3286da13d 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -7,7 +7,7 @@
   "quality_scale": "internal",
   "requirements": [
     "bleak==0.19.0",
-    "bleak-retry-connector==2.3.2",
+    "bleak-retry-connector==2.4.0",
     "bluetooth-adapters==0.6.0",
     "bluetooth-auto-recovery==0.3.6",
     "dbus-fast==1.47.0"
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index e27ad8e79e5..29b15503d1a 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.3.2
+bleak-retry-connector==2.4.0
 bleak==0.19.0
 bluetooth-adapters==0.6.0
 bluetooth-auto-recovery==0.3.6
diff --git a/requirements_all.txt b/requirements_all.txt
index ba49cdaa59e..cf642ff5b68 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.3.2
+bleak-retry-connector==2.4.0
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 169574d8d0e..5150aa147f5 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.3.2
+bleak-retry-connector==2.4.0
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
-- 
GitLab


From a7610909de50aac55bbc1de36e96f0ca7bd9720f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 24 Oct 2022 09:29:13 -0500
Subject: [PATCH 752/985] Fix HKC exceptions during BLE startup not being
 caught (#80882)

---
 .../components/homekit_controller/connection.py     |  8 +++++---
 .../components/homekit_controller/const.py          | 13 +++++++++++++
 2 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py
index 4d8d26fb557..320df671144 100644
--- a/homeassistant/components/homekit_controller/connection.py
+++ b/homeassistant/components/homekit_controller/connection.py
@@ -35,6 +35,7 @@ from .const import (
     IDENTIFIER_LEGACY_ACCESSORY_ID,
     IDENTIFIER_LEGACY_SERIAL_NUMBER,
     IDENTIFIER_SERIAL_NUMBER,
+    STARTUP_EXCEPTIONS,
 )
 from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry
 
@@ -187,11 +188,12 @@ class HKDevice:
         """
         try:
             await self.pairing.async_populate_accessories_state(force_update=True)
-        except (asyncio.TimeoutError, AccessoryNotFoundError):
+        except STARTUP_EXCEPTIONS as ex:
             _LOGGER.debug(
                 "Failed to populate BLE accessory state for %s, accessory may be sleeping"
-                " and will be retried the next time it advertises",
+                " and will be retried the next time it advertises: %s",
                 self.config_entry.title,
+                ex,
             )
 
     async def async_setup(self) -> None:
@@ -220,7 +222,7 @@ class HKDevice:
                 # BLE devices may sleep and we can't force a connection
                 raise
             entry.async_on_unload(
-                self.hass.bus.async_listen_once(
+                self.hass.bus.async_listen(
                     EVENT_HOMEASSISTANT_STARTED,
                     self._async_retry_populate_ble_accessory_state,
                 )
diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py
index 5ea8205260e..8c7db4dad00 100644
--- a/homeassistant/components/homekit_controller/const.py
+++ b/homeassistant/components/homekit_controller/const.py
@@ -1,6 +1,12 @@
 """Constants for the homekit_controller component."""
+import asyncio
 from typing import Final
 
+from aiohomekit.exceptions import (
+    AccessoryDisconnectedError,
+    AccessoryNotFoundError,
+    EncryptionError,
+)
 from aiohomekit.model.characteristics import CharacteristicsTypes
 from aiohomekit.model.services import ServicesTypes
 
@@ -94,3 +100,10 @@ CHARACTERISTIC_PLATFORMS = {
 
 # Device classes
 DEVICE_CLASS_ECOBEE_MODE: Final = "homekit_controller__ecobee_mode"
+
+STARTUP_EXCEPTIONS = (
+    asyncio.TimeoutError,
+    AccessoryNotFoundError,
+    EncryptionError,
+    AccessoryDisconnectedError,
+)
-- 
GitLab


From 1f8a9ed3c78dd10f7c37bdd994635e3168dd2d17 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 24 Oct 2022 09:38:28 -0500
Subject: [PATCH 753/985] Bump aiohomekit to 2.2.3 (#80891)

---
 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 f3ca06c851f..99b80df9157 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.2"],
+  "requirements": ["aiohomekit==2.2.3"],
   "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 cf642ff5b68..3fbcb97bdc1 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.2
+aiohomekit==2.2.3
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 5150aa147f5..f2609718373 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.2
+aiohomekit==2.2.3
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From 4279d738009dad365160b8fefab9cd297741efd3 Mon Sep 17 00:00:00 2001
From: Jc2k <john.carr@unrouted.co.uk>
Date: Mon, 24 Oct 2022 16:31:26 +0100
Subject: [PATCH 754/985] Add support for Netatmo noise sensor to
 homekit_controller (#80889)

Co-authored-by: J. Nick Koston <nick@koston.org>
---
 .../components/homekit_controller/sensor.py   |   7 +
 .../fixtures/netatmo_home_coach.json          | 249 ++++++++++++++++++
 .../test_netatmo_home_coach.py                |  45 ++++
 3 files changed, 301 insertions(+)
 create mode 100644 tests/components/homekit_controller/fixtures/netatmo_home_coach.json
 create mode 100644 tests/components/homekit_controller/specific_devices/test_netatmo_home_coach.py

diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py
index da9305a2ae5..150f2badc6b 100644
--- a/homeassistant/components/homekit_controller/sensor.py
+++ b/homeassistant/components/homekit_controller/sensor.py
@@ -32,6 +32,7 @@ from homeassistant.const import (
     POWER_WATT,
     PRESSURE_HPA,
     SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+    SOUND_PRESSURE_DB,
     TEMP_CELSIUS,
     Platform,
 )
@@ -309,6 +310,12 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = {
         entity_category=EntityCategory.DIAGNOSTIC,
         format=thread_status_to_str,
     ),
+    CharacteristicsTypes.VENDOR_NETATMO_NOISE: HomeKitSensorEntityDescription(
+        key=CharacteristicsTypes.VENDOR_NETATMO_NOISE,
+        name="Noise",
+        state_class=SensorStateClass.MEASUREMENT,
+        native_unit_of_measurement=SOUND_PRESSURE_DB,
+    ),
 }
 
 
diff --git a/tests/components/homekit_controller/fixtures/netatmo_home_coach.json b/tests/components/homekit_controller/fixtures/netatmo_home_coach.json
new file mode 100644
index 00000000000..b17c1bc542c
--- /dev/null
+++ b/tests/components/homekit_controller/fixtures/netatmo_home_coach.json
@@ -0,0 +1,249 @@
+[
+  {
+    "aid": 1,
+    "services": [
+      {
+        "iid": 1,
+        "type": "0000003E-0000-1000-8000-0026BB765291",
+        "characteristics": [
+          {
+            "type": "00000023-0000-1000-8000-0026BB765291",
+            "iid": 2,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Healthy Home Coach",
+            "description": "Name",
+            "maxLen": 64
+          },
+          {
+            "type": "00000020-0000-1000-8000-0026BB765291",
+            "iid": 3,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Netatmo",
+            "description": "Manufacturer",
+            "maxLen": 64
+          },
+          {
+            "type": "00000021-0000-1000-8000-0026BB765291",
+            "iid": 4,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Healthy Home Coach",
+            "description": "Model",
+            "maxLen": 64
+          },
+          {
+            "type": "00000030-0000-1000-8000-0026BB765291",
+            "iid": 5,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "AAAAAAAAAAAAA",
+            "description": "Serial Number",
+            "maxLen": 64
+          },
+          {
+            "type": "00000052-0000-1000-8000-0026BB765291",
+            "iid": 6,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "59",
+            "description": "Firmware Revision",
+            "maxLen": 64
+          },
+          {
+            "type": "00000014-0000-1000-8000-0026BB765291",
+            "iid": 7,
+            "perms": ["pw"],
+            "format": "bool",
+            "description": "Identify"
+          }
+        ]
+      },
+      {
+        "iid": 25,
+        "type": "000000A2-0000-1000-8000-0026BB765291",
+        "characteristics": [
+          {
+            "type": "00000037-0000-1000-8000-0026BB765291",
+            "iid": 26,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "1.1.0",
+            "description": "Version",
+            "maxLen": 64
+          }
+        ]
+      },
+      {
+        "iid": 27,
+        "type": "EA22EA53-6227-55EA-AC24-73ACF3EEA0E8",
+        "characteristics": [
+          {
+            "type": "4D05AE82-5A22-5BD6-A730-B7F8B4F3218D",
+            "iid": 28,
+            "perms": ["pw"],
+            "format": "bool"
+          },
+          {
+            "type": "00F44C18-042E-5C4E-9A4C-561D44DCD804",
+            "iid": 29,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "g262d1a",
+            "maxLen": 64
+          }
+        ]
+      },
+      {
+        "iid": 24,
+        "type": "0000008D-0000-1000-8000-0026BB765291",
+        "characteristics": [
+          {
+            "type": "00000095-0000-1000-8000-0026BB765291",
+            "iid": 8,
+            "perms": ["pr", "ev"],
+            "format": "uint8",
+            "value": 1,
+            "description": "Air Quality",
+            "minValue": 0,
+            "maxValue": 5
+          },
+          {
+            "type": "00000023-0000-1000-8000-0026BB765291",
+            "iid": 9,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Air quality sensor",
+            "description": "Name",
+            "maxLen": 64
+          }
+        ]
+      },
+      {
+        "iid": 10,
+        "type": "00000097-0000-1000-8000-0026BB765291",
+        "characteristics": [
+          {
+            "type": "00000092-0000-1000-8000-0026BB765291",
+            "iid": 11,
+            "perms": ["pr", "ev"],
+            "format": "uint8",
+            "value": 0,
+            "description": "Carbon Dioxide Detected",
+            "minValue": 0,
+            "maxValue": 1
+          },
+          {
+            "type": "00000093-0000-1000-8000-0026BB765291",
+            "iid": 12,
+            "perms": ["pr", "ev"],
+            "format": "float",
+            "value": 804,
+            "description": "Carbon Dioxide Level",
+            "minValue": 0,
+            "maxValue": 10000
+          },
+          {
+            "type": "00000023-0000-1000-8000-0026BB765291",
+            "iid": 13,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Carbon Dioxide sensor",
+            "description": "Name",
+            "maxLen": 64
+          }
+        ]
+      },
+      {
+        "iid": 14,
+        "type": "00000082-0000-1000-8000-0026BB765291",
+        "characteristics": [
+          {
+            "type": "00000010-0000-1000-8000-0026BB765291",
+            "iid": 15,
+            "perms": ["pr", "ev"],
+            "format": "float",
+            "value": 59,
+            "description": "Current Relative Humidity",
+            "unit": "percentage",
+            "minValue": 0,
+            "maxValue": 100,
+            "minStep": 1
+          },
+          {
+            "type": "00000023-0000-1000-8000-0026BB765291",
+            "iid": 16,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Humidity sensor",
+            "description": "Name",
+            "maxLen": 64
+          }
+        ]
+      },
+      {
+        "iid": 17,
+        "type": "0000008A-0000-1000-8000-0026BB765291",
+        "characteristics": [
+          {
+            "type": "00000011-0000-1000-8000-0026BB765291",
+            "iid": 18,
+            "perms": ["pr", "ev"],
+            "format": "float",
+            "value": 22.9,
+            "description": "Current Temperature",
+            "unit": "celsius",
+            "minValue": 0,
+            "maxValue": 50,
+            "minStep": 0.1
+          },
+          {
+            "type": "00000023-0000-1000-8000-0026BB765291",
+            "iid": 19,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Temperature sensor",
+            "description": "Name",
+            "maxLen": 64
+          }
+        ]
+      },
+      {
+        "iid": 20,
+        "type": "6237CEFC-9F4D-54B2-8033-2EDA0053B811",
+        "characteristics": [
+          {
+            "type": "B3BBFABC-D78C-5B8D-948C-5DAC1EE2CDE5",
+            "iid": 21,
+            "perms": ["pr", "ev"],
+            "format": "uint8",
+            "value": 0,
+            "minValue": 0,
+            "maxValue": 200,
+            "minStep": 1
+          },
+          {
+            "type": "627EA399-29D9-5DC8-9A02-08AE928F73D8",
+            "iid": 22,
+            "perms": ["pr", "ev"],
+            "format": "uint8",
+            "value": 0,
+            "minValue": 0,
+            "maxValue": 5,
+            "minStep": 1
+          },
+          {
+            "type": "00000023-0000-1000-8000-0026BB765291",
+            "iid": 23,
+            "perms": ["pr"],
+            "format": "string",
+            "value": "Noise sensor",
+            "description": "Name",
+            "maxLen": 64
+          }
+        ]
+      }
+    ]
+  }
+]
diff --git a/tests/components/homekit_controller/specific_devices/test_netatmo_home_coach.py b/tests/components/homekit_controller/specific_devices/test_netatmo_home_coach.py
new file mode 100644
index 00000000000..5769cc81129
--- /dev/null
+++ b/tests/components/homekit_controller/specific_devices/test_netatmo_home_coach.py
@@ -0,0 +1,45 @@
+"""
+Regression tests for Netamo Healthy Home Coach.
+
+https://github.com/home-assistant/core/issues/73360
+"""
+from homeassistant.components.sensor import SensorStateClass
+
+from ..common import (
+    HUB_TEST_ACCESSORY_ID,
+    DeviceTestInfo,
+    EntityTestInfo,
+    assert_devices_and_entities_created,
+    setup_accessories_from_file,
+    setup_test_accessories,
+)
+
+
+async def test_netamo_smart_co_alarm_setup(hass):
+    """Test that a Netamo Smart CO Alarm can be correctly setup in HA."""
+    accessories = await setup_accessories_from_file(hass, "netatmo_home_coach.json")
+    await setup_test_accessories(hass, accessories)
+
+    await assert_devices_and_entities_created(
+        hass,
+        DeviceTestInfo(
+            unique_id=HUB_TEST_ACCESSORY_ID,
+            name="Healthy Home Coach",
+            model="Healthy Home Coach",
+            manufacturer="Netatmo",
+            sw_version="59",
+            hw_version="",
+            serial_number="1234",
+            devices=[],
+            entities=[
+                EntityTestInfo(
+                    entity_id="sensor.healthy_home_coach_noise",
+                    friendly_name="Healthy Home Coach Noise",
+                    unique_id="00:00:00:00:00:00_1_20_21",
+                    state="0",
+                    unit_of_measurement="dB",
+                    capabilities={"state_class": SensorStateClass.MEASUREMENT},
+                ),
+            ],
+        ),
+    )
-- 
GitLab


From 838691f22f27852a05313809cdf9c51094ad3798 Mon Sep 17 00:00:00 2001
From: Martin Hjelmare <marhje52@gmail.com>
Date: Mon, 24 Oct 2022 18:21:05 +0200
Subject: [PATCH 755/985] Refactor zwave_js add-on manager (#80883)

* Make addon slug an instance attribute

* Extract addon name and addon config

* Update docstrings
---
 homeassistant/components/zwave_js/__init__.py |  20 +-
 homeassistant/components/zwave_js/addon.py    | 192 ++++++++----------
 tests/components/zwave_js/test_addon.py       |  17 +-
 3 files changed, 112 insertions(+), 117 deletions(-)

diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py
index 9082048badf..cab07f4287f 100644
--- a/homeassistant/components/zwave_js/__init__.py
+++ b/homeassistant/components/zwave_js/__init__.py
@@ -854,24 +854,24 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) ->
     s2_unauthenticated_key: str = entry.data.get(CONF_S2_UNAUTHENTICATED_KEY, "")
     addon_state = addon_info.state
 
+    addon_config = {
+        CONF_ADDON_DEVICE: usb_path,
+        CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key,
+        CONF_ADDON_S2_ACCESS_CONTROL_KEY: s2_access_control_key,
+        CONF_ADDON_S2_AUTHENTICATED_KEY: s2_authenticated_key,
+        CONF_ADDON_S2_UNAUTHENTICATED_KEY: s2_unauthenticated_key,
+    }
+
     if addon_state == AddonState.NOT_INSTALLED:
         addon_manager.async_schedule_install_setup_addon(
-            usb_path,
-            s0_legacy_key,
-            s2_access_control_key,
-            s2_authenticated_key,
-            s2_unauthenticated_key,
+            addon_config,
             catch_error=True,
         )
         raise ConfigEntryNotReady
 
     if addon_state == AddonState.NOT_RUNNING:
         addon_manager.async_schedule_setup_addon(
-            usb_path,
-            s0_legacy_key,
-            s2_access_control_key,
-            s2_authenticated_key,
-            s2_unauthenticated_key,
+            addon_config,
             catch_error=True,
         )
         raise ConfigEntryNotReady
diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py
index 610fc850e90..3e27235ef84 100644
--- a/homeassistant/components/zwave_js/addon.py
+++ b/homeassistant/components/zwave_js/addon.py
@@ -5,10 +5,10 @@ import asyncio
 from collections.abc import Awaitable, Callable, Coroutine
 from dataclasses import dataclass
 from enum import Enum
-from functools import partial
+from functools import partial, wraps
 from typing import Any, TypeVar
 
-from typing_extensions import ParamSpec
+from typing_extensions import Concatenate, ParamSpec
 
 from homeassistant.components.hassio import (
     async_create_backup,
@@ -28,17 +28,9 @@ from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.singleton import singleton
 
-from .const import (
-    ADDON_SLUG,
-    CONF_ADDON_DEVICE,
-    CONF_ADDON_S0_LEGACY_KEY,
-    CONF_ADDON_S2_ACCESS_CONTROL_KEY,
-    CONF_ADDON_S2_AUTHENTICATED_KEY,
-    CONF_ADDON_S2_UNAUTHENTICATED_KEY,
-    DOMAIN,
-    LOGGER,
-)
+from .const import ADDON_SLUG, DOMAIN, LOGGER
 
+_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager")
 _R = TypeVar("_R")
 _P = ParamSpec("_P")
 
@@ -49,25 +41,33 @@ DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
 @callback
 def get_addon_manager(hass: HomeAssistant) -> AddonManager:
     """Get the add-on manager."""
-    return AddonManager(hass)
+    return AddonManager(hass, "Z-Wave JS", ADDON_SLUG)
 
 
 def api_error(
     error_message: str,
-) -> Callable[[Callable[_P, Awaitable[_R]]], Callable[_P, Coroutine[Any, Any, _R]]]:
+) -> Callable[
+    [Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]],
+    Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]],
+]:
     """Handle HassioAPIError and raise a specific AddonError."""
 
     def handle_hassio_api_error(
-        func: Callable[_P, Awaitable[_R]]
-    ) -> Callable[_P, Coroutine[Any, Any, _R]]:
+        func: Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]
+    ) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]:
         """Handle a HassioAPIError."""
 
-        async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
+        @wraps(func)
+        async def wrapper(
+            self: _AddonManagerT, *args: _P.args, **kwargs: _P.kwargs
+        ) -> _R:
             """Wrap an add-on manager method."""
             try:
-                return_value = await func(*args, **kwargs)
+                return_value = await func(self, *args, **kwargs)
             except HassioAPIError as err:
-                raise AddonError(f"{error_message}: {err}") from err
+                raise AddonError(
+                    f"{error_message.format(addon_name=self.addon_name)}: {err}"
+                ) from err
 
             return return_value
 
@@ -100,12 +100,14 @@ class AddonManager:
     """Manage the add-on.
 
     Methods may raise AddonError.
-    Only one instance of this class may exist
+    Only one instance of this class may exist per add-on
     to keep track of running add-on tasks.
     """
 
-    def __init__(self, hass: HomeAssistant) -> None:
+    def __init__(self, hass: HomeAssistant, addon_name: str, addon_slug: str) -> None:
         """Set up the add-on manager."""
+        self.addon_name = addon_name
+        self.addon_slug = addon_slug
         self._hass = hass
         self._install_task: asyncio.Task | None = None
         self._restart_task: asyncio.Task | None = None
@@ -123,21 +125,23 @@ class AddonManager:
             )
         )
 
-    @api_error("Failed to get Z-Wave JS add-on discovery info")
+    @api_error("Failed to get {addon_name} add-on discovery info")
     async def async_get_addon_discovery_info(self) -> dict:
         """Return add-on discovery info."""
-        discovery_info = await async_get_addon_discovery_info(self._hass, ADDON_SLUG)
+        discovery_info = await async_get_addon_discovery_info(
+            self._hass, self.addon_slug
+        )
 
         if not discovery_info:
-            raise AddonError("Failed to get Z-Wave JS add-on discovery info")
+            raise AddonError(f"Failed to get {self.addon_name} add-on discovery info")
 
         discovery_info_config: dict = discovery_info["config"]
         return discovery_info_config
 
-    @api_error("Failed to get the Z-Wave JS add-on info")
+    @api_error("Failed to get the {addon_name} add-on info")
     async def async_get_addon_info(self) -> AddonInfo:
-        """Return and cache Z-Wave JS add-on info."""
-        addon_store_info = await async_get_addon_store_info(self._hass, ADDON_SLUG)
+        """Return and cache manager add-on info."""
+        addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug)
         LOGGER.debug("Add-on store info: %s", addon_store_info)
         if not addon_store_info["installed"]:
             return AddonInfo(
@@ -147,7 +151,7 @@ class AddonManager:
                 version=None,
             )
 
-        addon_info = await async_get_addon_info(self._hass, ADDON_SLUG)
+        addon_info = await async_get_addon_info(self._hass, self.addon_slug)
         addon_state = self.async_get_addon_state(addon_info)
         return AddonInfo(
             options=addon_info["options"],
@@ -158,7 +162,7 @@ class AddonManager:
 
     @callback
     def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState:
-        """Return the current state of the Z-Wave JS add-on."""
+        """Return the current state of the managed add-on."""
         addon_state = AddonState.NOT_RUNNING
 
         if addon_info["state"] == "started":
@@ -170,25 +174,27 @@ class AddonManager:
 
         return addon_state
 
-    @api_error("Failed to set the Z-Wave JS add-on options")
+    @api_error("Failed to set the {addon_name} add-on options")
     async def async_set_addon_options(self, config: dict) -> None:
-        """Set Z-Wave JS add-on options."""
+        """Set manager add-on options."""
         options = {"options": config}
-        await async_set_addon_options(self._hass, ADDON_SLUG, options)
+        await async_set_addon_options(self._hass, self.addon_slug, options)
 
-    @api_error("Failed to install the Z-Wave JS add-on")
+    @api_error("Failed to install the {addon_name} add-on")
     async def async_install_addon(self) -> None:
-        """Install the Z-Wave JS add-on."""
-        await async_install_addon(self._hass, ADDON_SLUG)
+        """Install the managed add-on."""
+        await async_install_addon(self._hass, self.addon_slug)
 
     @callback
     def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task:
-        """Schedule a task that installs the Z-Wave JS add-on.
+        """Schedule a task that installs the managed add-on.
 
         Only schedule a new install task if the there's no running task.
         """
         if not self._install_task or self._install_task.done():
-            LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
+            LOGGER.info(
+                "%s add-on is not installed. Installing add-on", self.addon_name
+            )
             self._install_task = self._async_schedule_addon_operation(
                 self.async_install_addon, catch_error=catch_error
             )
@@ -197,85 +203,79 @@ class AddonManager:
     @callback
     def async_schedule_install_setup_addon(
         self,
-        usb_path: str,
-        s0_legacy_key: str,
-        s2_access_control_key: str,
-        s2_authenticated_key: str,
-        s2_unauthenticated_key: str,
+        addon_config: dict[str, Any],
         catch_error: bool = False,
     ) -> asyncio.Task:
-        """Schedule a task that installs and sets up the Z-Wave JS add-on.
+        """Schedule a task that installs and sets up the managed add-on.
 
         Only schedule a new install task if the there's no running task.
         """
         if not self._install_task or self._install_task.done():
-            LOGGER.info("Z-Wave JS add-on is not installed. Installing add-on")
+            LOGGER.info(
+                "%s add-on is not installed. Installing add-on", self.addon_name
+            )
             self._install_task = self._async_schedule_addon_operation(
                 self.async_install_addon,
                 partial(
                     self.async_configure_addon,
-                    usb_path,
-                    s0_legacy_key,
-                    s2_access_control_key,
-                    s2_authenticated_key,
-                    s2_unauthenticated_key,
+                    addon_config,
                 ),
                 self.async_start_addon,
                 catch_error=catch_error,
             )
         return self._install_task
 
-    @api_error("Failed to uninstall the Z-Wave JS add-on")
+    @api_error("Failed to uninstall the {addon_name} add-on")
     async def async_uninstall_addon(self) -> None:
-        """Uninstall the Z-Wave JS add-on."""
-        await async_uninstall_addon(self._hass, ADDON_SLUG)
+        """Uninstall the managed add-on."""
+        await async_uninstall_addon(self._hass, self.addon_slug)
 
-    @api_error("Failed to update the Z-Wave JS add-on")
+    @api_error("Failed to update the {addon_name} add-on")
     async def async_update_addon(self) -> None:
-        """Update the Z-Wave JS add-on if needed."""
+        """Update the managed add-on if needed."""
         addon_info = await self.async_get_addon_info()
 
         if addon_info.state is AddonState.NOT_INSTALLED:
-            raise AddonError("Z-Wave JS add-on is not installed")
+            raise AddonError(f"{self.addon_name} add-on is not installed")
 
         if not addon_info.update_available:
             return
 
         await self.async_create_backup()
-        await async_update_addon(self._hass, ADDON_SLUG)
+        await async_update_addon(self._hass, self.addon_slug)
 
     @callback
     def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task:
-        """Schedule a task that updates and sets up the Z-Wave JS add-on.
+        """Schedule a task that updates and sets up the managed add-on.
 
         Only schedule a new update task if the there's no running task.
         """
         if not self._update_task or self._update_task.done():
-            LOGGER.info("Trying to update the Z-Wave JS add-on")
+            LOGGER.info("Trying to update the %s add-on", self.addon_name)
             self._update_task = self._async_schedule_addon_operation(
                 self.async_update_addon,
                 catch_error=catch_error,
             )
         return self._update_task
 
-    @api_error("Failed to start the Z-Wave JS add-on")
+    @api_error("Failed to start the {addon_name} add-on")
     async def async_start_addon(self) -> None:
-        """Start the Z-Wave JS add-on."""
-        await async_start_addon(self._hass, ADDON_SLUG)
+        """Start the managed add-on."""
+        await async_start_addon(self._hass, self.addon_slug)
 
-    @api_error("Failed to restart the Z-Wave JS add-on")
+    @api_error("Failed to restart the {addon_name} add-on")
     async def async_restart_addon(self) -> None:
-        """Restart the Z-Wave JS add-on."""
-        await async_restart_addon(self._hass, ADDON_SLUG)
+        """Restart the managed add-on."""
+        await async_restart_addon(self._hass, self.addon_slug)
 
     @callback
     def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task:
-        """Schedule a task that starts the Z-Wave JS add-on.
+        """Schedule a task that starts the managed add-on.
 
         Only schedule a new start task if the there's no running task.
         """
         if not self._start_task or self._start_task.done():
-            LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
+            LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name)
             self._start_task = self._async_schedule_addon_operation(
                 self.async_start_addon, catch_error=catch_error
             )
@@ -283,87 +283,67 @@ class AddonManager:
 
     @callback
     def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task:
-        """Schedule a task that restarts the Z-Wave JS add-on.
+        """Schedule a task that restarts the managed add-on.
 
         Only schedule a new restart task if the there's no running task.
         """
         if not self._restart_task or self._restart_task.done():
-            LOGGER.info("Restarting Z-Wave JS add-on")
+            LOGGER.info("Restarting %s add-on", self.addon_name)
             self._restart_task = self._async_schedule_addon_operation(
                 self.async_restart_addon, catch_error=catch_error
             )
         return self._restart_task
 
-    @api_error("Failed to stop the Z-Wave JS add-on")
+    @api_error("Failed to stop the {addon_name} add-on")
     async def async_stop_addon(self) -> None:
-        """Stop the Z-Wave JS add-on."""
-        await async_stop_addon(self._hass, ADDON_SLUG)
+        """Stop the managed add-on."""
+        await async_stop_addon(self._hass, self.addon_slug)
 
     async def async_configure_addon(
         self,
-        usb_path: str,
-        s0_legacy_key: str,
-        s2_access_control_key: str,
-        s2_authenticated_key: str,
-        s2_unauthenticated_key: str,
+        addon_config: dict[str, Any],
     ) -> None:
-        """Configure and start Z-Wave JS add-on."""
+        """Configure and start manager add-on."""
         addon_info = await self.async_get_addon_info()
 
         if addon_info.state is AddonState.NOT_INSTALLED:
-            raise AddonError("Z-Wave JS add-on is not installed")
-
-        new_addon_options = {
-            CONF_ADDON_DEVICE: usb_path,
-            CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key,
-            CONF_ADDON_S2_ACCESS_CONTROL_KEY: s2_access_control_key,
-            CONF_ADDON_S2_AUTHENTICATED_KEY: s2_authenticated_key,
-            CONF_ADDON_S2_UNAUTHENTICATED_KEY: s2_unauthenticated_key,
-        }
+            raise AddonError(f"{self.addon_name} add-on is not installed")
 
-        if new_addon_options != addon_info.options:
-            await self.async_set_addon_options(new_addon_options)
+        if addon_config != addon_info.options:
+            await self.async_set_addon_options(addon_config)
 
     @callback
     def async_schedule_setup_addon(
         self,
-        usb_path: str,
-        s0_legacy_key: str,
-        s2_access_control_key: str,
-        s2_authenticated_key: str,
-        s2_unauthenticated_key: str,
+        addon_config: dict[str, Any],
         catch_error: bool = False,
     ) -> asyncio.Task:
-        """Schedule a task that configures and starts the Z-Wave JS add-on.
+        """Schedule a task that configures and starts the managed add-on.
 
-        Only schedule a new setup task if the there's no running task.
+        Only schedule a new setup task if there's no running task.
         """
         if not self._start_task or self._start_task.done():
-            LOGGER.info("Z-Wave JS add-on is not running. Starting add-on")
+            LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name)
             self._start_task = self._async_schedule_addon_operation(
                 partial(
                     self.async_configure_addon,
-                    usb_path,
-                    s0_legacy_key,
-                    s2_access_control_key,
-                    s2_authenticated_key,
-                    s2_unauthenticated_key,
+                    addon_config,
                 ),
                 self.async_start_addon,
                 catch_error=catch_error,
             )
         return self._start_task
 
-    @api_error("Failed to create a backup of the Z-Wave JS add-on.")
+    @api_error("Failed to create a backup of the {addon_name} add-on.")
     async def async_create_backup(self) -> None:
-        """Create a partial backup of the Z-Wave JS add-on."""
+        """Create a partial backup of the managed add-on."""
         addon_info = await self.async_get_addon_info()
-        name = f"addon_{ADDON_SLUG}_{addon_info.version}"
+        name = f"addon_{self.addon_slug}_{addon_info.version}"
 
         LOGGER.debug("Creating backup: %s", name)
         await async_create_backup(
             self._hass,
-            {"name": name, "addons": [ADDON_SLUG]},
+            {"name": name, "addons": [self.addon_slug]},
             partial=True,
         )
 
@@ -388,4 +368,4 @@ class AddonManager:
 
 
 class AddonError(HomeAssistantError):
-    """Represent an error with Z-Wave JS add-on."""
+    """Represent an error with the managed add-on."""
diff --git a/tests/components/zwave_js/test_addon.py b/tests/components/zwave_js/test_addon.py
index 754be808cea..45f732c1aa2 100644
--- a/tests/components/zwave_js/test_addon.py
+++ b/tests/components/zwave_js/test_addon.py
@@ -2,14 +2,29 @@
 import pytest
 
 from homeassistant.components.zwave_js.addon import AddonError, get_addon_manager
+from homeassistant.components.zwave_js.const import (
+    CONF_ADDON_DEVICE,
+    CONF_ADDON_S0_LEGACY_KEY,
+    CONF_ADDON_S2_ACCESS_CONTROL_KEY,
+    CONF_ADDON_S2_AUTHENTICATED_KEY,
+    CONF_ADDON_S2_UNAUTHENTICATED_KEY,
+)
 
 
 async def test_not_installed_raises_exception(hass, addon_not_installed):
     """Test addon not installed raises exception."""
     addon_manager = get_addon_manager(hass)
 
+    addon_config = {
+        CONF_ADDON_DEVICE: "/test",
+        CONF_ADDON_S0_LEGACY_KEY: "123",
+        CONF_ADDON_S2_ACCESS_CONTROL_KEY: "456",
+        CONF_ADDON_S2_AUTHENTICATED_KEY: "789",
+        CONF_ADDON_S2_UNAUTHENTICATED_KEY: "012",
+    }
+
     with pytest.raises(AddonError):
-        await addon_manager.async_configure_addon("/test", "123", "456", "789", "012")
+        await addon_manager.async_configure_addon(addon_config)
 
     with pytest.raises(AddonError):
         await addon_manager.async_update_addon()
-- 
GitLab


From c474db66177653daac8f4071c973e72e499bad2a Mon Sep 17 00:00:00 2001
From: rikroe <42204099+rikroe@users.noreply.github.com>
Date: Mon, 24 Oct 2022 19:38:15 +0200
Subject: [PATCH 756/985] Add EMISSION_CHECK to BMW Connected Drive (#80819)

---
 homeassistant/components/bmw_connected_drive/binary_sensor.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py
index 2c8543bd72a..43beb2cbf81 100644
--- a/homeassistant/components/bmw_connected_drive/binary_sensor.py
+++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py
@@ -30,6 +30,7 @@ _LOGGER = logging.getLogger(__name__)
 
 ALLOWED_CONDITION_BASED_SERVICE_KEYS = {
     "BRAKE_FLUID",
+    "EMISSION_CHECK",
     "ENGINE_OIL",
     "OIL",
     "TIRE_WEAR_FRONT",
-- 
GitLab


From c5688072fdb9ce40eea55648fb3b95c043433030 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 24 Oct 2022 12:40:13 -0500
Subject: [PATCH 757/985] Bump aiohomekit to 2.2.4 (#80899)

---
 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 99b80df9157..0bad4ed9f7b 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.3"],
+  "requirements": ["aiohomekit==2.2.4"],
   "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 3fbcb97bdc1..e2040e1308e 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.3
+aiohomekit==2.2.4
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f2609718373..90042d3eded 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.3
+aiohomekit==2.2.4
 
 # homeassistant.components.emulated_hue
 # homeassistant.components.http
-- 
GitLab


From f7694c055064029c396bb456377f3d4c0673f0ba Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Mon, 24 Oct 2022 20:47:06 +0200
Subject: [PATCH 758/985] Only reload modified scripts (#80470)

Co-authored-by: Franck Nijhof <git@frenck.dev>
---
 homeassistant/components/script/__init__.py | 103 +++++++++++++--
 tests/components/automation/test_init.py    |   3 +-
 tests/components/script/test_init.py        | 131 +++++++++++++++++++-
 3 files changed, 227 insertions(+), 10 deletions(-)

diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py
index a1c683faba7..359486d2687 100644
--- a/homeassistant/components/script/__init__.py
+++ b/homeassistant/components/script/__init__.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import asyncio
+from dataclasses import dataclass
 import logging
 from typing import Any, cast
 
@@ -180,7 +181,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     async def reload_service(service: ServiceCall) -> None:
         """Call a service to reload scripts."""
         await async_get_blueprints(hass).async_reset_cache()
-        if (conf := await component.async_prepare_reload()) is None:
+        if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
             return
         await _async_process_config(hass, conf, component)
 
@@ -231,12 +232,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     return True
 
 
-async def _async_process_config(hass, config, component) -> None:
-    """Process script configuration.
+@dataclass
+class ScriptEntityConfig:
+    """Container for prepared script entity configuration."""
+
+    config_block: ConfigType
+    key: str
+    raw_blueprint_inputs: ConfigType | None
+    raw_config: ConfigType | None
 
-    Return true, if Blueprints were used.
-    """
-    entities = []
+
+async def _prepare_script_config(
+    hass: HomeAssistant,
+    config: ConfigType,
+) -> list[ScriptEntityConfig]:
+    """Parse configuration and prepare script entity configuration."""
+    script_configs: list[ScriptEntityConfig] = []
 
     conf: dict[str, dict[str, Any] | BlueprintInputs] = config[DOMAIN]
 
@@ -265,10 +276,86 @@ async def _async_process_config(hass, config, component) -> None:
         else:
             raw_config = cast(ScriptConfig, config_block).raw_config
 
-        entities.append(
-            ScriptEntity(hass, key, config_block, raw_config, raw_blueprint_inputs)
+        script_configs.append(
+            ScriptEntityConfig(config_block, key, raw_blueprint_inputs, raw_config)
+        )
+
+    return script_configs
+
+
+async def _create_script_entities(
+    hass: HomeAssistant, script_configs: list[ScriptEntityConfig]
+) -> list[ScriptEntity]:
+    """Create script entities from prepared configuration."""
+    entities: list[ScriptEntity] = []
+
+    for script_config in script_configs:
+
+        entity = ScriptEntity(
+            hass,
+            script_config.key,
+            script_config.config_block,
+            script_config.raw_config,
+            script_config.raw_blueprint_inputs,
         )
+        entities.append(entity)
 
+    return entities
+
+
+async def _async_process_config(hass, config, component) -> None:
+    """Process script configuration."""
+    entities = []
+
+    def script_matches_config(script: ScriptEntity, config: ScriptEntityConfig) -> bool:
+        return script.unique_id == config.key and script.raw_config == config.raw_config
+
+    def find_matches(
+        scripts: list[ScriptEntity],
+        script_configs: list[ScriptEntityConfig],
+    ) -> tuple[set[int], set[int]]:
+        """Find matches between a list of script entities and a list of configurations.
+
+        A script or configuration is only allowed to match at most once to handle
+        the case of multiple scripts with identical configuration.
+
+        Returns a tuple of sets of indices: ({script_matches}, {config_matches})
+        """
+        script_matches: set[int] = set()
+        config_matches: set[int] = set()
+
+        for script_idx, script in enumerate(scripts):
+            for config_idx, config in enumerate(script_configs):
+                if config_idx in config_matches:
+                    # Only allow a script config to match at most once
+                    continue
+                if script_matches_config(script, config):
+                    script_matches.add(script_idx)
+                    config_matches.add(config_idx)
+                    # Only allow a script to match at most once
+                    break
+
+        return script_matches, config_matches
+
+    script_configs = await _prepare_script_config(hass, config)
+    scripts: list[ScriptEntity] = list(component.entities)
+
+    # Find scripts and configurations which have matches
+    script_matches, config_matches = find_matches(scripts, script_configs)
+
+    # Remove scripts which have changed config or no longer exist
+    tasks = [
+        script.async_remove()
+        for idx, script in enumerate(scripts)
+        if idx not in script_matches
+    ]
+    await asyncio.gather(*tasks)
+
+    # Create scripts which have changed config or have been added
+    updated_script_configs = [
+        config for idx, config in enumerate(script_configs) if idx not in config_matches
+    ]
+    entities = await _create_script_entities(hass, updated_script_configs)
     await component.async_add_entities(entities)
 
 
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index 858f3de6549..f40309bf7f6 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -739,7 +739,7 @@ async def test_automation_stops(hass, calls, service):
 
 
 async def test_reload_unchanged_does_not_stop(hass, calls):
-    """Test that turning off / reloading stops any running actions as appropriate."""
+    """Test that reloading stops any running actions as appropriate."""
     test_entity = "test.entity"
 
     config = {
@@ -766,6 +766,7 @@ async def test_reload_unchanged_does_not_stop(hass, calls):
 
     hass.bus.async_fire("test_event")
     await running.wait()
+    assert len(calls) == 0
 
     with patch(
         "homeassistant.config.load_yaml_config_file",
diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py
index b48a65275b7..09d2c3c70b1 100644
--- a/tests/components/script/test_init.py
+++ b/tests/components/script/test_init.py
@@ -7,7 +7,7 @@ from unittest.mock import Mock, patch
 import pytest
 
 from homeassistant.components import script
-from homeassistant.components.script import DOMAIN, EVENT_SCRIPT_STARTED
+from homeassistant.components.script import DOMAIN, EVENT_SCRIPT_STARTED, ScriptEntity
 from homeassistant.const import (
     ATTR_ENTITY_ID,
     ATTR_NAME,
@@ -46,6 +46,12 @@ from tests.components.logbook.common import MockRow, mock_humanify
 ENTITY_ID = "script.test"
 
 
+@pytest.fixture
+def calls(hass):
+    """Track calls to a mock service."""
+    return async_mock_service(hass, "test", "script")
+
+
 async def test_passing_variables(hass):
     """Test different ways of passing in variables."""
     mock_restore_cache(hass, ())
@@ -219,6 +225,129 @@ async def test_reload_service(hass, running):
         assert hass.services.has_service(script.DOMAIN, "test")
 
 
+async def test_reload_unchanged_does_not_stop(hass, calls):
+    """Test that reloading stops any running actions as appropriate."""
+    test_entity = "test.entity"
+
+    config = {
+        script.DOMAIN: {
+            "test": {
+                "sequence": [
+                    {"event": "running"},
+                    {"wait_template": "{{ is_state('test.entity', 'goodbye') }}"},
+                    {"service": "test.script"},
+                ],
+            }
+        }
+    }
+    assert await async_setup_component(hass, script.DOMAIN, config)
+
+    assert hass.states.get(ENTITY_ID) is not None
+    assert hass.services.has_service(script.DOMAIN, "test")
+
+    running = asyncio.Event()
+
+    @callback
+    def running_cb(event):
+        running.set()
+
+    hass.bus.async_listen_once("running", running_cb)
+    hass.states.async_set(test_entity, "hello")
+
+    # Start the script and wait for it to start
+    _, object_id = split_entity_id(ENTITY_ID)
+    await hass.services.async_call(DOMAIN, object_id)
+    await running.wait()
+    assert len(calls) == 0
+
+    with patch(
+        "homeassistant.config.load_yaml_config_file",
+        autospec=True,
+        return_value=config,
+    ):
+        await hass.services.async_call(script.DOMAIN, SERVICE_RELOAD, blocking=True)
+
+    hass.states.async_set(test_entity, "goodbye")
+    await hass.async_block_till_done()
+
+    assert len(calls) == 1
+
+
+@pytest.mark.parametrize(
+    "script_config",
+    (
+        {
+            "test": {
+                "sequence": [{"service": "test.script"}],
+            }
+        },
+        # A script using templates
+        {
+            "test": {
+                "sequence": [{"service": "{{ 'test.script' }}"}],
+            }
+        },
+        # A script using blueprint
+        {
+            "test": {
+                "use_blueprint": {
+                    "path": "test_service.yaml",
+                    "input": {
+                        "service_to_call": "test.script",
+                    },
+                }
+            }
+        },
+        # A script using blueprint with templated input
+        {
+            "test": {
+                "use_blueprint": {
+                    "path": "test_service.yaml",
+                    "input": {
+                        "service_to_call": "{{ 'test.script' }}",
+                    },
+                }
+            }
+        },
+    ),
+)
+async def test_reload_unchanged_script(hass, calls, script_config):
+    """Test an unmodified script is not reloaded."""
+    with patch(
+        "homeassistant.components.script.ScriptEntity", wraps=ScriptEntity
+    ) as script_entity_init:
+        config = {script.DOMAIN: [script_config]}
+        assert await async_setup_component(hass, script.DOMAIN, config)
+        assert hass.states.get(ENTITY_ID) is not None
+        assert hass.services.has_service(script.DOMAIN, "test")
+
+        assert script_entity_init.call_count == 1
+        script_entity_init.reset_mock()
+
+        # Start the script and wait for it to finish
+        _, object_id = split_entity_id(ENTITY_ID)
+        await hass.services.async_call(DOMAIN, object_id)
+        await hass.async_block_till_done()
+        assert len(calls) == 1
+
+        # Reload the scripts without any change
+        with patch(
+            "homeassistant.config.load_yaml_config_file",
+            autospec=True,
+            return_value=config,
+        ):
+            await hass.services.async_call(script.DOMAIN, SERVICE_RELOAD, blocking=True)
+
+        assert script_entity_init.call_count == 0
+        script_entity_init.reset_mock()
+
+        # Start the script and wait for it to start
+        _, object_id = split_entity_id(ENTITY_ID)
+        await hass.services.async_call(DOMAIN, object_id)
+        await hass.async_block_till_done()
+        assert len(calls) == 2
+
+
 async def test_service_descriptions(hass):
     """Test that service descriptions are loaded and reloaded correctly."""
     # Test 1: has "description" but no "fields"
-- 
GitLab


From 66b473bae26d18e52200886aeb5c663ec2b88e7c Mon Sep 17 00:00:00 2001
From: shbatm <support@shbatm.com>
Date: Mon, 24 Oct 2022 14:02:23 -0500
Subject: [PATCH 759/985] Add integration_type to ISY994 manifest and bump
 pyisy to 3.0.8 (#80906)

---
 homeassistant/components/isy994/manifest.json | 3 ++-
 requirements_all.txt                          | 2 +-
 requirements_test_all.txt                     | 2 +-
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json
index f3620ec9663..d1c18fce595 100644
--- a/homeassistant/components/isy994/manifest.json
+++ b/homeassistant/components/isy994/manifest.json
@@ -1,8 +1,9 @@
 {
   "domain": "isy994",
   "name": "Universal Devices ISY994",
+  "integration_type": "hub",
   "documentation": "https://www.home-assistant.io/integrations/isy994",
-  "requirements": ["pyisy==3.0.7"],
+  "requirements": ["pyisy==3.0.8"],
   "codeowners": ["@bdraco", "@shbatm"],
   "config_flow": true,
   "ssdp": [
diff --git a/requirements_all.txt b/requirements_all.txt
index e2040e1308e..0abe3b8e32e 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1640,7 +1640,7 @@ pyirishrail==0.0.2
 pyiss==1.0.1
 
 # homeassistant.components.isy994
-pyisy==3.0.7
+pyisy==3.0.8
 
 # homeassistant.components.itach
 pyitachip2ir==0.0.7
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 90042d3eded..6c651570228 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1153,7 +1153,7 @@ pyiqvia==2022.04.0
 pyiss==1.0.1
 
 # homeassistant.components.isy994
-pyisy==3.0.7
+pyisy==3.0.8
 
 # homeassistant.components.kaleidescape
 pykaleidescape==1.0.1
-- 
GitLab


From 27a61f54076f649be351711dea52a10af2b62898 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 24 Oct 2022 14:04:50 -0500
Subject: [PATCH 760/985] Raise exception when esphome ble client disconnects
 during operation (#80885)

---
 homeassistant/components/esphome/__init__.py  |  1 +
 .../components/esphome/bluetooth/client.py    | 23 +++++++++++++++++--
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index 8846007374e..f2924558716 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -468,6 +468,7 @@ async def _cleanup_instance(
     """Cleanup the esphome client if it exists."""
     domain_data = DomainData.get(hass)
     data = domain_data.pop_entry_data(entry)
+    data.available = False
     for disconnect_cb in data.disconnect_callbacks:
         disconnect_cb()
     data.disconnect_callbacks = []
diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py
index eda75436502..9094186226f 100644
--- a/homeassistant/components/esphome/bluetooth/client.py
+++ b/homeassistant/components/esphome/bluetooth/client.py
@@ -47,9 +47,23 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
     async def _async_wrap_bluetooth_connected_operation(
         self: "ESPHomeClient", *args: Any, **kwargs: Any
     ) -> Any:
-        if not self._is_connected:  # pylint: disable=protected-access
+        disconnected_event = (
+            self._disconnected_event  # pylint: disable=protected-access
+        )
+        if not disconnected_event:
             raise BleakError("Not connected")
-        return await func(self, *args, **kwargs)
+        task = asyncio.create_task(func(self, *args, **kwargs))
+        done, _ = await asyncio.wait(
+            (task, disconnected_event.wait()),
+            return_when=asyncio.FIRST_COMPLETED,
+        )
+        if disconnected_event.is_set():
+            task.cancel()
+            raise BleakError(
+                f"{self._ble_device.name} ({self._ble_device.address}): "  # pylint: disable=protected-access
+                "Disconnected during operation"
+            )
+        return next(iter(done)).result()
 
     return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation)
 
@@ -91,6 +105,7 @@ class ESPHomeClient(BaseBleakClient):
         self._mtu: int | None = None
         self._cancel_connection_state: CALLBACK_TYPE | None = None
         self._notify_cancels: dict[int, Callable[[], Coroutine[Any, Any, None]]] = {}
+        self._disconnected_event: asyncio.Event | None = None
 
     def __str__(self) -> str:
         """Return the string representation of the client."""
@@ -114,6 +129,9 @@ class ESPHomeClient(BaseBleakClient):
         _LOGGER.debug("%s: BLE device disconnected", self._source)
         self._is_connected = False
         self.services = BleakGATTServiceCollection()  # type: ignore[no-untyped-call]
+        if self._disconnected_event:
+            self._disconnected_event.set()
+            self._disconnected_event = None
         self._async_call_bleak_disconnected_callback()
         self._unsubscribe_connection_state()
 
@@ -184,6 +202,7 @@ class ESPHomeClient(BaseBleakClient):
         )
         await connected_future
         await self.get_services(dangerous_use_bleak_cache=dangerous_use_bleak_cache)
+        self._disconnected_event = asyncio.Event()
         return True
 
     @api_error_as_bleak_error
-- 
GitLab


From 613ea28b0e94e2cf36f829c1f65494185a2936d7 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 24 Oct 2022 21:06:35 +0200
Subject: [PATCH 761/985] Add new WATER device class (#80886)

---
 homeassistant/components/sensor/__init__.py         | 13 +++++++++++--
 homeassistant/components/sensor/device_condition.py |  2 ++
 homeassistant/components/sensor/device_trigger.py   |  2 ++
 homeassistant/components/sensor/strings.json        |  2 ++
 .../components/sensor/translations/en.json          |  2 ++
 5 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index 7913dd345db..e353f089951 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -318,11 +318,19 @@ class SensorDeviceClass(StrEnum):
 
     Unit of measurement: `VOLUME_*` units
     - SI / metric: `mL`, `L`, `m³`
-    - USCS / imperial: `fl. oz.`, `gal`, `ft³` (warning: volumes expressed in
+    - USCS / imperial: `fl. oz.`, `ft³`, `gal` (warning: volumes expressed in
+    USCS/imperial units are currently assumed to be US volumes)
+    """
+
+    WATER = "water"
+    """Water.
+
+    Unit of measurement:
+    - SI / metric: `m³`, `L`
+    - USCS / imperial: `ft³`, `gal` (warning: volumes expressed in
     USCS/imperial units are currently assumed to be US volumes)
     """
 
-    # weight/mass (g, kg, mg, µg, oz, lb)
     WEIGHT = "weight"
     """Generic weight, represents a measurement of an object's mass.
 
@@ -375,6 +383,7 @@ UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
     SensorDeviceClass.SPEED: SpeedConverter,
     SensorDeviceClass.TEMPERATURE: TemperatureConverter,
     SensorDeviceClass.VOLUME: VolumeConverter,
+    SensorDeviceClass.WATER: VolumeConverter,
     SensorDeviceClass.WEIGHT: MassConverter,
 }
 
diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py
index 6abd0bfbfaa..c1015636d40 100644
--- a/homeassistant/components/sensor/device_condition.py
+++ b/homeassistant/components/sensor/device_condition.py
@@ -63,6 +63,7 @@ CONF_IS_VALUE = "is_value"
 CONF_IS_VOLATILE_ORGANIC_COMPOUNDS = "is_volatile_organic_compounds"
 CONF_IS_VOLTAGE = "is_voltage"
 CONF_IS_VOLUME = "is_volume"
+CONF_IS_WATER = "is_water"
 CONF_IS_WEIGHT = "is_weight"
 
 ENTITY_CONDITIONS = {
@@ -101,6 +102,7 @@ ENTITY_CONDITIONS = {
     ],
     SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}],
     SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_IS_VOLUME}],
+    SensorDeviceClass.WATER: [{CONF_TYPE: CONF_IS_WATER}],
     SensorDeviceClass.WEIGHT: [{CONF_TYPE: CONF_IS_WEIGHT}],
     DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}],
 }
diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py
index 675a59ef78d..c2cda7d8e9d 100644
--- a/homeassistant/components/sensor/device_trigger.py
+++ b/homeassistant/components/sensor/device_trigger.py
@@ -62,6 +62,7 @@ CONF_VALUE = "value"
 CONF_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
 CONF_VOLTAGE = "voltage"
 CONF_VOLUME = "volume"
+CONF_WATER = "water"
 CONF_WEIGHT = "weight"
 
 ENTITY_TRIGGERS = {
@@ -100,6 +101,7 @@ ENTITY_TRIGGERS = {
     ],
     SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}],
     SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_VOLUME}],
+    SensorDeviceClass.WATER: [{CONF_TYPE: CONF_WATER}],
     SensorDeviceClass.WEIGHT: [{CONF_TYPE: CONF_WEIGHT}],
     DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}],
 }
diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json
index 3584777bb5f..12c902816b1 100644
--- a/homeassistant/components/sensor/strings.json
+++ b/homeassistant/components/sensor/strings.json
@@ -33,6 +33,7 @@
       "is_volatile_organic_compounds": "Current {entity_name} volatile organic compounds concentration level",
       "is_voltage": "Current {entity_name} voltage",
       "is_volume": "Current {entity_name} volume",
+      "is_water": "Current {entity_name} water",
       "is_weight": "Current {entity_name} weight"
     },
     "trigger_type": {
@@ -67,6 +68,7 @@
       "volatile_organic_compounds": "{entity_name} volatile organic compounds concentration changes",
       "voltage": "{entity_name} voltage changes",
       "volume": "{entity_name} volume changes",
+      "water": "{entity_name} water changes",
       "weight": "{entity_name} weight changes"
     }
   },
diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json
index 1eeb31aa15c..7fbb6e4a336 100644
--- a/homeassistant/components/sensor/translations/en.json
+++ b/homeassistant/components/sensor/translations/en.json
@@ -32,6 +32,7 @@
             "is_volatile_organic_compounds": "Current {entity_name} volatile organic compounds concentration level",
             "is_voltage": "Current {entity_name} voltage",
             "is_volume": "Current {entity_name} volume",
+            "is_water": "Current {entity_name} water",
             "is_weight": "Current {entity_name} weight"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "{entity_name} volatile organic compounds concentration changes",
             "voltage": "{entity_name} voltage changes",
             "volume": "{entity_name} volume changes",
+            "water": "{entity_name} water changes",
             "weight": "{entity_name} weight changes"
         }
     },
-- 
GitLab


From 9c762a5a5a4e6292f881036f64bf811520f1d84e Mon Sep 17 00:00:00 2001
From: SoCalix <48040807+Socalix@users.noreply.github.com>
Date: Mon, 24 Oct 2022 14:10:56 -0500
Subject: [PATCH 762/985] Fix XMPP room notifications (#80794)

---
 homeassistant/components/xmpp/notify.py | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py
index 6b4faf32458..28d60d317e0 100644
--- a/homeassistant/components/xmpp/notify.py
+++ b/homeassistant/components/xmpp/notify.py
@@ -159,6 +159,9 @@ async def async_send_message(  # noqa: C901
 
         async def start(self, event):
             """Start the communication and sends the message."""
+            if room:
+                _LOGGER.debug("Joining room %s", room)
+                await self.plugin["xep_0045"].join_muc_wait(room, sender, seconds=0)
             # Sending image and message independently from each other
             if data:
                 await self.send_file(timeout=timeout)
@@ -173,9 +176,6 @@ async def async_send_message(  # noqa: C901
             Send XMPP file message using OOB (XEP_0066) and
             HTTP Upload (XEP_0363)
             """
-            if room:
-                self.plugin["xep_0045"].join_muc(room, sender)
-
             try:
                 # Uploading with XEP_0363
                 _LOGGER.debug("Timeout set to %ss", timeout)
@@ -335,8 +335,7 @@ async def async_send_message(  # noqa: C901
             """Send a text only message to a room or a recipient."""
             try:
                 if room:
-                    _LOGGER.debug("Joining room %s", room)
-                    self.plugin["xep_0045"].join_muc(room, sender)
+                    _LOGGER.debug("Sending message to room %s", room)
                     self.send_message(mto=room, mbody=message, mtype="groupchat")
                 else:
                     for recipient in recipients:
-- 
GitLab


From f61c010116f2c63d583e7b097bebfbe547485441 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 24 Oct 2022 14:27:04 -0500
Subject: [PATCH 763/985] Bump bleak-retry-connector to 2.4.2 (#80908)

---
 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 2a3286da13d..a725ecbc70e 100644
--- a/homeassistant/components/bluetooth/manifest.json
+++ b/homeassistant/components/bluetooth/manifest.json
@@ -7,7 +7,7 @@
   "quality_scale": "internal",
   "requirements": [
     "bleak==0.19.0",
-    "bleak-retry-connector==2.4.0",
+    "bleak-retry-connector==2.4.2",
     "bluetooth-adapters==0.6.0",
     "bluetooth-auto-recovery==0.3.6",
     "dbus-fast==1.47.0"
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 29b15503d1a..2aca1d72eba 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.4.0
+bleak-retry-connector==2.4.2
 bleak==0.19.0
 bluetooth-adapters==0.6.0
 bluetooth-auto-recovery==0.3.6
diff --git a/requirements_all.txt b/requirements_all.txt
index 0abe3b8e32e..e8991661ddd 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.4.0
+bleak-retry-connector==2.4.2
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 6c651570228..82e87ca9ae2 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.4.0
+bleak-retry-connector==2.4.2
 
 # homeassistant.components.bluetooth
 bleak==0.19.0
-- 
GitLab


From ac4645a37e01ee27401c74385bd8bd00a88265e1 Mon Sep 17 00:00:00 2001
From: shbatm <support@shbatm.com>
Date: Mon, 24 Oct 2022 14:39:37 -0500
Subject: [PATCH 764/985] Add "Push Flow Meter Data" service to RainMachine and
 bump regenmaschine to 2022.10.0 (#80890)

---
 .../components/rainmachine/__init__.py        | 38 +++++++++++++++++++
 .../components/rainmachine/manifest.json      |  2 +-
 .../components/rainmachine/services.yaml      | 31 +++++++++++++++
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 5 files changed, 72 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py
index 8cc3b3d5e80..a0b11653272 100644
--- a/homeassistant/components/rainmachine/__init__.py
+++ b/homeassistant/components/rainmachine/__init__.py
@@ -20,6 +20,7 @@ from homeassistant.const import (
     CONF_PASSWORD,
     CONF_PORT,
     CONF_SSL,
+    CONF_UNIT_OF_MEASUREMENT,
     Platform,
 )
 from homeassistant.core import HomeAssistant, ServiceCall, callback
@@ -79,9 +80,19 @@ CONF_SECONDS = "seconds"
 CONF_SOLARRAD = "solarrad"
 CONF_TEMPERATURE = "temperature"
 CONF_TIMESTAMP = "timestamp"
+CONF_UNITS = "units"
+CONF_VALUE = "value"
 CONF_WEATHER = "weather"
 CONF_WIND = "wind"
 
+# Config Validator for Flow Meter Data
+CV_FLOW_METER_VALID_UNITS = {
+    "clicks",
+    "gal",
+    "litre",
+    "m3",
+}
+
 # Config Validators for Weather Service Data
 CV_WX_DATA_VALID_PERCENTAGE = vol.All(vol.Coerce(int), vol.Range(min=0, max=100))
 CV_WX_DATA_VALID_TEMP_RANGE = vol.All(vol.Coerce(float), vol.Range(min=-40.0, max=40.0))
@@ -91,6 +102,7 @@ CV_WX_DATA_VALID_PRESSURE = vol.All(vol.Coerce(float), vol.Range(min=60.0, max=1
 CV_WX_DATA_VALID_SOLARRAD = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=5.0))
 
 SERVICE_NAME_PAUSE_WATERING = "pause_watering"
+SERVICE_NAME_PUSH_FLOW_METER_DATA = "push_flow_meter_data"
 SERVICE_NAME_PUSH_WEATHER_DATA = "push_weather_data"
 SERVICE_NAME_RESTRICT_WATERING = "restrict_watering"
 SERVICE_NAME_STOP_ALL = "stop_all"
@@ -109,6 +121,15 @@ SERVICE_PAUSE_WATERING_SCHEMA = SERVICE_SCHEMA.extend(
     }
 )
 
+SERVICE_PUSH_FLOW_METER_DATA_SCHEMA = SERVICE_SCHEMA.extend(
+    {
+        vol.Required(CONF_VALUE): cv.positive_float,
+        vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(
+            cv.string, vol.In(CV_FLOW_METER_VALID_UNITS)
+        ),
+    }
+)
+
 SERVICE_PUSH_WEATHER_DATA_SCHEMA = SERVICE_SCHEMA.extend(
     {
         vol.Optional(CONF_TIMESTAMP): cv.positive_float,
@@ -320,6 +341,17 @@ async def async_setup_entry(  # noqa: C901
         """Pause watering for a set number of seconds."""
         await controller.watering.pause_all(call.data[CONF_SECONDS])
 
+    @call_with_controller(update_programs_and_zones=False)
+    async def async_push_flow_meter_data(
+        call: ServiceCall, controller: Controller
+    ) -> None:
+        """Push flow meter data to the device."""
+        value = call.data[CONF_VALUE]
+        if units := call.data.get(CONF_UNIT_OF_MEASUREMENT):
+            await controller.watering.post_flowmeter(value=value, units=units)
+        else:
+            await controller.watering.post_flowmeter(value=value)
+
     @call_with_controller(update_programs_and_zones=False)
     async def async_push_weather_data(
         call: ServiceCall, controller: Controller
@@ -378,6 +410,11 @@ async def async_setup_entry(  # noqa: C901
             SERVICE_PAUSE_WATERING_SCHEMA,
             async_pause_watering,
         ),
+        (
+            SERVICE_NAME_PUSH_FLOW_METER_DATA,
+            SERVICE_PUSH_FLOW_METER_DATA_SCHEMA,
+            async_push_flow_meter_data,
+        ),
         (
             SERVICE_NAME_PUSH_WEATHER_DATA,
             SERVICE_PUSH_WEATHER_DATA_SCHEMA,
@@ -419,6 +456,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         # defined during integration setup:
         for service_name in (
             SERVICE_NAME_PAUSE_WATERING,
+            SERVICE_NAME_PUSH_FLOW_METER_DATA,
             SERVICE_NAME_PUSH_WEATHER_DATA,
             SERVICE_NAME_RESTRICT_WATERING,
             SERVICE_NAME_STOP_ALL,
diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json
index ca7543bfb38..08fef4973bc 100644
--- a/homeassistant/components/rainmachine/manifest.json
+++ b/homeassistant/components/rainmachine/manifest.json
@@ -3,7 +3,7 @@
   "name": "RainMachine",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/rainmachine",
-  "requirements": ["regenmaschine==2022.09.2"],
+  "requirements": ["regenmaschine==2022.10.0"],
   "codeowners": ["@bachya"],
   "iot_class": "local_polling",
   "homekit": {
diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml
index 198aec94a22..9aa2bb7f50a 100644
--- a/homeassistant/components/rainmachine/services.yaml
+++ b/homeassistant/components/rainmachine/services.yaml
@@ -97,6 +97,37 @@ unpause_watering:
       selector:
         device:
           integration: rainmachine
+push_flow_meter_data:
+  name: Push Flow Meter Data
+  description: Push Flow Meter data to the RainMachine device.
+  fields:
+    device_id:
+      name: Controller
+      description: The controller to send flow meter data to
+      required: true
+      selector:
+        device:
+          integration: rainmachine
+    value:
+      name: Value
+      description: The flow meter value to send
+      required: true
+      selector:
+        number:
+          min: 0.0
+          max: 1000000000.0
+          step: 0.1
+          mode: box
+    unit_of_measurement:
+      name: Unit of Measurement
+      description: The flow meter units to send
+      selector:
+        select:
+          options:
+            - "clicks"
+            - "gal"
+            - "litre"
+            - "m3"
 push_weather_data:
   name: Push Weather Data
   description: >-
diff --git a/requirements_all.txt b/requirements_all.txt
index e8991661ddd..c25834d151b 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2147,7 +2147,7 @@ raincloudy==0.0.7
 raspyrfm-client==1.2.8
 
 # homeassistant.components.rainmachine
-regenmaschine==2022.09.2
+regenmaschine==2022.10.0
 
 # homeassistant.components.renault
 renault-api==0.1.11
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 82e87ca9ae2..f4d4e2ed2ba 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1483,7 +1483,7 @@ radios==0.1.1
 radiotherm==2.1.0
 
 # homeassistant.components.rainmachine
-regenmaschine==2022.09.2
+regenmaschine==2022.10.0
 
 # homeassistant.components.renault
 renault-api==0.1.11
-- 
GitLab


From b5f70a404a8e5b636f536eee7ae02a21ad3c77d4 Mon Sep 17 00:00:00 2001
From: Steven Looman <steven.looman@gmail.com>
Date: Mon, 24 Oct 2022 22:19:19 +0200
Subject: [PATCH 765/985] Fix SSDP/UPnP server after testing (#80815)

---
 homeassistant/components/dlna_dmr/manifest.json  |  2 +-
 homeassistant/components/dlna_dms/manifest.json  |  2 +-
 homeassistant/components/samsungtv/manifest.json |  2 +-
 homeassistant/components/ssdp/__init__.py        | 16 +++++++++++++---
 homeassistant/components/ssdp/manifest.json      |  2 +-
 homeassistant/components/upnp/manifest.json      |  2 +-
 homeassistant/components/yeelight/manifest.json  |  2 +-
 homeassistant/package_constraints.txt            |  2 +-
 requirements_all.txt                             |  2 +-
 requirements_test_all.txt                        |  2 +-
 tests/components/ssdp/test_init.py               |  4 ++--
 11 files changed, 24 insertions(+), 14 deletions(-)

diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json
index 582e48de839..9d05b02000b 100644
--- a/homeassistant/components/dlna_dmr/manifest.json
+++ b/homeassistant/components/dlna_dmr/manifest.json
@@ -3,7 +3,7 @@
   "name": "DLNA Digital Media Renderer",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
-  "requirements": ["async-upnp-client==0.32.0"],
+  "requirements": ["async-upnp-client==0.32.1"],
   "dependencies": ["ssdp"],
   "after_dependencies": ["media_source"],
   "ssdp": [
diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json
index 569b2cc635e..98ad81e653a 100644
--- a/homeassistant/components/dlna_dms/manifest.json
+++ b/homeassistant/components/dlna_dms/manifest.json
@@ -3,7 +3,7 @@
   "name": "DLNA Digital Media Server",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/dlna_dms",
-  "requirements": ["async-upnp-client==0.32.0"],
+  "requirements": ["async-upnp-client==0.32.1"],
   "dependencies": ["ssdp"],
   "after_dependencies": ["media_source"],
   "ssdp": [
diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json
index 0fc989c57c6..a7aa842f485 100644
--- a/homeassistant/components/samsungtv/manifest.json
+++ b/homeassistant/components/samsungtv/manifest.json
@@ -7,7 +7,7 @@
     "samsungctl[websocket]==0.7.1",
     "samsungtvws[async,encrypted]==2.5.0",
     "wakeonlan==2.1.0",
-    "async-upnp-client==0.32.0"
+    "async-upnp-client==0.32.1"
   ],
   "ssdp": [
     {
diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py
index 8783f5eeb34..195bebb8321 100644
--- a/homeassistant/components/ssdp/__init__.py
+++ b/homeassistant/components/ssdp/__init__.py
@@ -9,6 +9,7 @@ from enum import Enum
 from ipaddress import IPv4Address, IPv6Address
 import logging
 import socket
+from time import time
 from typing import Any
 from urllib.parse import urljoin
 import xml.etree.ElementTree as ET
@@ -29,7 +30,12 @@ from async_upnp_client.server import (
     UpnpServerDevice,
     UpnpServerService,
 )
-from async_upnp_client.ssdp import SSDP_PORT, determine_source_target, is_ipv4_address
+from async_upnp_client.ssdp import (
+    SSDP_PORT,
+    determine_source_target,
+    fix_ipv6_address_scope_id,
+    is_ipv4_address,
+)
 from async_upnp_client.ssdp_listener import SsdpDevice, SsdpDeviceTracker, SsdpListener
 from async_upnp_client.utils import CaseInsensitiveDict
 
@@ -213,8 +219,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
         UPNP_SERVER: server,
     }
 
-    hass.create_task(scanner.async_start())
-    hass.create_task(server.async_start())
+    await scanner.async_start()
+    await server.async_start()
 
     return True
 
@@ -415,6 +421,7 @@ class Scanner:
             else:
                 source_tuple = (source_ip_str, 0)
             source, target = determine_source_target(source_tuple)
+            source = fix_ipv6_address_scope_id(source) or source
             self._ssdp_listeners.append(
                 SsdpListener(
                     async_callback=self._ssdp_listener_callback,
@@ -710,6 +717,7 @@ class Server:
             )
 
         # Start a server on all source IPs.
+        boot_id = int(time())
         for source_ip in await async_build_source_set(self.hass):
             source_ip_str = str(source_ip)
             if source_ip.version == 6:
@@ -722,6 +730,7 @@ class Server:
             else:
                 source_tuple = (source_ip_str, 0)
             source, target = determine_source_target(source_tuple)
+            source = fix_ipv6_address_scope_id(source) or source
             http_port = await _async_find_next_available_port(source)
             _LOGGER.debug("Binding UPnP HTTP server to: %s:%s", source_ip, http_port)
             self._upnp_servers.append(
@@ -730,6 +739,7 @@ class Server:
                     target=target,
                     http_port=http_port,
                     server_device=HassUpnpServiceDevice,
+                    boot_id=boot_id,
                     options={
                         SSDP_SEARCH_RESPONDER_OPTIONS: {
                             SSDP_SEARCH_RESPONDER_OPTION_ALWAYS_REPLY_WITH_ROOT_DEVICE: True
diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json
index b74883e2008..59d9d6ddad8 100644
--- a/homeassistant/components/ssdp/manifest.json
+++ b/homeassistant/components/ssdp/manifest.json
@@ -2,7 +2,7 @@
   "domain": "ssdp",
   "name": "Simple Service Discovery Protocol (SSDP)",
   "documentation": "https://www.home-assistant.io/integrations/ssdp",
-  "requirements": ["async-upnp-client==0.32.0"],
+  "requirements": ["async-upnp-client==0.32.1"],
   "dependencies": ["network"],
   "after_dependencies": ["zeroconf"],
   "codeowners": [],
diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json
index b81b0ceb777..c574a5d7269 100644
--- a/homeassistant/components/upnp/manifest.json
+++ b/homeassistant/components/upnp/manifest.json
@@ -3,7 +3,7 @@
   "name": "UPnP/IGD",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/upnp",
-  "requirements": ["async-upnp-client==0.32.0", "getmac==0.8.2"],
+  "requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"],
   "dependencies": ["network", "ssdp"],
   "codeowners": ["@StevenLooman"],
   "ssdp": [
diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json
index ac688719fea..16fe2ae7700 100644
--- a/homeassistant/components/yeelight/manifest.json
+++ b/homeassistant/components/yeelight/manifest.json
@@ -2,7 +2,7 @@
   "domain": "yeelight",
   "name": "Yeelight",
   "documentation": "https://www.home-assistant.io/integrations/yeelight",
-  "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.0"],
+  "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"],
   "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
   "config_flow": true,
   "dependencies": ["network"],
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 2aca1d72eba..80402eef843 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -4,7 +4,7 @@ aiodiscover==1.4.13
 aiohttp==3.8.1
 aiohttp_cors==0.7.0
 astral==2.2
-async-upnp-client==0.32.0
+async-upnp-client==0.32.1
 async_timeout==4.0.2
 atomicwrites-homeassistant==1.4.1
 attrs==21.2.0
diff --git a/requirements_all.txt b/requirements_all.txt
index c25834d151b..7770c84193e 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -353,7 +353,7 @@ asterisk_mbox==0.5.0
 # homeassistant.components.ssdp
 # homeassistant.components.upnp
 # homeassistant.components.yeelight
-async-upnp-client==0.32.0
+async-upnp-client==0.32.1
 
 # homeassistant.components.supla
 asyncpysupla==0.0.5
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f4d4e2ed2ba..1679aee7e6d 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -307,7 +307,7 @@ arcam-fmj==0.12.0
 # homeassistant.components.ssdp
 # homeassistant.components.upnp
 # homeassistant.components.yeelight
-async-upnp-client==0.32.0
+async-upnp-client==0.32.1
 
 # homeassistant.components.sleepiq
 asyncsleepiq==1.2.3
diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py
index e5d452af948..4076ef4685d 100644
--- a/tests/components/ssdp/test_init.py
+++ b/tests/components/ssdp/test_init.py
@@ -671,7 +671,7 @@ async def test_async_detect_interfaces_setting_empty_route(
 
     ssdp_listeners = hass.data[ssdp.DOMAIN][ssdp.SSDP_SCANNER]._ssdp_listeners
     sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners}
-    assert sources == {("2001:db8::%1", 0, 0, 1), ("192.168.1.5", 0)}
+    assert sources == {("2001:db8::", 0, 0, 1), ("192.168.1.5", 0)}
 
 
 @pytest.mark.usefixtures("mock_get_source_ip")
@@ -695,7 +695,7 @@ async def test_bind_failure_skips_adapter(
     """Test that an adapter with a bind failure is skipped."""
 
     async def _async_start(self):
-        if self.source == ("2001:db8::%1", 0, 0, 1):
+        if self.source == ("2001:db8::", 0, 0, 1):
             raise OSError
 
     SsdpListener.async_start = _async_start
-- 
GitLab


From 2ddf1f9416cdf769fe8e5a3a3fc12647f21d124a Mon Sep 17 00:00:00 2001
From: Simon Hansen <67142049+DurgNomis-drol@users.noreply.github.com>
Date: Mon, 24 Oct 2022 23:19:59 +0200
Subject: [PATCH 766/985] Add integration_type to iss (#80909)

---
 homeassistant/components/iss/manifest.json | 1 +
 homeassistant/generated/integrations.json  | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json
index bd1aa967f07..d91c35e8f6b 100644
--- a/homeassistant/components/iss/manifest.json
+++ b/homeassistant/components/iss/manifest.json
@@ -2,6 +2,7 @@
   "domain": "iss",
   "config_flow": true,
   "name": "International Space Station (ISS)",
+  "integration_type": "service",
   "documentation": "https://www.home-assistant.io/integrations/iss",
   "requirements": ["pyiss==1.0.1"],
   "codeowners": ["@DurgNomis-drol"],
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index c04d9f28ebe..8805bea7e58 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2482,7 +2482,7 @@
     },
     "iss": {
       "name": "International Space Station (ISS)",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
-- 
GitLab


From dce4753510162fc0bcab49ced4d1c105f0c6fca6 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Tue, 25 Oct 2022 00:20:26 +0300
Subject: [PATCH 767/985] Cleanup Shelly update platform (#80845)

---
 .../components/shelly/coordinator.py          |  79 ----
 homeassistant/components/shelly/update.py     |  89 ++--
 tests/components/shelly/test_update.py        | 389 +++++++++++++++++-
 3 files changed, 436 insertions(+), 121 deletions(-)

diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py
index fdb94b3bdb0..014355116c1 100644
--- a/homeassistant/components/shelly/coordinator.py
+++ b/homeassistant/components/shelly/coordinator.py
@@ -14,13 +14,11 @@ from aioshelly.rpc_device import RpcDevice
 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.exceptions import HomeAssistantError
 from homeassistant.helpers import device_registry
 from homeassistant.helpers.debounce import Debouncer
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
 from .const import (
-    ATTR_BETA,
     ATTR_CHANNEL,
     ATTR_CLICK_TYPE,
     ATTR_DEVICE,
@@ -249,43 +247,6 @@ class ShellyBlockCoordinator(DataUpdateCoordinator):
         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:
-            result = await self.device.trigger_ota_update(beta=beta)
-        except DeviceConnectionError as err:
-            raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err
-        except InvalidAuthError:
-            self.entry.async_start_reauth(self.hass)
-        else:
-            LOGGER.debug("Result of OTA update call: %s", result)
-
     def shutdown(self) -> None:
         """Shutdown the coordinator."""
         self.device.shutdown()
@@ -480,46 +441,6 @@ class ShellyRpcCoordinator(DataUpdateCoordinator):
         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:
-            await self.device.trigger_ota_update(beta=beta)
-        except DeviceConnectionError as err:
-            raise HomeAssistantError(
-                f"OTA update connection error: {repr(err)}"
-            ) from err
-        except RpcCallError as err:
-            raise HomeAssistantError(f"OTA update request error: {repr(err)}") from err
-        except InvalidAuthError:
-            self.entry.async_start_reauth(self.hass)
-        else:
-            LOGGER.debug("OTA update call successful")
-
     async def shutdown(self) -> None:
         """Shutdown the coordinator."""
         await self.device.shutdown()
diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py
index faceea62a59..3d562bf86e5 100644
--- a/homeassistant/components/shelly/update.py
+++ b/homeassistant/components/shelly/update.py
@@ -6,6 +6,8 @@ from dataclasses import dataclass
 import logging
 from typing import Any, Final, cast
 
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
+
 from homeassistant.components.update import (
     UpdateDeviceClass,
     UpdateEntity,
@@ -14,11 +16,12 @@ from homeassistant.components.update import (
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import CONF_SLEEP_PERIOD
-from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
+from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
 from .entity import (
     RestEntityDescription,
     RpcEntityDescription,
@@ -37,7 +40,7 @@ class RpcUpdateRequiredKeysMixin:
     """Class for RPC update required keys."""
 
     latest_version: Callable[[dict], Any]
-    install: Callable
+    beta: bool
 
 
 @dataclass
@@ -45,7 +48,7 @@ class RestUpdateRequiredKeysMixin:
     """Class for REST update required keys."""
 
     latest_version: Callable[[dict], Any]
-    install: Callable
+    beta: bool
 
 
 @dataclass
@@ -67,7 +70,7 @@ REST_UPDATES: Final = {
         name="Firmware Update",
         key="fwupdate",
         latest_version=lambda status: status["update"]["new_version"],
-        install=lambda coordinator: coordinator.async_trigger_ota_update(),
+        beta=False,
         device_class=UpdateDeviceClass.FIRMWARE,
         entity_category=EntityCategory.CONFIG,
         entity_registry_enabled_default=False,
@@ -76,7 +79,7 @@ REST_UPDATES: Final = {
         name="Beta Firmware Update",
         key="fwupdate",
         latest_version=lambda status: status["update"].get("beta_version"),
-        install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True),
+        beta=True,
         device_class=UpdateDeviceClass.FIRMWARE,
         entity_category=EntityCategory.CONFIG,
         entity_registry_enabled_default=False,
@@ -88,10 +91,8 @@ RPC_UPDATES: Final = {
         name="Firmware Update",
         key="sys",
         sub_key="available_updates",
-        latest_version=lambda status: status.get("stable", {"version": None})[
-            "version"
-        ],
-        install=lambda coordinator: coordinator.async_trigger_ota_update(),
+        latest_version=lambda status: status.get("stable", {"version": ""})["version"],
+        beta=False,
         device_class=UpdateDeviceClass.FIRMWARE,
         entity_category=EntityCategory.CONFIG,
         entity_registry_enabled_default=False,
@@ -100,8 +101,8 @@ RPC_UPDATES: Final = {
         name="Beta Firmware Update",
         key="sys",
         sub_key="available_updates",
-        latest_version=lambda status: status.get("beta", {"version": None})["version"],
-        install=lambda coordinator: coordinator.async_trigger_ota_update(beta=True),
+        latest_version=lambda status: status.get("beta", {"version": ""})["version"],
+        beta=True,
         device_class=UpdateDeviceClass.FIRMWARE,
         entity_category=EntityCategory.CONFIG,
         entity_registry_enabled_default=False,
@@ -151,11 +152,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
     @property
     def installed_version(self) -> str | None:
         """Version currently in use."""
-        version = self.block_coordinator.device.status["update"]["old_version"]
-        if version is None:
-            return None
-
-        return cast(str, version)
+        return cast(str, self.block_coordinator.device.status["update"]["old_version"])
 
     @property
     def latest_version(self) -> str | None:
@@ -163,7 +160,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
         new_version = self.entity_description.latest_version(
             self.block_coordinator.device.status,
         )
-        if new_version not in (None, ""):
+        if new_version:
             return cast(str, new_version)
 
         return self.installed_version
@@ -177,10 +174,29 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
         self, version: str | None, backup: bool, **kwargs: Any
     ) -> None:
         """Install the latest firmware version."""
-        config_entry = self.block_coordinator.entry
-        coordinator = get_entry_data(self.hass)[config_entry.entry_id].block
         self._in_progress_old_version = self.installed_version
-        await self.entity_description.install(coordinator)
+        beta = self.entity_description.beta
+        update_data = self.coordinator.device.status["update"]
+        LOGGER.debug("OTA update service - update_data: %s", update_data)
+
+        new_version = update_data["new_version"]
+        if beta:
+            new_version = update_data["beta_version"]
+
+        LOGGER.info(
+            "Starting OTA update of device %s from '%s' to '%s'",
+            self.name,
+            self.coordinator.device.firmware_version,
+            new_version,
+        )
+        try:
+            result = await self.coordinator.device.trigger_ota_update(beta=beta)
+        except DeviceConnectionError as err:
+            raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err
+        except InvalidAuthError:
+            self.coordinator.entry.async_start_reauth(self.hass)
+        else:
+            LOGGER.debug("Result of OTA update call: %s", result)
 
 
 class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
@@ -205,9 +221,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
     @property
     def installed_version(self) -> str | None:
         """Version currently in use."""
-        if self.coordinator.device.shelly is None:
-            return None
-
+        assert self.coordinator.device.shelly
         return cast(str, self.coordinator.device.shelly["ver"])
 
     @property
@@ -216,7 +230,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
         new_version = self.entity_description.latest_version(
             self.coordinator.device.status[self.key][self.entity_description.sub_key],
         )
-        if new_version is not None:
+        if new_version:
             return cast(str, new_version)
 
         return self.installed_version
@@ -231,4 +245,29 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
     ) -> None:
         """Install the latest firmware version."""
         self._in_progress_old_version = self.installed_version
-        await self.entity_description.install(self.coordinator)
+        beta = self.entity_description.beta
+        update_data = self.coordinator.device.status["sys"]["available_updates"]
+        LOGGER.debug("OTA update service - update_data: %s", update_data)
+
+        new_version = update_data.get("stable", {"version": ""})["version"]
+        if beta:
+            new_version = update_data.get("beta", {"version": ""})["version"]
+
+        LOGGER.info(
+            "Starting OTA update of device %s from '%s' to '%s'",
+            self.coordinator.name,
+            self.coordinator.device.firmware_version,
+            new_version,
+        )
+        try:
+            await self.coordinator.device.trigger_ota_update(beta=beta)
+        except DeviceConnectionError as err:
+            raise HomeAssistantError(
+                f"OTA update connection error: {repr(err)}"
+            ) from err
+        except RpcCallError as err:
+            raise HomeAssistantError(f"OTA update request error: {repr(err)}") from err
+        except InvalidAuthError:
+            self.coordinator.entry.async_start_reauth(self.hass)
+        else:
+            LOGGER.debug("OTA update call successful")
diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py
index f5f713eb81e..6a1ecc58245 100644
--- a/tests/components/shelly/test_update.py
+++ b/tests/components/shelly/test_update.py
@@ -1,13 +1,29 @@
 """Tests for Shelly update platform."""
-from homeassistant.components.shelly.const import DOMAIN
-from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
-from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNKNOWN
+from datetime import timedelta
+from unittest.mock import AsyncMock
+
+from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
+import pytest
+
+from homeassistant.components.shelly.const import DOMAIN, REST_SENSORS_UPDATE_INTERVAL
+from homeassistant.components.update import (
+    ATTR_IN_PROGRESS,
+    ATTR_INSTALLED_VERSION,
+    ATTR_LATEST_VERSION,
+    DOMAIN as UPDATE_DOMAIN,
+    SERVICE_INSTALL,
+)
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
+from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity_component import async_update_entity
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.entity_registry import async_get
+from homeassistant.util import dt
 
 from . import MOCK_MAC, init_integration
 
+from tests.common import async_fire_time_changed
+
 
 async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch):
     """Test block device update entity."""
@@ -19,9 +35,15 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch)
         suggested_object_id="test_name_firmware_update",
         disabled_by=None,
     )
+    monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1")
+    monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2")
     await init_integration(hass, 1)
 
-    assert hass.states.get("update.test_name_firmware_update").state == STATE_ON
+    state = hass.states.get("update.test_name_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
 
     await hass.services.async_call(
         UPDATE_DOMAIN,
@@ -31,17 +53,162 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch)
     )
     assert mock_block_device.trigger_ota_update.call_count == 1
 
-    monkeypatch.setitem(mock_block_device.status["update"], "old_version", None)
-    monkeypatch.setitem(mock_block_device.status["update"], "new_version", None)
+    state = hass.states.get("update.test_name_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2"
+    assert state.attributes[ATTR_IN_PROGRESS] is True
+
+    monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2")
+    async_fire_time_changed(
+        hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL)
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("update.test_name_firmware_update")
+    assert state.state == STATE_OFF
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "2"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
+
+async def test_block_beta_update(hass: HomeAssistant, mock_block_device, monkeypatch):
+    """Test block device beta update entity."""
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        f"{MOCK_MAC}-fwupdate_beta",
+        suggested_object_id="test_name_beta_firmware_update",
+        disabled_by=None,
+    )
+    monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1")
+    monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2")
+    monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "")
+    await init_integration(hass, 1)
+
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_OFF
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "1"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
+    monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "2b")
+    async_fire_time_changed(
+        hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL)
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2b"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
+    await hass.services.async_call(
+        UPDATE_DOMAIN,
+        SERVICE_INSTALL,
+        {ATTR_ENTITY_ID: "update.test_name_beta_firmware_update"},
+        blocking=True,
+    )
+    assert mock_block_device.trigger_ota_update.call_count == 1
+
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2b"
+    assert state.attributes[ATTR_IN_PROGRESS] is True
+
+    monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2b")
+    async_fire_time_changed(
+        hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL)
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_OFF
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "2b"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2b"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
 
-    # update entity
-    await async_update_entity(hass, "update.test_name_firmware_update")
+async def test_block_update_connection_error(
+    hass: HomeAssistant, mock_block_device, monkeypatch, caplog
+):
+    """Test block device update connection error."""
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        f"{MOCK_MAC}-fwupdate",
+        suggested_object_id="test_name_firmware_update",
+        disabled_by=None,
+    )
+    monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1")
+    monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2")
+    monkeypatch.setattr(
+        mock_block_device,
+        "trigger_ota_update",
+        AsyncMock(side_effect=DeviceConnectionError),
+    )
+    await init_integration(hass, 1)
+
+    with pytest.raises(HomeAssistantError):
+        await hass.services.async_call(
+            UPDATE_DOMAIN,
+            SERVICE_INSTALL,
+            {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
+            blocking=True,
+        )
+        assert "Error starting OTA update" in caplog.text
+
+
+async def test_block_update_auth_error(
+    hass: HomeAssistant, mock_block_device, monkeypatch
+):
+    """Test block device update authentication error."""
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        f"{MOCK_MAC}-fwupdate",
+        suggested_object_id="test_name_firmware_update",
+        disabled_by=None,
+    )
+    monkeypatch.setitem(mock_block_device.status["update"], "old_version", "1")
+    monkeypatch.setitem(mock_block_device.status["update"], "new_version", "2")
+    monkeypatch.setattr(
+        mock_block_device,
+        "trigger_ota_update",
+        AsyncMock(side_effect=InvalidAuthError),
+    )
+    entry = await init_integration(hass, 1)
 
-    assert hass.states.get("update.test_name_firmware_update").state == STATE_UNKNOWN
+    assert entry.state == ConfigEntryState.LOADED
+
+    await hass.services.async_call(
+        UPDATE_DOMAIN,
+        SERVICE_INSTALL,
+        {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
+        blocking=True,
+    )
+
+    assert entry.state == ConfigEntryState.LOADED
+
+    flows = hass.config_entries.flow.async_progress()
+    assert len(flows) == 1
+
+    flow = flows[0]
+    assert flow.get("step_id") == "reauth_confirm"
+    assert flow.get("handler") == DOMAIN
+
+    assert "context" in flow
+    assert flow["context"].get("source") == SOURCE_REAUTH
+    assert flow["context"].get("entry_id") == entry.entry_id
 
 
 async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch):
-    """Test rpc device update entity."""
+    """Test RPC device update entity."""
     entity_registry = async_get(hass)
     entity_registry.async_get_or_create(
         UPDATE_DOMAIN,
@@ -50,9 +217,21 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch):
         suggested_object_id="test_name_firmware_update",
         disabled_by=None,
     )
+    monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1")
+    monkeypatch.setitem(
+        mock_rpc_device.status["sys"],
+        "available_updates",
+        {
+            "stable": {"version": "2"},
+        },
+    )
     await init_integration(hass, 2)
 
-    assert hass.states.get("update.test_name_firmware_update").state == STATE_ON
+    state = hass.states.get("update.test_name_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
 
     await hass.services.async_call(
         UPDATE_DOMAIN,
@@ -60,13 +239,189 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch):
         {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
         blocking=True,
     )
+    assert mock_rpc_device.trigger_ota_update.call_count == 1
+
+    state = hass.states.get("update.test_name_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2"
+    assert state.attributes[ATTR_IN_PROGRESS] is True
+
+    monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2")
+    async_fire_time_changed(
+        hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL)
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("update.test_name_firmware_update")
+    assert state.state == STATE_OFF
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "2"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
+
+async def test_rpc_beta_update(hass: HomeAssistant, mock_rpc_device, monkeypatch):
+    """Test RPC device beta update entity."""
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        f"{MOCK_MAC}-sys-fwupdate_beta",
+        suggested_object_id="test_name_beta_firmware_update",
+        disabled_by=None,
+    )
+    monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1")
+    monkeypatch.setitem(
+        mock_rpc_device.status["sys"],
+        "available_updates",
+        {
+            "stable": {"version": "2"},
+            "beta": {"version": ""},
+        },
+    )
+    await init_integration(hass, 2)
+
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_OFF
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "1"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
+    monkeypatch.setitem(
+        mock_rpc_device.status["sys"],
+        "available_updates",
+        {
+            "stable": {"version": "2"},
+            "beta": {"version": "2b"},
+        },
+    )
+    async_fire_time_changed(
+        hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL)
+    )
     await hass.async_block_till_done()
+
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2b"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
+    await hass.services.async_call(
+        UPDATE_DOMAIN,
+        SERVICE_INSTALL,
+        {ATTR_ENTITY_ID: "update.test_name_beta_firmware_update"},
+        blocking=True,
+    )
     assert mock_rpc_device.trigger_ota_update.call_count == 1
 
-    monkeypatch.setitem(mock_rpc_device.status["sys"], "available_updates", {})
-    monkeypatch.setattr(mock_rpc_device, "shelly", None)
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_ON
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "1"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2b"
+    assert state.attributes[ATTR_IN_PROGRESS] is True
+
+    monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2b")
+    async_fire_time_changed(
+        hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL)
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("update.test_name_beta_firmware_update")
+    assert state.state == STATE_OFF
+    assert state.attributes[ATTR_INSTALLED_VERSION] == "2b"
+    assert state.attributes[ATTR_LATEST_VERSION] == "2b"
+    assert state.attributes[ATTR_IN_PROGRESS] is False
+
+
+@pytest.mark.parametrize(
+    "exc, error",
+    [
+        (DeviceConnectionError, "Error starting OTA update"),
+        (RpcCallError(-1, "error"), "OTA update request error"),
+    ],
+)
+async def test_rpc_update__errors(
+    hass: HomeAssistant, exc, error, mock_rpc_device, monkeypatch, caplog
+):
+    """Test RPC device update connection/call errors."""
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        f"{MOCK_MAC}-sys-fwupdate",
+        suggested_object_id="test_name_firmware_update",
+        disabled_by=None,
+    )
+    monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1")
+    monkeypatch.setitem(
+        mock_rpc_device.status["sys"],
+        "available_updates",
+        {
+            "stable": {"version": "2"},
+            "beta": {"version": ""},
+        },
+    )
+    monkeypatch.setattr(
+        mock_rpc_device, "trigger_ota_update", AsyncMock(side_effect=exc)
+    )
+    await init_integration(hass, 2)
+
+    with pytest.raises(HomeAssistantError):
+        await hass.services.async_call(
+            UPDATE_DOMAIN,
+            SERVICE_INSTALL,
+            {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
+            blocking=True,
+        )
+        assert error in caplog.text
+
+
+async def test_rpc_update_auth_error(
+    hass: HomeAssistant, mock_rpc_device, monkeypatch, caplog
+):
+    """Test RPC device update authentication error."""
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        UPDATE_DOMAIN,
+        DOMAIN,
+        f"{MOCK_MAC}-sys-fwupdate",
+        suggested_object_id="test_name_firmware_update",
+        disabled_by=None,
+    )
+    monkeypatch.setitem(mock_rpc_device.shelly, "ver", "1")
+    monkeypatch.setitem(
+        mock_rpc_device.status["sys"],
+        "available_updates",
+        {
+            "stable": {"version": "2"},
+            "beta": {"version": ""},
+        },
+    )
+    monkeypatch.setattr(
+        mock_rpc_device,
+        "trigger_ota_update",
+        AsyncMock(side_effect=InvalidAuthError),
+    )
+    entry = await init_integration(hass, 2)
+
+    assert entry.state == ConfigEntryState.LOADED
+
+    await hass.services.async_call(
+        UPDATE_DOMAIN,
+        SERVICE_INSTALL,
+        {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
+        blocking=True,
+    )
+
+    assert entry.state == ConfigEntryState.LOADED
+
+    flows = hass.config_entries.flow.async_progress()
+    assert len(flows) == 1
 
-    # update entity
-    await async_update_entity(hass, "update.test_name_firmware_update")
+    flow = flows[0]
+    assert flow.get("step_id") == "reauth_confirm"
+    assert flow.get("handler") == DOMAIN
 
-    assert hass.states.get("update.test_name_firmware_update").state == STATE_UNKNOWN
+    assert "context" in flow
+    assert flow["context"].get("source") == SOURCE_REAUTH
+    assert flow["context"].get("entry_id") == entry.entry_id
-- 
GitLab


From b56d332c26d2d4feba696d74785df4625044e08e Mon Sep 17 00:00:00 2001
From: Simon Hansen <67142049+DurgNomis-drol@users.noreply.github.com>
Date: Mon, 24 Oct 2022 23:23:14 +0200
Subject: [PATCH 768/985] Add integration_type to launchlibrary (#80907)

---
 homeassistant/components/launch_library/manifest.json | 1 +
 homeassistant/generated/integrations.json             | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json
index af0cf77007e..12b743b22d1 100644
--- a/homeassistant/components/launch_library/manifest.json
+++ b/homeassistant/components/launch_library/manifest.json
@@ -2,6 +2,7 @@
   "domain": "launch_library",
   "name": "Launch Library",
   "config_flow": true,
+  "integration_type": "service",
   "documentation": "https://www.home-assistant.io/integrations/launch_library",
   "requirements": ["pylaunches==1.3.0"],
   "codeowners": ["@ludeeus", "@DurgNomis-drol"],
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 8805bea7e58..033465408f2 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2692,7 +2692,7 @@
     },
     "launch_library": {
       "name": "Launch Library",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
-- 
GitLab


From 9b331abe91e9970f51b1a09ac9b2c8660ded2724 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Tue, 25 Oct 2022 00:39:01 +0000
Subject: [PATCH 769/985] [ci skip] Translation update

---
 .../google_travel_time/translations/he.json   |  3 +-
 .../components/mqtt/translations/ca.json      |  1 +
 .../components/mqtt/translations/de.json      | 49 +++++++++++++++++--
 .../components/mqtt/translations/en.json      | 13 ++---
 .../components/mqtt/translations/es.json      | 49 +++++++++++++++++--
 .../components/mqtt/translations/et.json      |  7 +++
 .../components/mqtt/translations/fr.json      |  9 ++++
 .../components/mqtt/translations/pt-BR.json   | 49 +++++++++++++++++--
 .../nibe_heatpump/translations/de.json        | 13 +++--
 .../nibe_heatpump/translations/en.json        |  3 ++
 .../nibe_heatpump/translations/es.json        | 13 +++--
 .../nibe_heatpump/translations/et.json        |  9 +++-
 .../nibe_heatpump/translations/fr.json        |  2 +-
 .../nibe_heatpump/translations/pt-BR.json     | 11 ++++-
 .../components/sensibo/translations/de.json   |  4 +-
 .../components/sensibo/translations/et.json   |  4 +-
 .../components/sensibo/translations/no.json   |  4 +-
 .../sensibo/translations/sensor.bg.json       |  4 ++
 .../sensibo/translations/sensor.ca.json       |  4 ++
 .../sensibo/translations/sensor.de.json       |  5 ++
 .../sensibo/translations/sensor.es.json       |  5 ++
 .../sensibo/translations/sensor.et.json       |  5 ++
 .../sensibo/translations/sensor.fr.json       |  5 ++
 .../sensibo/translations/sensor.no.json       |  5 ++
 .../sensibo/translations/sensor.pt-BR.json    |  5 ++
 .../components/sensor/translations/ca.json    |  2 +
 .../components/sensor/translations/es.json    |  2 +
 .../components/sensor/translations/et.json    |  2 +
 .../xiaomi_miio/translations/bg.json          |  3 +-
 .../xiaomi_miio/translations/he.json          |  3 +-
 .../xiaomi_miio/translations/no.json          |  3 +-
 31 files changed, 255 insertions(+), 41 deletions(-)

diff --git a/homeassistant/components/google_travel_time/translations/he.json b/homeassistant/components/google_travel_time/translations/he.json
index 0db92654b41..b10c8890fd4 100644
--- a/homeassistant/components/google_travel_time/translations/he.json
+++ b/homeassistant/components/google_travel_time/translations/he.json
@@ -4,7 +4,8 @@
             "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
         },
         "error": {
-            "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4"
+            "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": {
diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json
index 43ab994f4d8..819d06b5b29 100644
--- a/homeassistant/components/mqtt/translations/ca.json
+++ b/homeassistant/components/mqtt/translations/ca.json
@@ -14,6 +14,7 @@
                     "discovery": "Habilita el descobriment autom\u00e0tic",
                     "password": "Contrasenya",
                     "port": "Port",
+                    "protocol": "Protocol MQTT",
                     "username": "Nom d'usuari"
                 },
                 "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT."
diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json
index 8a0171a6f85..b193f18b39e 100644
--- a/homeassistant/components/mqtt/translations/de.json
+++ b/homeassistant/components/mqtt/translations/de.json
@@ -5,15 +5,33 @@
             "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
         },
         "error": {
-            "cannot_connect": "Verbindung fehlgeschlagen"
+            "bad_birth": "Ung\u00fcltiges \u201eBirth\u201c-Thema",
+            "bad_certificate": "Das CA-Zertifikat ist ung\u00fcltig",
+            "bad_client_cert": "Ung\u00fcltiges Client-Zertifikat. Stelle sicher, dass eine PEM-codierte Datei bereitgestellt wird",
+            "bad_client_cert_key": "Client-Zertifikat und privates Zertifikat sind kein g\u00fcltiges Paar",
+            "bad_client_key": "Ung\u00fcltiger privater Schl\u00fcssel. Stelle sicher, dass eine PEM-codierte Datei ohne Passwort bereitgestellt wird",
+            "bad_discovery_prefix": "Ung\u00fcltiges Discovery-Pr\u00e4fix",
+            "bad_will": "Ung\u00fcltiges \u201eWill\u201c-Thema",
+            "cannot_connect": "Verbindung fehlgeschlagen",
+            "invalid_inclusion": "Das Client-Zertifikat und der private Schl\u00fcssel m\u00fcssen gemeinsam konfiguriert werden"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Erweiterte Optionen",
                     "broker": "Server",
+                    "certificate": "Pfad zur benutzerdefinierten CA-Zertifikatsdatei",
+                    "client_cert": "Pfad zur Client-Zertifikatsdatei",
+                    "client_id": "Client-ID (leer lassen, um eine zuf\u00e4llig generierte zu erhalten)",
+                    "client_key": "Pfad zur privaten Schl\u00fcsseldatei",
                     "discovery": "Suche aktivieren",
+                    "keepalive": "Die Zeit zwischen dem Senden von Keep-Alive-Nachrichten",
                     "password": "Passwort",
                     "port": "Port",
+                    "protocol": "MQTT-Protokoll",
+                    "set_ca_cert": "Validierung des Broker-Zertifikats",
+                    "set_client_cert": "Ein Client-Zertifikat verwenden",
+                    "tls_insecure": "Validierung des Broker-Zertifikats ignorieren",
                     "username": "Benutzername"
                 },
                 "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein."
@@ -53,20 +71,40 @@
         "deprecated_yaml": {
             "description": "Manuell konfigurierte MQTT-{platform}(en) gefunden unter Plattformschl\u00fcssel `{platform}`. \n\nBitte verschiebe die Konfiguration auf den \u201emqtt\u201c-Integrationsschl\u00fcssel und starte Home Assistant neu, um dieses Problem zu beheben. Weitere Informationen findest du in der [Dokumentation]({more_info_url}).",
             "title": "Deine manuell konfigurierte(n) MQTT-{platform}(en) erfordert Aufmerksamkeit"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "Die folgenden Einstellungen in \u201econfiguration.yaml\u201c wurden in den MQTT-Konfigurationseintrag migriert und \u00fcberschreiben nun die Einstellungen in \u201econfiguration.yaml\u201c:\n\u201e{deprecated_settings}\u201c \n\nBitte entferne diese Einstellungen aus \u201econfiguration.yaml\u201c und starte Home Assistant neu, um dieses Problem zu beheben. Weitere Informationen findest du in der [Dokumentation]( {more_info_url} ).",
+            "title": "Veraltete MQTT-Einstellungen in \u201econfiguration.yaml\u201c gefunden"
         }
     },
     "options": {
         "error": {
-            "bad_birth": "Ung\u00fcltiges Birth Thema.",
-            "bad_will": "Ung\u00fcltiges will Thema.",
-            "cannot_connect": "Verbindung fehlgeschlagen"
+            "bad_birth": "Ung\u00fcltiges \u201eBirth\u201c-Thema",
+            "bad_certificate": "Das CA-Zertifikat ist ung\u00fcltig",
+            "bad_client_cert": "Ung\u00fcltiges Client-Zertifikat. Stelle sicher, dass eine PEM-codierte Datei bereitgestellt wird",
+            "bad_client_cert_key": "Client-Zertifikat und privates Zertifikat sind kein g\u00fcltiges Paar",
+            "bad_client_key": "Ung\u00fcltiger privater Schl\u00fcssel. Stelle sicher, dass eine PEM-codierte Datei ohne Passwort bereitgestellt wird",
+            "bad_discovery_prefix": "Ung\u00fcltiges Discovery-Pr\u00e4fix",
+            "bad_will": "Ung\u00fcltiges \u201eWill\u201c-Thema",
+            "cannot_connect": "Verbindung fehlgeschlagen",
+            "invalid_inclusion": "Das Client-Zertifikat und der private Schl\u00fcssel m\u00fcssen gemeinsam konfiguriert werden"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Erweiterte Optionen",
                     "broker": "Broker",
+                    "certificate": "Hochladen einer benutzerdefinierten CA-Zertifikatsdatei",
+                    "client_cert": "Client-Zertifikatsdatei hochladen",
+                    "client_id": "Client-ID (leer lassen, um eine zuf\u00e4llig generierte zu erhalten)",
+                    "client_key": "Private Schl\u00fcsseldatei hochladen",
+                    "keepalive": "Die Zeit zwischen dem Senden von Keep-Alive-Nachrichten",
                     "password": "Passwort",
                     "port": "Port",
+                    "protocol": "MQTT-Protokoll",
+                    "set_ca_cert": "Validierung des Broker-Zertifikats",
+                    "set_client_cert": "Ein Client-Zertifikat verwenden",
+                    "tls_insecure": "Validierung des Broker-Zertifikats ignorieren",
                     "username": "Benutzername"
                 },
                 "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.",
@@ -80,13 +118,14 @@
                     "birth_retain": "Birth Nachricht zwischenspeichern",
                     "birth_topic": "Thema der Birth Nachricht",
                     "discovery": "Erkennung aktivieren",
+                    "discovery_prefix": "Discovery-Pr\u00e4fix",
                     "will_enable": "Letzten Willen aktivieren",
                     "will_payload": "Nutzdaten der Letzter-Wille Nachricht",
                     "will_qos": "Letzter-Wille Nachricht QoS",
                     "will_retain": "Letzter-Wille Nachricht zwischenspeichern",
                     "will_topic": "Thema der Letzter-Wille Nachricht"
                 },
-                "description": "Erkennung - Wenn die Erkennung aktiviert ist (empfohlen), erkennt Home Assistant automatisch Ger\u00e4te und Entit\u00e4ten, die ihre Konfiguration auf dem MQTT-Broker ver\u00f6ffentlichen. Wenn die Erkennung deaktiviert ist, muss die gesamte Konfiguration manuell vorgenommen werden.\nGeburtsnachricht - Die Geburtsnachricht wird jedes Mal gesendet, wenn sich Home Assistant (erneut) mit dem MQTT-Broker verbindet.\nWill-Nachricht - Die Will-Nachricht wird jedes Mal gesendet, wenn Home Assistant die Verbindung zum Broker verliert, sowohl im Falle einer sauberen (z. B. Herunterfahren von Home Assistant) als auch im Falle einer unsauberen (z. B. Absturz von Home Assistant oder Verlust der Netzwerkverbindung) Verbindungstrennung.",
+                "description": "Erkennung - Wenn die Erkennung aktiviert ist (empfohlen), erkennt Home Assistant automatisch Ger\u00e4te und Einheiten, die ihre Konfiguration auf dem MQTT-Broker ver\u00f6ffentlichen. Wenn die Erkennung deaktiviert ist, muss die gesamte Konfiguration manuell vorgenommen werden.\nErkennungspr\u00e4fix - Das Pr\u00e4fix, mit dem ein Konfigurationsthema f\u00fcr die automatische Erkennung beginnen muss.\nBirth-Nachricht - Die Birth-Nachricht wird jedes Mal gesendet, wenn sich Home Assistant (erneut) mit dem MQTT-Broker verbindet.\nWill-Nachricht - Die Will-Nachricht wird jedes Mal gesendet, wenn Home Assistant die Verbindung zum Broker verliert, sowohl im Falle einer sauberen (z.B. Home Assistant wird heruntergefahren) als auch im Falle einer unsauberen (z.B. Home Assistant st\u00fcrzt ab oder verliert die Netzwerkverbindung) Trennung der Verbindung.",
                 "title": "MQTT-Optionen"
             }
         }
diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json
index 92ab99d90dc..f60f457abd3 100644
--- a/homeassistant/components/mqtt/translations/en.json
+++ b/homeassistant/components/mqtt/translations/en.json
@@ -6,11 +6,11 @@
         },
         "error": {
             "bad_birth": "Invalid birth topic",
-            "bad_discovery_prefix": "Invalid discovery prefix",
             "bad_certificate": "The CA certificate is invalid",
             "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
             "bad_client_cert_key": "Client certificate and private are no valid pair",
             "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
+            "bad_discovery_prefix": "Invalid discovery prefix",
             "bad_will": "Invalid will topic",
             "cannot_connect": "Failed to connect",
             "invalid_inclusion": "The client certificate and private key must be configurered together"
@@ -21,9 +21,10 @@
                     "advanced_options": "Advanced options",
                     "broker": "Broker",
                     "certificate": "Path to custom CA certificate file",
-                    "client_id": "Client ID (leave empty to randomly generated one)",
                     "client_cert": "Path to a client certificate file",
+                    "client_id": "Client ID (leave empty to randomly generated one)",
                     "client_key": "Path to a private key file",
+                    "discovery": "Enable discovery",
                     "keepalive": "The time between sending keep alive messages",
                     "password": "Password",
                     "port": "Port",
@@ -72,18 +73,18 @@
             "title": "Your manually configured MQTT {platform}(s) needs attention"
         },
         "deprecated_yaml_broker_settings": {
-            "title": "Deprecated MQTT settings found in `configuration.yaml`",
-            "description": "The following settings found in `configuration.yaml` were migrated to MQTT config entry and will now override the settings in `configuration.yaml`:\n`{deprecated_settings}`\n\nPlease remove these settings from `configuration.yaml` and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information."
+            "description": "The following settings found in `configuration.yaml` were migrated to MQTT config entry and will now override the settings in `configuration.yaml`:\n`{deprecated_settings}`\n\nPlease remove these settings from `configuration.yaml` and restart Home Assistant to fix this issue. See the [documentation]({more_info_url}), for more information.",
+            "title": "Deprecated MQTT settings found in `configuration.yaml`"
         }
     },
     "options": {
         "error": {
             "bad_birth": "Invalid birth topic",
-            "bad_discovery_prefix": "Invalid discovery prefix",
             "bad_certificate": "The CA certificate is invalid",
             "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied",
             "bad_client_cert_key": "Client certificate and private are no valid pair",
             "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password",
+            "bad_discovery_prefix": "Invalid discovery prefix",
             "bad_will": "Invalid will topic",
             "cannot_connect": "Failed to connect",
             "invalid_inclusion": "The client certificate and private key must be configured together"
@@ -94,8 +95,8 @@
                     "advanced_options": "Advanced options",
                     "broker": "Broker",
                     "certificate": "Upload custom CA certificate file",
-                    "client_id": "Client ID (leave empty to randomly generated one)",
                     "client_cert": "Upload client certificate file",
+                    "client_id": "Client ID (leave empty to randomly generated one)",
                     "client_key": "Upload private key file",
                     "keepalive": "The time between sending keep alive messages",
                     "password": "Password",
diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json
index e87f078c5b8..8c05afe997a 100644
--- a/homeassistant/components/mqtt/translations/es.json
+++ b/homeassistant/components/mqtt/translations/es.json
@@ -5,15 +5,33 @@
             "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
         },
         "error": {
-            "cannot_connect": "No se pudo conectar"
+            "bad_birth": "Tema de nacimiento no v\u00e1lido",
+            "bad_certificate": "El certificado de la CA no es v\u00e1lido",
+            "bad_client_cert": "Certificado de cliente no v\u00e1lido, aseg\u00farate de proporcionar un archivo codificado PEM",
+            "bad_client_cert_key": "Certificado de cliente y la clave privada no son un par v\u00e1lido",
+            "bad_client_key": "Clave privada no v\u00e1lida, aseg\u00farate de que se proporcione un archivo codificado PEM sin contrase\u00f1a",
+            "bad_discovery_prefix": "Prefijo de descubrimiento no v\u00e1lido",
+            "bad_will": "Tema de voluntad no v\u00e1lido",
+            "cannot_connect": "No se pudo conectar",
+            "invalid_inclusion": "El certificado del cliente y la clave privada deben configurarse juntos"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opciones avanzadas",
                     "broker": "Br\u00f3ker",
+                    "certificate": "Ruta al archivo de certificado de la CA personalizado",
+                    "client_cert": "Ruta a un archivo de certificado de cliente",
+                    "client_id": "ID de cliente (dejar vac\u00edo para generar uno aleatoriamente)",
+                    "client_key": "Ruta a un archivo de clave privada",
                     "discovery": "Habilitar descubrimiento",
+                    "keepalive": "El tiempo entre el env\u00edo de mensajes keep alive",
                     "password": "Contrase\u00f1a",
                     "port": "Puerto",
+                    "protocol": "Protocolo MQTT",
+                    "set_ca_cert": "Validaci\u00f3n del certificado del br\u00f3ker",
+                    "set_client_cert": "Utilizar un certificado de cliente",
+                    "tls_insecure": "Ignorar la validaci\u00f3n del certificado del br\u00f3ker",
                     "username": "Nombre de usuario"
                 },
                 "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT."
@@ -53,20 +71,40 @@
         "deprecated_yaml": {
             "description": "Configuraci\u00f3n manual MQTT de {platform}(s) encontrada en la clave de la plataforma `{platform}`.\n\nPor favor, mueve la configuraci\u00f3n a la clave `mqtt` de la integraci\u00f3n y reinicia Home Assistant para solucionar este problema. Consulta la [documentaci\u00f3n]({more_info_url}), para obtener m\u00e1s informaci\u00f3n.",
             "title": "Tu configuraci\u00f3n manual MQTT de {platform}(s) necesita atenci\u00f3n"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "Los siguientes ajustes que se encuentran en `configuration.yaml` se migraron a la entrada de configuraci\u00f3n de MQTT y ahora anular\u00e1n las configuraciones en `configuration.yaml`:\n`{deprecated_settings}` \n\nElimina esta configuraci\u00f3n de `configuration.yaml` y reinicia Home Assistant para solucionar este problema. Consulta la [documentaci\u00f3n]({more_info_url}) para obtener m\u00e1s informaci\u00f3n.",
+            "title": "Se encontraron ajustes obsoletos de MQTT `configuration.yaml`"
         }
     },
     "options": {
         "error": {
-            "bad_birth": "Tema de nacimiento no v\u00e1lido.",
-            "bad_will": "Tema de voluntad no v\u00e1lido.",
-            "cannot_connect": "No se pudo conectar"
+            "bad_birth": "Tema de nacimiento no v\u00e1lido",
+            "bad_certificate": "El certificado de la CA no es v\u00e1lido",
+            "bad_client_cert": "Certificado de cliente no v\u00e1lido, aseg\u00farate de proporcionar un archivo codificado PEM",
+            "bad_client_cert_key": "Certificado de cliente y la clave privada no son un par v\u00e1lido",
+            "bad_client_key": "Clave privada no v\u00e1lida, aseg\u00farate de que se proporcione un archivo codificado PEM sin contrase\u00f1a",
+            "bad_discovery_prefix": "Prefijo de descubrimiento no v\u00e1lido",
+            "bad_will": "Tema de voluntad no v\u00e1lido",
+            "cannot_connect": "No se pudo conectar",
+            "invalid_inclusion": "El certificado del cliente y la clave privada deben configurarse juntos"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opciones avanzadas",
                     "broker": "Br\u00f3ker",
+                    "certificate": "Subir archivo de certificado de la CA personalizado",
+                    "client_cert": "Subir archivo de certificado de cliente",
+                    "client_id": "ID de cliente (dejar vac\u00edo para generar uno aleatoriamente)",
+                    "client_key": "Subir archivo de clave privada",
+                    "keepalive": "El tiempo entre el env\u00edo de mensajes keep alive",
                     "password": "Contrase\u00f1a",
                     "port": "Puerto",
+                    "protocol": "Protocolo MQTT",
+                    "set_ca_cert": "Validaci\u00f3n del certificado del br\u00f3ker",
+                    "set_client_cert": "Utilizar un certificado de cliente",
+                    "tls_insecure": "Ignorar la validaci\u00f3n del certificado del br\u00f3ker",
                     "username": "Nombre de usuario"
                 },
                 "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT.",
@@ -80,13 +118,14 @@
                     "birth_retain": "Retenci\u00f3n del mensaje de nacimiento",
                     "birth_topic": "Tema del mensaje de nacimiento",
                     "discovery": "Habilitar descubrimiento",
+                    "discovery_prefix": "Prefijo de descubrimiento",
                     "will_enable": "Habilitar mensaje de voluntad",
                     "will_payload": "Carga del mensaje de voluntad",
                     "will_qos": "QoS del mensaje de voluntad",
                     "will_retain": "Retenci\u00f3n del mensaje de voluntad",
                     "will_topic": "Tema del mensaje de voluntad"
                 },
-                "description": "Descubrimiento: si el descubrimiento est\u00e1 habilitado (recomendado), Home Assistant descubrir\u00e1 autom\u00e1ticamente los dispositivos y entidades que publican su configuraci\u00f3n en el br\u00f3ker MQTT. Si el descubrimiento est\u00e1 deshabilitado, toda la configuraci\u00f3n debe realizarse manualmente.\nMensaje de nacimiento: el mensaje de nacimiento se enviar\u00e1 cada vez que Home Assistant se (re)conecte con el br\u00f3ker MQTT.\nMensaje de voluntad: el mensaje de voluntad se enviar\u00e1 cada vez que Home Assistant pierda su conexi\u00f3n con el br\u00f3ker, tanto en caso de desconexi\u00f3n limpia (por ejemplo, que Home Assistant se apague) como en caso de desconexi\u00f3n no limpia (por ejemplo, Home Assistant se cuelgue o pierda su conexi\u00f3n de red).",
+                "description": "Descubrimiento: si el descubrimiento est\u00e1 habilitado (recomendado), Home Assistant descubrir\u00e1 autom\u00e1ticamente los dispositivos y entidades que publican su configuraci\u00f3n en el agente MQTT. Si el descubrimiento est\u00e1 deshabilitado, toda la configuraci\u00f3n debe realizarse manualmente.\nPrefijo de descubrimiento: el prefijo con el que debe comenzar un tema de configuraci\u00f3n para el descubrimiento autom\u00e1tico.\nMensaje de nacimiento: el mensaje de nacimiento se enviar\u00e1 cada vez que Home Assistant se (re)conecte con el br\u00f3ker MQTT.\nMensaje de voluntad: el mensaje de voluntad se enviar\u00e1 cada vez que Home Assistant pierda su conexi\u00f3n con el br\u00f3ker, tanto en caso de desconexi\u00f3n limpia (por ejemplo, que Home Assistant se apague) como no limpia (por ejemplo, Home Assistant se cuelgue o pierda su conexi\u00f3n de red).",
                 "title": "Opciones de MQTT"
             }
         }
diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json
index 9b85b6e51bb..672137013e5 100644
--- a/homeassistant/components/mqtt/translations/et.json
+++ b/homeassistant/components/mqtt/translations/et.json
@@ -65,8 +65,14 @@
             "broker": {
                 "data": {
                     "broker": "Vahendaja",
+                    "client_key": "Privaatse v\u00f5tme faili \u00fcleslaadimine",
+                    "keepalive": "Aegumiss\u00f5numite saatmise vaheline aeg",
                     "password": "Salas\u00f5na",
                     "port": "Port",
+                    "protocol": "MQTT protokoll",
+                    "set_ca_cert": "Sertifikaadi kinnitamine",
+                    "set_client_cert": "Kasuta kliendi sertifikaati",
+                    "tls_insecure": "Eira serdi valideerimist",
                     "username": "Kasutajanimi"
                 },
                 "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave.",
@@ -80,6 +86,7 @@
                     "birth_retain": "S\u00fcnniteate j\u00e4\u00e4dvustamine",
                     "birth_topic": "S\u00fcnniteate teema",
                     "discovery": "Luba avastamine",
+                    "discovery_prefix": "Avastamise eesliide",
                     "will_enable": "Luba loomisteavitus",
                     "will_payload": "L\u00f5petamisteate v\u00e4\u00e4rtus",
                     "will_qos": "L\u00f5petamisteate QoS",
diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json
index 87ce8c2bdee..49d041bc0f8 100644
--- a/homeassistant/components/mqtt/translations/fr.json
+++ b/homeassistant/components/mqtt/translations/fr.json
@@ -5,15 +5,19 @@
             "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
         },
         "error": {
+            "bad_discovery_prefix": "Pr\u00e9fixe de d\u00e9couverte non valide",
             "cannot_connect": "\u00c9chec de connexion"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Options avanc\u00e9es",
                     "broker": "Broker",
                     "discovery": "Activer la d\u00e9couverte",
                     "password": "Mot de passe",
                     "port": "Port",
+                    "protocol": "Protocole MQTT",
+                    "set_client_cert": "Utiliser un certificat client",
                     "username": "Nom d'utilisateur"
                 },
                 "description": "Veuillez entrer les informations de connexion de votre broker MQTT."
@@ -57,15 +61,19 @@
     "options": {
         "error": {
             "bad_birth": "Sujet de la naissance non valide.",
+            "bad_discovery_prefix": "Pr\u00e9fixe de d\u00e9couverte non valide",
             "bad_will": "Sujet du testament non valide.",
             "cannot_connect": "\u00c9chec de connexion"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Options avanc\u00e9es",
                     "broker": "Broker",
                     "password": "Mot de passe",
                     "port": "Port",
+                    "protocol": "Protocole MQTT",
+                    "set_client_cert": "Utiliser un certificat client",
                     "username": "Nom d'utilisateur"
                 },
                 "description": "Veuillez entrer les informations de connexion de votre broker MQTT.",
@@ -79,6 +87,7 @@
                     "birth_retain": "Retenir le message de naissance",
                     "birth_topic": "Topic du message de naissance",
                     "discovery": "Activer la d\u00e9couverte",
+                    "discovery_prefix": "Pr\u00e9fixe de d\u00e9couverte",
                     "will_enable": "Activer le message de naissance",
                     "will_payload": "Contenu du message de testament",
                     "will_qos": "QoS du message de testament",
diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json
index 51860685270..d9e8ac43192 100644
--- a/homeassistant/components/mqtt/translations/pt-BR.json
+++ b/homeassistant/components/mqtt/translations/pt-BR.json
@@ -5,15 +5,33 @@
             "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel."
         },
         "error": {
-            "cannot_connect": "Falha ao conectar"
+            "bad_birth": "T\u00f3pico de nascimento inv\u00e1lido",
+            "bad_certificate": "O certificado CA \u00e9 inv\u00e1lido",
+            "bad_client_cert": "Certificado de cliente inv\u00e1lido, certifique-se de que um arquivo codificado PEM seja fornecido",
+            "bad_client_cert_key": "Certificado de cliente e privado n\u00e3o s\u00e3o pares v\u00e1lidos",
+            "bad_client_key": "Chave privada inv\u00e1lida, certifique-se de que um arquivo codificado PEM seja fornecido sem senha",
+            "bad_discovery_prefix": "Prefixo de descoberta inv\u00e1lido",
+            "bad_will": "T\u00f3pico de vontade inv\u00e1lido",
+            "cannot_connect": "Falha ao conectar",
+            "invalid_inclusion": "O certificado do cliente e a chave privada devem ser configurados juntos"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Op\u00e7\u00f5es avan\u00e7adas",
                     "broker": "Endere\u00e7o do Broker",
+                    "certificate": "Caminho para o arquivo de certificado de CA personalizado",
+                    "client_cert": "Caminho para um arquivo de certificado de cliente",
+                    "client_id": "ID do cliente (deixe em branco para um gerado aleatoriamente)",
+                    "client_key": "Caminho para um arquivo de chave privada",
                     "discovery": "Ativar descoberta",
+                    "keepalive": "O tempo entre o envio de mensagens de manuten\u00e7\u00e3o viva",
                     "password": "Senha",
                     "port": "Porta",
+                    "protocol": "Protocolo MQTT",
+                    "set_ca_cert": "Valida\u00e7\u00e3o do certificado do corretor",
+                    "set_client_cert": "Usar um certificado de cliente",
+                    "tls_insecure": "Ignorar a valida\u00e7\u00e3o do certificado do corretor",
                     "username": "Usu\u00e1rio"
                 },
                 "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT."
@@ -53,20 +71,40 @@
         "deprecated_yaml": {
             "description": "MQTT configurado manualmente {platform}(s) encontrado na chave de plataforma ` {platform} `. \n\n Por favor, mova a configura\u00e7\u00e3o para a chave de integra\u00e7\u00e3o `mqtt` e reinicie o Home Assistant para corrigir este problema. Consulte a [documenta\u00e7\u00e3o]( {more_info_url} ), para mais informa\u00e7\u00f5es.",
             "title": "Sua(s) {platform}(s) MQTT configurada(s) manualmente precisa(m) de aten\u00e7\u00e3o"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "As seguintes configura\u00e7\u00f5es encontradas em `configuration.yaml` foram migradas para a entrada de configura\u00e7\u00e3o MQTT e agora substituir\u00e3o as configura\u00e7\u00f5es em `configuration.yaml`:\n `{deprecated_settings}` \n\n Remova essas configura\u00e7\u00f5es de `configuration.yaml` e reinicie o Home Assistant para corrigir esse problema. Consulte a [documenta\u00e7\u00e3o]( {more_info_url} ), para mais informa\u00e7\u00f5es.",
+            "title": "Configura\u00e7\u00f5es de MQTT obsoletas encontradas em `configuration.yaml`"
         }
     },
     "options": {
         "error": {
-            "bad_birth": "T\u00f3pico \u00b4Birth message\u00b4 inv\u00e1lido",
-            "bad_will": "T\u00f3pico \u00b4Will message\u00b4 inv\u00e1lido",
-            "cannot_connect": "Falha ao conectar"
+            "bad_birth": "T\u00f3pico de nascimento inv\u00e1lido",
+            "bad_certificate": "O certificado CA \u00e9 inv\u00e1lido",
+            "bad_client_cert": "Certificado de cliente inv\u00e1lido, certifique-se de que um arquivo codificado PEM seja fornecido",
+            "bad_client_cert_key": "Certificado de cliente e privado n\u00e3o s\u00e3o pares v\u00e1lidos",
+            "bad_client_key": "Chave privada inv\u00e1lida, certifique-se de que um arquivo codificado PEM seja fornecido sem senha",
+            "bad_discovery_prefix": "Prefixo de descoberta inv\u00e1lido",
+            "bad_will": "T\u00f3pico de vontade inv\u00e1lido",
+            "cannot_connect": "Falha ao conectar",
+            "invalid_inclusion": "O certificado do cliente e a chave privada devem ser configurados juntos"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Op\u00e7\u00f5es avan\u00e7adas",
                     "broker": "",
+                    "certificate": "Carregar arquivo de certificado de CA personalizado",
+                    "client_cert": "Carregar arquivo de certificado do cliente",
+                    "client_id": "ID do cliente (deixe em branco para um gerado aleatoriamente)",
+                    "client_key": "Carregar arquivo de chave privada",
+                    "keepalive": "O tempo entre o envio de mensagens de manuten\u00e7\u00e3o viva",
                     "password": "Senha",
                     "port": "Porta",
+                    "protocol": "Protocolo MQTT",
+                    "set_ca_cert": "Valida\u00e7\u00e3o do certificado do corretor",
+                    "set_client_cert": "Usar um certificado de cliente",
+                    "tls_insecure": "Ignorar a valida\u00e7\u00e3o do certificado do corretor",
                     "username": "Usu\u00e1rio"
                 },
                 "description": "Insira as informa\u00e7\u00f5es de conex\u00e3o do seu broker MQTT.",
@@ -80,13 +118,14 @@
                     "birth_retain": "Retain \u00b4Birth message\u00b4",
                     "birth_topic": "T\u00f3pico \u00b4Birth message\u00b4",
                     "discovery": "Ativar descoberta",
+                    "discovery_prefix": "Prefixo de descoberta",
                     "will_enable": "Ativar `Will message`",
                     "will_payload": "Payload `Will message`",
                     "will_qos": "QoS `Will message`",
                     "will_retain": "Retain `Will message`",
                     "will_topic": "T\u00f3pico `Will message`"
                 },
-                "description": "Descoberta - Se a descoberta estiver habilitada (recomendado), o Home Assistant descobrir\u00e1 automaticamente dispositivos e entidades que publicam suas configura\u00e7\u00f5es no broker MQTT. Se a descoberta estiver desabilitada, toda a configura\u00e7\u00e3o dever\u00e1 ser feita manualmente.\n\u00b4Birth message\u00b4 - Ser\u00e1 enviada sempre que o Home Assistant (re)conectar-se ao broker MQTT.\n`Will message` - Ser\u00e1 enviada sempre que o Home Assistant perder sua conex\u00e3o com o broker, tanto no caso de uma parada programada (por exemplo, o Home Assistant desligando) quanto no caso de uma parada inesperada (por exemplo, o Home Assistant travando ou perdendo sua conex\u00e3o de rede).",
+                "description": "Descoberta - Se a descoberta estiver habilitada (recomendado), o Home Assistant descobrir\u00e1 automaticamente dispositivos e entidades que publicam suas configura\u00e7\u00f5es no broker MQTT. Se a descoberta estiver desabilitada, toda a configura\u00e7\u00e3o dever\u00e1 ser feita manualmente.\n Prefixo de descoberta - O prefixo com o qual um t\u00f3pico de configura\u00e7\u00e3o para descoberta autom\u00e1tica deve come\u00e7ar.\n Mensagem de nascimento - A mensagem de nascimento ser\u00e1 enviada sempre que o Home Assistant (re)conectar-se ao broker MQTT.\n Mensagem de vontade - A mensagem de vontade ser\u00e1 enviada sempre que o Home Assistant perder sua conex\u00e3o com o corretor, tanto no caso de uma limpeza (por exemplo, o Home Assistant desligando) quanto no caso de uma limpeza (por exemplo, o Home Assistant travando ou perdendo sua conex\u00e3o de rede) desconectar.",
                 "title": "Op\u00e7\u00f5es de MQTT"
             }
         }
diff --git a/homeassistant/components/nibe_heatpump/translations/de.json b/homeassistant/components/nibe_heatpump/translations/de.json
index 8eda1b68b8b..5cddee9d912 100644
--- a/homeassistant/components/nibe_heatpump/translations/de.json
+++ b/homeassistant/components/nibe_heatpump/translations/de.json
@@ -4,7 +4,7 @@
             "already_configured": "Ger\u00e4t ist bereits konfiguriert"
         },
         "error": {
-            "address": "Ung\u00fcltige Remote-IP-Adresse angegeben. Die Adresse muss eine IPV4-Adresse sein.",
+            "address": "Ung\u00fcltige Remote-Adresse angegeben. Die Adresse muss eine IP-Adresse oder ein aufl\u00f6sbarer Hostname sein.",
             "address_in_use": "Der ausgew\u00e4hlte Listening-Port wird auf diesem System bereits verwendet.",
             "model": "Das ausgew\u00e4hlte Modell scheint modbus40 nicht zu unterst\u00fctzen",
             "read": "Fehler bei Leseanforderung von Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Leseport\u201c oder \u201eRemote-IP-Adresse\u201c.",
@@ -14,11 +14,18 @@
         "step": {
             "user": {
                 "data": {
-                    "ip_address": "Remote-IP-Adresse",
+                    "ip_address": "Remote-Adresse",
                     "listening_port": "Lokaler Leseport",
                     "remote_read_port": "Remote-Leseport",
                     "remote_write_port": "Remote-Schreibport"
-                }
+                },
+                "data_description": {
+                    "ip_address": "Die Adresse des NibeGW-Ger\u00e4ts. Das Ger\u00e4t sollte mit einer statischen Adresse konfiguriert worden sein.",
+                    "listening_port": "Der lokale Port auf diesem System, an den das NibeGW-Ger\u00e4t Daten senden soll.",
+                    "remote_read_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Leseanfragen wartet.",
+                    "remote_write_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Schreibanfragen wartet."
+                },
+                "description": "Bevor du versuchst, die Integration zu konfigurieren, \u00fcberpr\u00fcfe folgendes:\n - Das NibeGW-Ger\u00e4t ist an eine W\u00e4rmepumpe angeschlossen.\n - Das MODBUS40-Zubeh\u00f6r wurde in der Konfiguration der W\u00e4rmepumpe aktiviert.\n - Die Pumpe ist nicht in einen Alarmzustand wegen fehlendem MODBUS40-Zubeh\u00f6r \u00fcbergegangen."
             }
         }
     }
diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json
index 459b0792df7..4c6e86720f1 100644
--- a/homeassistant/components/nibe_heatpump/translations/en.json
+++ b/homeassistant/components/nibe_heatpump/translations/en.json
@@ -1,5 +1,8 @@
 {
     "config": {
+        "abort": {
+            "already_configured": "Device is already configured"
+        },
         "error": {
             "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.",
             "address_in_use": "The selected listening port is already in use on this system.",
diff --git a/homeassistant/components/nibe_heatpump/translations/es.json b/homeassistant/components/nibe_heatpump/translations/es.json
index 60c228a28e7..0619471f538 100644
--- a/homeassistant/components/nibe_heatpump/translations/es.json
+++ b/homeassistant/components/nibe_heatpump/translations/es.json
@@ -4,7 +4,7 @@
             "already_configured": "El dispositivo ya est\u00e1 configurado"
         },
         "error": {
-            "address": "Se especific\u00f3 una direcci\u00f3n IP remota no v\u00e1lida. La direcci\u00f3n debe ser una direcci\u00f3n IPv4.",
+            "address": "Se especific\u00f3 una direcci\u00f3n remota no v\u00e1lida. La direcci\u00f3n debe ser una direcci\u00f3n IP o un nombre de host resoluble.",
             "address_in_use": "El puerto de escucha seleccionado ya est\u00e1 en uso en este sistema.",
             "model": "El modelo seleccionado no parece ser compatible con modbus40",
             "read": "Error en la solicitud de lectura de la bomba. Verifica tu `Puerto de lectura remoto` o `Direcci\u00f3n IP remota`.",
@@ -14,11 +14,18 @@
         "step": {
             "user": {
                 "data": {
-                    "ip_address": "Direcci\u00f3n IP remota",
+                    "ip_address": "Direcci\u00f3n remota",
                     "listening_port": "Puerto de escucha local",
                     "remote_read_port": "Puerto de lectura remoto",
                     "remote_write_port": "Puerto de escritura remoto"
-                }
+                },
+                "data_description": {
+                    "ip_address": "La direcci\u00f3n de la unidad NibeGW. El dispositivo deber\u00eda haber sido configurado con una direcci\u00f3n est\u00e1tica.",
+                    "listening_port": "El puerto local en este sistema, al que la unidad NibeGW est\u00e1 configurada para enviar datos.",
+                    "remote_read_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando las peticiones de lectura.",
+                    "remote_write_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando peticiones de escritura."
+                },
+                "description": "Antes de intentar configurar la integraci\u00f3n, verifica que:\n- La unidad NibeGW est\u00e1 conectada a una bomba de calor.\n- Se ha habilitado el accesorio MODBUS40 en la configuraci\u00f3n de la bomba de calor.\n- La bomba no ha entrado en estado de alarma por falta del accesorio MODBUS40."
             }
         }
     }
diff --git a/homeassistant/components/nibe_heatpump/translations/et.json b/homeassistant/components/nibe_heatpump/translations/et.json
index 3055a863bd4..a25e843855f 100644
--- a/homeassistant/components/nibe_heatpump/translations/et.json
+++ b/homeassistant/components/nibe_heatpump/translations/et.json
@@ -18,7 +18,14 @@
                     "listening_port": "Kohalik kuulamisport",
                     "remote_read_port": "Kauglugemise port",
                     "remote_write_port": "Kaugkirjutusport"
-                }
+                },
+                "data_description": {
+                    "ip_address": "NibeGW-\u00fcksuse aadress. Seade peaks olema seadistatud staatilise aadressiga.",
+                    "listening_port": "Selle s\u00fcsteemi kohalik port kuhu NibeGW seade on seadistatud andmeid saatma.",
+                    "remote_read_port": "Port, mille kaudu NibeGW-\u00fcksus loeb lugemisp\u00e4ringuid.",
+                    "remote_write_port": "Port, mille kaudu NibeGW-\u00fcksus kuulab kirjutamisp\u00e4ringuid."
+                },
+                "description": "Enne sidumise seadistamist veendu, et:\n - NibeGW seade on \u00fchendatud soojuspumbaga.\n - MODBUS40 tarvik on soojuspumba konfiguratsioonis lubatud.\n - Pump ei ole MODBUS40 lisaseadme puudumise t\u00f5ttu h\u00e4ireolekusse l\u00e4inud."
             }
         }
     }
diff --git a/homeassistant/components/nibe_heatpump/translations/fr.json b/homeassistant/components/nibe_heatpump/translations/fr.json
index dc28a729aea..6c12361adc5 100644
--- a/homeassistant/components/nibe_heatpump/translations/fr.json
+++ b/homeassistant/components/nibe_heatpump/translations/fr.json
@@ -9,7 +9,7 @@
         "step": {
             "user": {
                 "data": {
-                    "ip_address": "Adresse IP distante",
+                    "ip_address": "Adresse distante",
                     "listening_port": "Port d'\u00e9coute local",
                     "remote_read_port": "Port de lecture distant",
                     "remote_write_port": "Port d'\u00e9criture distant"
diff --git a/homeassistant/components/nibe_heatpump/translations/pt-BR.json b/homeassistant/components/nibe_heatpump/translations/pt-BR.json
index b20ab8acbf8..9f999846036 100644
--- a/homeassistant/components/nibe_heatpump/translations/pt-BR.json
+++ b/homeassistant/components/nibe_heatpump/translations/pt-BR.json
@@ -4,7 +4,7 @@
             "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado"
         },
         "error": {
-            "address": "Endere\u00e7o IP remoto inv\u00e1lido especificado. O endere\u00e7o deve ser um endere\u00e7o IPV4.",
+            "address": "Endere\u00e7o remoto inv\u00e1lido especificado. O endere\u00e7o deve ser um endere\u00e7o IP ou um nome de host resolv\u00edvel.",
             "address_in_use": "A porta de escuta selecionada j\u00e1 est\u00e1 em uso neste sistema.",
             "model": "O modelo selecionado parece n\u00e3o suportar modbus40",
             "read": "Erro na solicita\u00e7\u00e3o de leitura da bomba. Verifique sua `Porta de leitura remota` ou `Endere\u00e7o IP remoto`.",
@@ -18,7 +18,14 @@
                     "listening_port": "Porta de escuta local",
                     "remote_read_port": "Porta de leitura remota",
                     "remote_write_port": "Porta de grava\u00e7\u00e3o remota"
-                }
+                },
+                "data_description": {
+                    "ip_address": "O endere\u00e7o da unidade NibeGW. O dispositivo deve ter sido configurado com um endere\u00e7o est\u00e1tico.",
+                    "listening_port": "A porta local neste sistema para a qual a unidade NibeGW est\u00e1 configurada para enviar dados.",
+                    "remote_read_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de leitura.",
+                    "remote_write_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de grava\u00e7\u00e3o."
+                },
+                "description": "Antes de tentar configurar a integra\u00e7\u00e3o, verifique se:\n - A unidade NibeGW est\u00e1 conectada a uma bomba de calor.\n - O acess\u00f3rio MODBUS40 foi habilitado na configura\u00e7\u00e3o da bomba de calor.\n - A bomba n\u00e3o entrou em estado de alarme por falta de acess\u00f3rio MODBUS40."
             }
         }
     }
diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json
index f6145f49998..13c258d4016 100644
--- a/homeassistant/components/sensibo/translations/de.json
+++ b/homeassistant/components/sensibo/translations/de.json
@@ -17,7 +17,7 @@
                     "api_key": "API-Schl\u00fcssel"
                 },
                 "data_description": {
-                    "api_key": "Folge der Dokumentation, um einen neuen Api-Schl\u00fcssel zu erhalten."
+                    "api_key": "Folge der Dokumentation, um deinen API-Schl\u00fcssel zu erhalten."
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "API-Schl\u00fcssel"
                 },
                 "data_description": {
-                    "api_key": "Folge der Dokumentation, um deinen Api-Schl\u00fcssel zu erhalten."
+                    "api_key": "Folge der Dokumentation, um deinen API-Schl\u00fcssel zu erhalten."
                 }
             }
         }
diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json
index acd3eb5ac1a..803dcc01600 100644
--- a/homeassistant/components/sensibo/translations/et.json
+++ b/homeassistant/components/sensibo/translations/et.json
@@ -17,7 +17,7 @@
                     "api_key": "API v\u00f5ti"
                 },
                 "data_description": {
-                    "api_key": "Uue API-v\u00f5tme saamiseks j\u00e4rgi dokumentatsiooni."
+                    "api_key": "API-v\u00f5tme saamiseks j\u00e4rgi dokumentatsiooni"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "API v\u00f5ti"
                 },
                 "data_description": {
-                    "api_key": "API-v\u00f5tme hankimiseks j\u00e4rgi dokumentatsiooni."
+                    "api_key": "API-v\u00f5tme hankimiseks j\u00e4rgi dokumentatsiooni"
                 }
             }
         }
diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json
index 02067c773fd..ebd4cb77b48 100644
--- a/homeassistant/components/sensibo/translations/no.json
+++ b/homeassistant/components/sensibo/translations/no.json
@@ -17,7 +17,7 @@
                     "api_key": "API-n\u00f8kkel"
                 },
                 "data_description": {
-                    "api_key": "F\u00f8lg dokumentasjonen for \u00e5 f\u00e5 en ny api-n\u00f8kkel."
+                    "api_key": "F\u00f8lg dokumentasjonen for \u00e5 f\u00e5 api-n\u00f8kkelen din"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "API-n\u00f8kkel"
                 },
                 "data_description": {
-                    "api_key": "F\u00f8lg dokumentasjonen for \u00e5 f\u00e5 api-n\u00f8kkelen din."
+                    "api_key": "F\u00f8lg dokumentasjonen for \u00e5 f\u00e5 api-n\u00f8kkelen din"
                 }
             }
         }
diff --git a/homeassistant/components/sensibo/translations/sensor.bg.json b/homeassistant/components/sensibo/translations/sensor.bg.json
index 0ab81a5de11..bae9aca4e35 100644
--- a/homeassistant/components/sensibo/translations/sensor.bg.json
+++ b/homeassistant/components/sensibo/translations/sensor.bg.json
@@ -3,6 +3,10 @@
         "sensibo__sensitivity": {
             "n": "\u041d\u043e\u0440\u043c\u0430\u043b\u0435\u043d",
             "s": "\u0427\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d"
+        },
+        "sensibo__smart_type": {
+            "humidity": "\u0412\u043b\u0430\u0436\u043d\u043e\u0441\u0442",
+            "temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.ca.json b/homeassistant/components/sensibo/translations/sensor.ca.json
index 1b251c70f7d..c9872da68db 100644
--- a/homeassistant/components/sensibo/translations/sensor.ca.json
+++ b/homeassistant/components/sensibo/translations/sensor.ca.json
@@ -3,6 +3,10 @@
         "sensibo__sensitivity": {
             "n": "Normal",
             "s": "Sensible"
+        },
+        "sensibo__smart_type": {
+            "humidity": "Humitat",
+            "temperature": "Temperatura"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.de.json b/homeassistant/components/sensibo/translations/sensor.de.json
index ab456f555af..07cd377de7c 100644
--- a/homeassistant/components/sensibo/translations/sensor.de.json
+++ b/homeassistant/components/sensibo/translations/sensor.de.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Normal",
             "s": "Empfindlich"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "F\u00fchlt sich an wie",
+            "humidity": "Luftfeuchtigkeit",
+            "temperature": "Temperatur"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.es.json b/homeassistant/components/sensibo/translations/sensor.es.json
index 1b251c70f7d..d55b36fcf52 100644
--- a/homeassistant/components/sensibo/translations/sensor.es.json
+++ b/homeassistant/components/sensibo/translations/sensor.es.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Normal",
             "s": "Sensible"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "Se siente como",
+            "humidity": "Humedad",
+            "temperature": "Temperatura"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.et.json b/homeassistant/components/sensibo/translations/sensor.et.json
index 44bdfe9183a..16e7eb1bb89 100644
--- a/homeassistant/components/sensibo/translations/sensor.et.json
+++ b/homeassistant/components/sensibo/translations/sensor.et.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Tavaline",
             "s": "Tundlik"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "Tundub nagu",
+            "humidity": "Niiskus",
+            "temperature": "Temperatuur"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.fr.json b/homeassistant/components/sensibo/translations/sensor.fr.json
index 1b251c70f7d..ffbdb551c95 100644
--- a/homeassistant/components/sensibo/translations/sensor.fr.json
+++ b/homeassistant/components/sensibo/translations/sensor.fr.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Normal",
             "s": "Sensible"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "Ressenti",
+            "humidity": "Humidit\u00e9",
+            "temperature": "Temp\u00e9rature"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.no.json b/homeassistant/components/sensibo/translations/sensor.no.json
index e3de20b4636..f70217ca2b9 100644
--- a/homeassistant/components/sensibo/translations/sensor.no.json
+++ b/homeassistant/components/sensibo/translations/sensor.no.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Vanlig",
             "s": "F\u00f8lsom"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "F\u00f8les som",
+            "humidity": "Fuktighet",
+            "temperature": "Temperatur"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.pt-BR.json b/homeassistant/components/sensibo/translations/sensor.pt-BR.json
index 91d092a1760..6b12febda32 100644
--- a/homeassistant/components/sensibo/translations/sensor.pt-BR.json
+++ b/homeassistant/components/sensibo/translations/sensor.pt-BR.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Normal",
             "s": "Sens\u00edvel"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "Parece que",
+            "humidity": "Umidade",
+            "temperature": "Temperatura"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json
index 2c89081e0bd..18517b48066 100644
--- a/homeassistant/components/sensor/translations/ca.json
+++ b/homeassistant/components/sensor/translations/ca.json
@@ -32,6 +32,7 @@
             "is_volatile_organic_compounds": "Concentraci\u00f3 actual de compostos org\u00e0nics vol\u00e0tils de {entity_name}",
             "is_voltage": "Voltatge actual de {entity_name}",
             "is_volume": "Volum actual de {entity_name}",
+            "is_water": "Aigua actual de {entity_name}",
             "is_weight": "Pes actual de {entity_name}"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "Canvia la concentraci\u00f3 de compostos org\u00e0nics vol\u00e0tils de {entity_name}",
             "voltage": "Canvia el voltatge de {entity_name}",
             "volume": "Canvia el volum de {entity_name}",
+            "water": "Canvia l'aigua de {entity_name}",
             "weight": "Canvia el pes de {entity_name}"
         }
     },
diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json
index 67008424f0d..7914f0ff0d3 100644
--- a/homeassistant/components/sensor/translations/es.json
+++ b/homeassistant/components/sensor/translations/es.json
@@ -32,6 +32,7 @@
             "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_volume": "Volumen actual de {entity_name}",
+            "is_water": "Agua actual de {entity_name}",
             "is_weight": "El peso actual de {entity_name}"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "La concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name} cambia",
             "voltage": "El voltaje de {entity_name} cambia",
             "volume": "El volumen de {entity_name} cambia",
+            "water": "El agua de {entity_name} cambia",
             "weight": "El peso de {entity_name} cambia"
         }
     },
diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json
index 00343d6ae10..cbc760ea929 100644
--- a/homeassistant/components/sensor/translations/et.json
+++ b/homeassistant/components/sensor/translations/et.json
@@ -32,6 +32,7 @@
             "is_volatile_organic_compounds": "Praegune {entity_name} lenduvate orgaaniliste \u00fchendite kontsentratsioonitase",
             "is_voltage": "Praegune {entity_name}pinge",
             "is_volume": "Praegune {entity_name} helitugevus",
+            "is_water": "Praegune {entity_name} veekulu",
             "is_weight": "Praegune {entity_name} kaal"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "{entity_name} lenduvate orgaaniliste \u00fchendite kontsentratsiooni muutused",
             "voltage": "{entity_name} pingemuutub",
             "volume": "{entity_name} helitugevus muutub",
+            "water": "{entity_name} veekulu muutub",
             "weight": "{entity_name} kaal muutus"
         }
     },
diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json
index 2339d5bc830..2ad6a9dda26 100644
--- a/homeassistant/components/xiaomi_miio/translations/bg.json
+++ b/homeassistant/components/xiaomi_miio/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",
-            "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",
+            "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435"
diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json
index 1a21bd9840a..07cdec38236 100644
--- a/homeassistant/components/xiaomi_miio/translations/he.json
+++ b/homeassistant/components/xiaomi_miio/translations/he.json
@@ -5,7 +5,8 @@
             "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",
             "incomplete_info": "\u05de\u05d9\u05d3\u05e2 \u05dc\u05d0 \u05e9\u05dc\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df, \u05dc\u05d0 \u05e1\u05d5\u05e4\u05e7\u05d5 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df.",
             "not_xiaomi_miio": "\u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da (\u05e2\u05d3\u05d9\u05d9\u05df) \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5.",
-            "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7"
+            "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": {
             "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json
index fc39a454eaf..8f06bee0223 100644
--- a/homeassistant/components/xiaomi_miio/translations/no.json
+++ b/homeassistant/components/xiaomi_miio/translations/no.json
@@ -5,7 +5,8 @@
             "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": "Re-autentisering var vellykket"
+            "reauth_successful": "Re-autentisering var vellykket",
+            "unknown": "Uventet feil"
         },
         "error": {
             "cannot_connect": "Tilkobling mislyktes",
-- 
GitLab


From 3759be09df09be61a4b880eaa58c7d9d8a099080 Mon Sep 17 00:00:00 2001
From: Ongy <github@ongy.net>
Date: Tue, 25 Oct 2022 04:45:01 +0200
Subject: [PATCH 770/985] Add media_player platform to Jellyfin (#76801)

---
 homeassistant/components/jellyfin/__init__.py |   5 +-
 .../components/jellyfin/browse_media.py       | 179 ++++++
 .../components/jellyfin/client_wrapper.py     |  21 +-
 homeassistant/components/jellyfin/const.py    |  28 +-
 .../components/jellyfin/coordinator.py        |   4 +-
 .../components/jellyfin/media_player.py       | 295 ++++++++++
 homeassistant/components/jellyfin/models.py   |   1 +
 tests/components/jellyfin/conftest.py         |  30 ++
 .../fixtures/get-item-collection.json         | 504 +++++++++++++++++
 .../jellyfin/fixtures/get-media-folders.json  | 510 ++++++++++++++++++
 .../fixtures/user-items-parent-id.json        | 510 ++++++++++++++++++
 .../jellyfin/fixtures/user-items.json         | 510 ++++++++++++++++++
 .../components/jellyfin/test_media_player.py  | 356 ++++++++++++
 13 files changed, 2947 insertions(+), 6 deletions(-)
 create mode 100644 homeassistant/components/jellyfin/browse_media.py
 create mode 100644 homeassistant/components/jellyfin/media_player.py
 create mode 100644 tests/components/jellyfin/fixtures/get-item-collection.json
 create mode 100644 tests/components/jellyfin/fixtures/get-media-folders.json
 create mode 100644 tests/components/jellyfin/fixtures/user-items-parent-id.json
 create mode 100644 tests/components/jellyfin/fixtures/user-items.json
 create mode 100644 tests/components/jellyfin/test_media_player.py

diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py
index e1d8600530f..39085317a54 100644
--- a/homeassistant/components/jellyfin/__init__.py
+++ b/homeassistant/components/jellyfin/__init__.py
@@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     )
 
     try:
-        _, connect_result = await validate_input(hass, dict(entry.data), client)
+        user_id, 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:
@@ -36,13 +36,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     server_info: dict[str, Any] = connect_result["Servers"][0]
 
     coordinators: dict[str, JellyfinDataUpdateCoordinator[Any]] = {
-        "sessions": SessionsDataUpdateCoordinator(hass, client, server_info),
+        "sessions": SessionsDataUpdateCoordinator(hass, client, server_info, user_id),
     }
 
     for coordinator in coordinators.values():
         await coordinator.async_config_entry_first_refresh()
 
     hass.data[DOMAIN][entry.entry_id] = JellyfinData(
+        client_device_id=entry.data[CONF_CLIENT_DEVICE_ID],
         jellyfin_client=client,
         coordinators=coordinators,
     )
diff --git a/homeassistant/components/jellyfin/browse_media.py b/homeassistant/components/jellyfin/browse_media.py
new file mode 100644
index 00000000000..0e63cb2f5d2
--- /dev/null
+++ b/homeassistant/components/jellyfin/browse_media.py
@@ -0,0 +1,179 @@
+"""Support for media browsing."""
+from __future__ import annotations
+
+import asyncio
+from typing import Any
+
+from jellyfin_apiclient_python import JellyfinClient
+
+from homeassistant.components.media_player import BrowseError, MediaClass, MediaType
+from homeassistant.components.media_player.browse_media import BrowseMedia
+from homeassistant.core import HomeAssistant
+
+from .client_wrapper import get_artwork_url
+from .const import CONTENT_TYPE_MAP, MEDIA_CLASS_MAP, MEDIA_TYPE_NONE
+
+CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = {
+    MediaType.MUSIC: MediaClass.MUSIC,
+    MediaType.SEASON: MediaClass.SEASON,
+    MediaType.TVSHOW: MediaClass.TV_SHOW,
+    "boxset": MediaClass.DIRECTORY,
+    "collection": MediaClass.DIRECTORY,
+    "library": MediaClass.DIRECTORY,
+}
+
+JF_SUPPORTED_LIBRARY_TYPES = ["movies", "music", "tvshows"]
+
+PLAYABLE_MEDIA_TYPES = [
+    MediaType.EPISODE,
+    MediaType.MOVIE,
+    MediaType.MUSIC,
+]
+
+
+async def item_payload(
+    hass: HomeAssistant,
+    client: JellyfinClient,
+    user_id: str,
+    item: dict[str, Any],
+) -> BrowseMedia:
+    """Create response payload for a single media item."""
+    title = item["Name"]
+    thumbnail = get_artwork_url(client, item, 600)
+
+    media_content_id = item["Id"]
+    media_content_type = CONTENT_TYPE_MAP.get(item["Type"], MEDIA_TYPE_NONE)
+
+    return BrowseMedia(
+        title=title,
+        media_content_id=media_content_id,
+        media_content_type=media_content_type,
+        media_class=MEDIA_CLASS_MAP.get(item["Type"], MediaClass.DIRECTORY),
+        can_play=bool(media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id),
+        can_expand=bool(item.get("IsFolder")),
+        children_media_class=None,
+        thumbnail=thumbnail,
+    )
+
+
+async def build_root_response(
+    hass: HomeAssistant, client: JellyfinClient, user_id: str
+) -> BrowseMedia:
+    """Create response payload for root folder."""
+    folders = await hass.async_add_executor_job(client.jellyfin.get_media_folders)
+
+    children = [
+        await item_payload(hass, client, user_id, folder)
+        for folder in folders["Items"]
+        if folder["CollectionType"] in JF_SUPPORTED_LIBRARY_TYPES
+    ]
+
+    return BrowseMedia(
+        media_content_id="",
+        media_content_type="root",
+        media_class=MediaClass.DIRECTORY,
+        children_media_class=MediaClass.DIRECTORY,
+        title="Jellyfin",
+        can_play=False,
+        can_expand=True,
+        children=children,
+    )
+
+
+async def build_item_response(
+    hass: HomeAssistant,
+    client: JellyfinClient,
+    user_id: str,
+    media_content_type: str | None,
+    media_content_id: str,
+) -> BrowseMedia:
+    """Create response payload for the provided media query."""
+    title, media, thumbnail = await get_media_info(
+        hass, client, user_id, media_content_type, media_content_id
+    )
+
+    if title is None or media is None:
+        raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
+
+    children = await asyncio.gather(
+        *(item_payload(hass, client, user_id, media_item) for media_item in media)
+    )
+
+    response = BrowseMedia(
+        media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
+            str(media_content_type), MediaClass.DIRECTORY
+        ),
+        media_content_id=media_content_id,
+        media_content_type=str(media_content_type),
+        title=title,
+        can_play=bool(media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id),
+        can_expand=True,
+        children=children,
+        thumbnail=thumbnail,
+    )
+
+    response.calculate_children_class()
+
+    return response
+
+
+def fetch_item(client: JellyfinClient, item_id: str) -> dict[str, Any] | None:
+    """Fetch item from Jellyfin server."""
+    result = client.jellyfin.get_item(item_id)
+
+    if not result:
+        return None
+
+    item: dict[str, Any] = result
+    return item
+
+
+def fetch_items(
+    client: JellyfinClient,
+    params: dict[str, Any],
+) -> list[dict[str, Any]] | None:
+    """Fetch items from Jellyfin server."""
+    result = client.jellyfin.user_items(params=params)
+
+    if not result or "Items" not in result or len(result["Items"]) < 1:
+        return None
+
+    items: list[dict[str, Any]] = result["Items"]
+
+    return [
+        item
+        for item in items
+        if not item.get("IsFolder")
+        or (item.get("IsFolder") and item.get("ChildCount", 1) > 0)
+    ]
+
+
+async def get_media_info(
+    hass: HomeAssistant,
+    client: JellyfinClient,
+    user_id: str,
+    media_content_type: str | None,
+    media_content_id: str,
+) -> tuple[str | None, list[dict[str, Any]] | None, str | None]:
+    """Fetch media info."""
+    thumbnail: str | None = None
+    title: str | None = None
+    media: list[dict[str, Any]] | None = None
+
+    item = await hass.async_add_executor_job(fetch_item, client, media_content_id)
+
+    if item is None:
+        return None, None, None
+
+    title = item["Name"]
+    thumbnail = get_artwork_url(client, item)
+
+    if item.get("IsFolder"):
+        media = await hass.async_add_executor_job(
+            fetch_items, client, {"parentId": media_content_id, "fields": "childCount"}
+        )
+
+    if not media or len(media) == 0:
+        media = None
+
+    return title, media, thumbnail
diff --git a/homeassistant/components/jellyfin/client_wrapper.py b/homeassistant/components/jellyfin/client_wrapper.py
index c6ae67b4c80..ab771d405ea 100644
--- a/homeassistant/components/jellyfin/client_wrapper.py
+++ b/homeassistant/components/jellyfin/client_wrapper.py
@@ -15,7 +15,7 @@ from homeassistant import exceptions
 from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
 from homeassistant.core import HomeAssistant
 
-from .const import CLIENT_VERSION, USER_AGENT, USER_APP_NAME
+from .const import CLIENT_VERSION, ITEM_KEY_IMAGE_TAGS, USER_AGENT, USER_APP_NAME
 
 
 async def validate_input(
@@ -92,6 +92,25 @@ def _get_user_id(api: API) -> str:
     return userid
 
 
+def get_artwork_url(
+    client: JellyfinClient, item: dict[str, Any], max_width: int = 600
+) -> str | None:
+    """Find a suitable thumbnail for an item."""
+    artwork_id: str = item["Id"]
+    artwork_type = "Primary"
+    parent_backdrop_id: str | None = item.get("ParentBackdropItemId")
+
+    if "Backdrop" in item[ITEM_KEY_IMAGE_TAGS]:
+        artwork_type = "Backdrop"
+    elif parent_backdrop_id:
+        artwork_type = "Backdrop"
+        artwork_id = parent_backdrop_id
+    elif "Primary" not in item[ITEM_KEY_IMAGE_TAGS]:
+        return None
+
+    return str(client.jellyfin.artwork(artwork_id, artwork_type, max_width))
+
+
 class CannotConnect(exceptions.HomeAssistantError):
     """Error to indicate the server is unreachable."""
 
diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py
index 67956899cab..865e05a0081 100644
--- a/homeassistant/components/jellyfin/const.py
+++ b/homeassistant/components/jellyfin/const.py
@@ -2,6 +2,7 @@
 import logging
 from typing import Final
 
+from homeassistant.components.media_player import MediaClass, MediaType
 from homeassistant.const import Platform, __version__ as hass_version
 
 DOMAIN: Final = "jellyfin"
@@ -32,7 +33,6 @@ ITEM_TYPE_MOVIE: Final = "Movie"
 MAX_IMAGE_WIDTH: Final = 500
 MAX_STREAMING_BITRATE: Final = "140000000"
 
-
 MEDIA_SOURCE_KEY_PATH: Final = "Path"
 
 MEDIA_TYPE_AUDIO: Final = "Audio"
@@ -44,5 +44,29 @@ 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]
+CONTENT_TYPE_MAP = {
+    "Audio": MediaType.MUSIC,
+    "Episode": MediaType.EPISODE,
+    "Season": MediaType.SEASON,
+    "Series": MediaType.TVSHOW,
+    "Movie": MediaType.MOVIE,
+    "CollectionFolder": "collection",
+    "AggregateFolder": "library",
+    "Folder": "library",
+    "BoxSet": "boxset",
+}
+MEDIA_CLASS_MAP = {
+    "MusicAlbum": MediaClass.ALBUM,
+    "MusicArtist": MediaClass.ARTIST,
+    "Audio": MediaClass.MUSIC,
+    "Series": MediaClass.DIRECTORY,
+    "Movie": MediaClass.MOVIE,
+    "CollectionFolder": MediaClass.DIRECTORY,
+    "Folder": MediaClass.DIRECTORY,
+    "BoxSet": MediaClass.DIRECTORY,
+    "Episode": MediaClass.EPISODE,
+    "Season": MediaClass.SEASON,
+}
+
+PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR]
 LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py
index 3e277711f6c..626b2126fee 100644
--- a/homeassistant/components/jellyfin/coordinator.py
+++ b/homeassistant/components/jellyfin/coordinator.py
@@ -32,18 +32,20 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT]):
         hass: HomeAssistant,
         api_client: JellyfinClient,
         system_info: dict[str, Any],
+        user_id: str,
     ) -> None:
         """Initialize the coordinator."""
         super().__init__(
             hass=hass,
             logger=LOGGER,
             name=DOMAIN,
-            update_interval=timedelta(seconds=30),
+            update_interval=timedelta(seconds=10),
         )
         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")
+        self.user_id: str = user_id
 
     async def _async_update_data(self) -> JellyfinDataT:
         """Get the latest data from Jellyfin."""
diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py
new file mode 100644
index 00000000000..aea9fd4a594
--- /dev/null
+++ b/homeassistant/components/jellyfin/media_player.py
@@ -0,0 +1,295 @@
+"""Support for the Jellyfin media player."""
+from __future__ import annotations
+
+from typing import Any
+
+from homeassistant.components.media_player import (
+    MediaPlayerEntity,
+    MediaPlayerEntityDescription,
+    MediaPlayerEntityFeature,
+    MediaPlayerState,
+    MediaType,
+)
+from homeassistant.components.media_player.browse_media import BrowseMedia
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util.dt import parse_datetime
+
+from .browse_media import build_item_response, build_root_response
+from .client_wrapper import get_artwork_url
+from .const import CONTENT_TYPE_MAP, DOMAIN
+from .coordinator import JellyfinDataUpdateCoordinator
+from .entity import JellyfinEntity
+from .models import JellyfinData
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up Jellyfin media_player from a config entry."""
+    jellyfin_data: JellyfinData = hass.data[DOMAIN][entry.entry_id]
+    coordinator = jellyfin_data.coordinators["sessions"]
+
+    async_add_entities(
+        (
+            JellyfinMediaPlayer(coordinator, session_id, session_data)
+            for session_id, session_data in coordinator.data.items()
+            if session_data["DeviceId"] != jellyfin_data.client_device_id
+            and session_data["Client"] != "Home Assistant"
+        ),
+    )
+
+
+class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity):
+    """Represents a Jellyfin Player device."""
+
+    def __init__(
+        self,
+        coordinator: JellyfinDataUpdateCoordinator,
+        session_id: str,
+        session_data: dict[str, Any],
+    ) -> None:
+        """Initialize the Jellyfin Media Player entity."""
+        super().__init__(
+            coordinator,
+            MediaPlayerEntityDescription(
+                key=session_id,
+            ),
+        )
+
+        self.session_id = session_id
+        self.session_data: dict[str, Any] | None = session_data
+        self.device_id: str = session_data["DeviceId"]
+        self.device_name: str = session_data["DeviceName"]
+        self.client_name: str = session_data["Client"]
+        self.app_version: str = session_data["ApplicationVersion"]
+
+        self.capabilities: dict[str, Any] = session_data["Capabilities"]
+        self.now_playing: dict[str, Any] | None = session_data.get("NowPlayingItem")
+        self.play_state: dict[str, Any] | None = session_data.get("PlayState")
+
+        if self.capabilities.get("SupportsPersistentIdentifier", False):
+            self._attr_device_info = DeviceInfo(
+                identifiers={(DOMAIN, self.device_id)},
+                manufacturer="Jellyfin",
+                model=self.client_name,
+                name=self.device_name,
+                sw_version=self.app_version,
+                via_device=(DOMAIN, coordinator.server_id),
+            )
+        else:
+            self._attr_device_info = None
+            self._attr_has_entity_name = False
+            self._attr_name = self.device_name
+
+        self._update_from_session_data()
+
+    @callback
+    def _handle_coordinator_update(self) -> None:
+        self.session_data = (
+            self.coordinator.data.get(self.session_id)
+            if self.coordinator.data is not None
+            else None
+        )
+
+        if self.session_data is not None:
+            self.now_playing = self.session_data.get("NowPlayingItem")
+            self.play_state = self.session_data.get("PlayState")
+        else:
+            self.now_playing = None
+            self.play_state = None
+
+        self._update_from_session_data()
+        super()._handle_coordinator_update()
+
+    @callback
+    def _update_from_session_data(self) -> None:
+        """Process session data to update entity properties."""
+        state = None
+        media_content_type = None
+        media_content_id = None
+        media_title = None
+        media_series_title = None
+        media_season = None
+        media_episode = None
+        media_album_name = None
+        media_album_artist = None
+        media_artist = None
+        media_track = None
+        media_duration = None
+        media_position = None
+        media_position_updated = None
+        volume_muted = False
+        volume_level = None
+
+        if self.session_data is not None:
+            state = MediaPlayerState.IDLE
+            media_position_updated = (
+                parse_datetime(self.session_data["LastPlaybackCheckIn"])
+                if self.now_playing
+                else None
+            )
+
+        if self.now_playing is not None:
+            state = MediaPlayerState.PLAYING
+            media_content_type = CONTENT_TYPE_MAP.get(self.now_playing["Type"], None)
+            media_content_id = self.now_playing["Id"]
+            media_title = self.now_playing["Name"]
+            media_duration = int(self.now_playing["RunTimeTicks"] / 10000000)
+
+            if media_content_type == MediaType.EPISODE:
+                media_content_type = MediaType.TVSHOW
+                media_series_title = self.now_playing.get("SeriesName")
+                media_season = self.now_playing.get("ParentIndexNumber")
+                media_episode = self.now_playing.get("IndexNumber")
+            elif media_content_type == MediaType.MUSIC:
+                media_album_name = self.now_playing.get("Album")
+                media_album_artist = self.now_playing.get("AlbumArtist")
+                media_track = self.now_playing.get("IndexNumber")
+                if media_artists := self.now_playing.get("Artists"):
+                    media_artist = str(media_artists[0])
+
+        if self.play_state is not None:
+            if self.play_state.get("IsPaused"):
+                state = MediaPlayerState.PAUSED
+
+            media_position = (
+                int(self.play_state["PositionTicks"] / 10000000)
+                if "PositionTicks" in self.play_state
+                else None
+            )
+            volume_muted = bool(self.play_state.get("IsMuted", False))
+            volume_level = (
+                float(self.play_state["VolumeLevel"] / 100)
+                if "VolumeLevel" in self.play_state
+                else None
+            )
+
+        self._attr_state = state
+        self._attr_is_volume_muted = volume_muted
+        self._attr_volume_level = volume_level
+        self._attr_media_content_type = media_content_type
+        self._attr_media_content_id = media_content_id
+        self._attr_media_title = media_title
+        self._attr_media_series_title = media_series_title
+        self._attr_media_season = media_season
+        self._attr_media_episode = media_episode
+        self._attr_media_album_name = media_album_name
+        self._attr_media_album_artist = media_album_artist
+        self._attr_media_artist = media_artist
+        self._attr_media_track = media_track
+        self._attr_media_duration = media_duration
+        self._attr_media_position = media_position
+        self._attr_media_position_updated_at = media_position_updated
+        self._attr_media_image_remotely_accessible = True
+
+    @property
+    def media_image_url(self) -> str | None:
+        """Image url of current playing media."""
+        # We always need the now playing item.
+        # If there is none, there's also no url
+        if self.now_playing is None:
+            return None
+
+        return get_artwork_url(self.coordinator.api_client, self.now_playing, 150)
+
+    @property
+    def supported_features(self) -> int:
+        """Flag media player features that are supported."""
+        commands: list[str] = self.capabilities.get("SupportedCommands", [])
+        controllable = self.capabilities.get("SupportsMediaControl", False)
+        features = 0
+
+        if controllable:
+            features |= (
+                MediaPlayerEntityFeature.BROWSE_MEDIA
+                | MediaPlayerEntityFeature.PLAY_MEDIA
+                | MediaPlayerEntityFeature.PAUSE
+                | MediaPlayerEntityFeature.PLAY
+                | MediaPlayerEntityFeature.STOP
+                | MediaPlayerEntityFeature.SEEK
+            )
+
+            if "Mute" in commands:
+                features |= MediaPlayerEntityFeature.VOLUME_MUTE
+
+            if "VolumeSet" in commands:
+                features |= MediaPlayerEntityFeature.VOLUME_SET
+
+        return features
+
+    @property
+    def available(self) -> bool:
+        """Return if entity is available."""
+        return self.coordinator.last_update_success and self.session_data is not None
+
+    def media_seek(self, position: float) -> None:
+        """Send seek command."""
+        self.coordinator.api_client.jellyfin.remote_seek(
+            self.session_id, int(position * 10000000)
+        )
+
+    def media_pause(self) -> None:
+        """Send pause command."""
+        self.coordinator.api_client.jellyfin.remote_pause(self.session_id)
+        self._attr_state = MediaPlayerState.PAUSED
+
+    def media_play(self) -> None:
+        """Send play command."""
+        self.coordinator.api_client.jellyfin.remote_unpause(self.session_id)
+        self._attr_state = MediaPlayerState.PLAYING
+
+    def media_play_pause(self) -> None:
+        """Send the PlayPause command to the session."""
+        self.coordinator.api_client.jellyfin.remote_playpause(self.session_id)
+
+    def media_stop(self) -> None:
+        """Send stop command."""
+        self.coordinator.api_client.jellyfin.remote_stop(self.session_id)
+        self._attr_state = MediaPlayerState.IDLE
+
+    def play_media(
+        self, media_type: str, media_id: str, **kwargs: dict[str, Any]
+    ) -> None:
+        """Play a piece of media."""
+        self.coordinator.api_client.jellyfin.remote_play_media(
+            self.session_id, [media_id]
+        )
+
+    def set_volume_level(self, volume: float) -> None:
+        """Set volume level, range 0..1."""
+        self.coordinator.api_client.jellyfin.remote_set_volume(
+            self.session_id, int(volume * 100)
+        )
+
+    def mute_volume(self, mute: bool) -> None:
+        """Mute the volume."""
+        if mute:
+            self.coordinator.api_client.jellyfin.remote_mute(self.session_id)
+        else:
+            self.coordinator.api_client.jellyfin.remote_unmute(self.session_id)
+
+    async def async_browse_media(
+        self, media_content_type: str | None = None, media_content_id: str | None = None
+    ) -> BrowseMedia:
+        """Return a BrowseMedia instance.
+
+        The BrowseMedia instance will be used by the "media_player/browse_media" websocket command.
+
+        """
+        if media_content_id is None or media_content_id == "media-source://jellyfin":
+            return await build_root_response(
+                self.hass, self.coordinator.api_client, self.coordinator.user_id
+            )
+
+        return await build_item_response(
+            self.hass,
+            self.coordinator.api_client,
+            self.coordinator.user_id,
+            media_content_type,
+            media_content_id,
+        )
diff --git a/homeassistant/components/jellyfin/models.py b/homeassistant/components/jellyfin/models.py
index 913e40e14d5..b6365042127 100644
--- a/homeassistant/components/jellyfin/models.py
+++ b/homeassistant/components/jellyfin/models.py
@@ -12,5 +12,6 @@ from .coordinator import JellyfinDataUpdateCoordinator
 class JellyfinData:
     """Data for the Jellyfin integration."""
 
+    client_device_id: str
     jellyfin_client: JellyfinClient
     coordinators: dict[str, JellyfinDataUpdateCoordinator]
diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py
index 65b66e5b663..423e4ad3950 100644
--- a/tests/components/jellyfin/conftest.py
+++ b/tests/components/jellyfin/conftest.py
@@ -73,6 +73,12 @@ def mock_api() -> MagicMock:
     jf_api.get_user_settings.return_value = load_json_fixture("get-user-settings.json")
     jf_api.sessions.return_value = load_json_fixture("sessions.json")
 
+    jf_api.artwork.side_effect = api_artwork_side_effect
+    jf_api.user_items.side_effect = api_user_items_side_effect
+    jf_api.get_item.side_effect = api_get_item_side_effect
+    jf_api.get_media_folders.return_value = load_json_fixture("get-media-folders.json")
+    jf_api.user_items.side_effect = api_user_items_side_effect
+
     return jf_api
 
 
@@ -121,3 +127,27 @@ async def init_integration(
     await hass.async_block_till_done()
 
     return mock_config_entry
+
+
+def api_artwork_side_effect(*args, **kwargs):
+    """Handle variable responses for artwork method."""
+    item_id = args[0]
+    art = args[1]
+    ext = "jpg"
+
+    return f"http://localhost/Items/{item_id}/Images/{art}.{ext}"
+
+
+def api_get_item_side_effect(*args):
+    """Handle variable responses for get_item method."""
+    return load_json_fixture("get-item-collection.json")
+
+
+def api_user_items_side_effect(*args, **kwargs):
+    """Handle variable responses for items  method."""
+    params = kwargs.get("params", {}) if kwargs else {}
+
+    if "parentId" in params:
+        return load_json_fixture("user-items-parent-id.json")
+
+    return load_json_fixture("user-items.json")
diff --git a/tests/components/jellyfin/fixtures/get-item-collection.json b/tests/components/jellyfin/fixtures/get-item-collection.json
new file mode 100644
index 00000000000..90ad63a39e4
--- /dev/null
+++ b/tests/components/jellyfin/fixtures/get-item-collection.json
@@ -0,0 +1,504 @@
+{
+  "Name": "FOLDER",
+  "OriginalTitle": "string",
+  "ServerId": "SERVER-UUID",
+  "Id": "FOLDER-UUID",
+  "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": "CollectionFolder",
+  "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": {}
+}
diff --git a/tests/components/jellyfin/fixtures/get-media-folders.json b/tests/components/jellyfin/fixtures/get-media-folders.json
new file mode 100644
index 00000000000..ff87751a9da
--- /dev/null
+++ b/tests/components/jellyfin/fixtures/get-media-folders.json
@@ -0,0 +1,510 @@
+{
+  "Items": [
+    {
+      "Name": "COLLECTION FOLDER",
+      "OriginalTitle": "string",
+      "ServerId": "SERVER-UUID",
+      "Id": "COLLECTION-FOLDER-UUID",
+      "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": "CollectionFolder",
+      "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": "SERIES-UUID",
+      "SeasonId": "SEASON-UUID",
+      "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": "tvshows",
+      "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": {}
+    }
+  ],
+  "TotalRecordCount": 0,
+  "StartIndex": 0
+}
diff --git a/tests/components/jellyfin/fixtures/user-items-parent-id.json b/tests/components/jellyfin/fixtures/user-items-parent-id.json
new file mode 100644
index 00000000000..2e06c30894c
--- /dev/null
+++ b/tests/components/jellyfin/fixtures/user-items-parent-id.json
@@ -0,0 +1,510 @@
+{
+  "Items": [
+    {
+      "Name": "EPISODE",
+      "OriginalTitle": "string",
+      "ServerId": "SERVER-UUID",
+      "Id": "EPISODE-UUID",
+      "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": false,
+      "ParentId": "FOLDER-UUID",
+      "Type": "Episode",
+      "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": {}
+    }
+  ],
+  "TotalRecordCount": 0,
+  "StartIndex": 0
+}
diff --git a/tests/components/jellyfin/fixtures/user-items.json b/tests/components/jellyfin/fixtures/user-items.json
new file mode 100644
index 00000000000..7461626de18
--- /dev/null
+++ b/tests/components/jellyfin/fixtures/user-items.json
@@ -0,0 +1,510 @@
+{
+  "Items": [
+    {
+      "Name": "FOLDER",
+      "OriginalTitle": "string",
+      "ServerId": "SERVER-UUID",
+      "Id": "FOLDER-UUID",
+      "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": {}
+    }
+  ],
+  "TotalRecordCount": 0,
+  "StartIndex": 0
+}
diff --git a/tests/components/jellyfin/test_media_player.py b/tests/components/jellyfin/test_media_player.py
new file mode 100644
index 00000000000..40ddfefce39
--- /dev/null
+++ b/tests/components/jellyfin/test_media_player.py
@@ -0,0 +1,356 @@
+"""Tests for the Jellyfin media_player platform."""
+from datetime import timedelta
+from unittest.mock import MagicMock
+
+from aiohttp import ClientSession
+
+from homeassistant.components.jellyfin.const import DOMAIN
+from homeassistant.components.media_player import (
+    ATTR_MEDIA_ALBUM_ARTIST,
+    ATTR_MEDIA_ALBUM_NAME,
+    ATTR_MEDIA_ARTIST,
+    ATTR_MEDIA_CONTENT_ID,
+    ATTR_MEDIA_CONTENT_TYPE,
+    ATTR_MEDIA_DURATION,
+    ATTR_MEDIA_EPISODE,
+    ATTR_MEDIA_POSITION,
+    ATTR_MEDIA_POSITION_UPDATED_AT,
+    ATTR_MEDIA_SEASON,
+    ATTR_MEDIA_SERIES_TITLE,
+    ATTR_MEDIA_TRACK,
+    ATTR_MEDIA_VOLUME_LEVEL,
+    ATTR_MEDIA_VOLUME_MUTED,
+    DOMAIN as MP_DOMAIN,
+    MediaClass,
+    MediaPlayerState,
+    MediaType,
+)
+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.util.dt import utcnow
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+async def test_media_player(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_jellyfin: MagicMock,
+    mock_api: MagicMock,
+) -> None:
+    """Test the Jellyfin media player."""
+    device_registry = dr.async_get(hass)
+    entity_registry = er.async_get(hass)
+
+    state = hass.states.get("media_player.jellyfin_device")
+
+    assert state
+    assert state.state == MediaPlayerState.PAUSED
+    assert state.attributes.get(ATTR_DEVICE_CLASS) is None
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "JELLYFIN-DEVICE"
+    assert state.attributes.get(ATTR_ICON) is None
+    assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0.0
+    assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True
+    assert state.attributes.get(ATTR_MEDIA_DURATION) == 60
+    assert state.attributes.get(ATTR_MEDIA_POSITION) == 10
+    assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT)
+    assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "EPISODE-UUID"
+    assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.TVSHOW
+    assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == "SERIES"
+    assert state.attributes.get(ATTR_MEDIA_SEASON) == 1
+    assert state.attributes.get(ATTR_MEDIA_EPISODE) == 3
+
+    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-SESSION-UUID"
+
+    assert len(mock_api.sessions.mock_calls) == 1
+    async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
+    await hass.async_block_till_done()
+    assert len(mock_api.sessions.mock_calls) == 2
+
+    mock_api.sessions.return_value = []
+    async_fire_time_changed(hass, utcnow() + timedelta(seconds=20))
+    await hass.async_block_till_done()
+    assert len(mock_api.sessions.mock_calls) == 3
+
+    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 None
+    assert device.hw_version is None
+    assert device.identifiers == {(DOMAIN, "DEVICE-UUID")}
+    assert device.manufacturer == "Jellyfin"
+    assert device.name == "JELLYFIN-DEVICE"
+    assert device.sw_version == "1.0.0"
+
+
+async def test_media_player_music(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_jellyfin: MagicMock,
+    mock_api: MagicMock,
+) -> None:
+    """Test the Jellyfin media player."""
+    entity_registry = er.async_get(hass)
+
+    state = hass.states.get("media_player.jellyfin_device_four")
+
+    assert state
+    assert state.state == MediaPlayerState.PLAYING
+    assert state.attributes.get(ATTR_DEVICE_CLASS) is None
+    assert state.attributes.get(ATTR_FRIENDLY_NAME) == "JELLYFIN DEVICE FOUR"
+    assert state.attributes.get(ATTR_ICON) is None
+    assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 1.0
+    assert state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is False
+    assert state.attributes.get(ATTR_MEDIA_DURATION) == 73
+    assert state.attributes.get(ATTR_MEDIA_POSITION) == 22
+    assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT)
+    assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "MUSIC-UUID"
+    assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MediaType.MUSIC
+    assert state.attributes.get(ATTR_MEDIA_ALBUM_NAME) == "ALBUM"
+    assert state.attributes.get(ATTR_MEDIA_ALBUM_ARTIST) == "Album Artist"
+    assert state.attributes.get(ATTR_MEDIA_ARTIST) == "Contributing Artist"
+    assert state.attributes.get(ATTR_MEDIA_TRACK) == 1
+    assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) is None
+    assert state.attributes.get(ATTR_MEDIA_SEASON) is None
+    assert state.attributes.get(ATTR_MEDIA_EPISODE) is None
+
+    entry = entity_registry.async_get(state.entity_id)
+    assert entry
+    assert entry.device_id is None
+    assert entry.entity_category is None
+    assert entry.unique_id == "SERVER-UUID-SESSION-UUID-FOUR"
+
+
+async def test_services(
+    hass: HomeAssistant,
+    init_integration: MockConfigEntry,
+    mock_jellyfin: MagicMock,
+    mock_api: MagicMock,
+) -> None:
+    """Test Jellyfin media player services."""
+    state = hass.states.get("media_player.jellyfin_device")
+    assert state
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "play_media",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+            "media_content_type": "",
+            "media_content_id": "ITEM-UUID",
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_play_media.mock_calls) == 1
+    assert mock_api.remote_play_media.mock_calls[0].args == (
+        "SESSION-UUID",
+        ["ITEM-UUID"],
+    )
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "media_pause",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_pause.mock_calls) == 1
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "media_play",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_unpause.mock_calls) == 1
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "media_play_pause",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_playpause.mock_calls) == 1
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "media_seek",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+            "seek_position": 10,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_seek.mock_calls) == 1
+    assert mock_api.remote_seek.mock_calls[0].args == (
+        "SESSION-UUID",
+        100000000,
+    )
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "media_stop",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_stop.mock_calls) == 1
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "volume_set",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+            "volume_level": 0.5,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_set_volume.mock_calls) == 1
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "volume_mute",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+            "is_volume_muted": True,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_mute.mock_calls) == 1
+
+    await hass.services.async_call(
+        MP_DOMAIN,
+        "volume_mute",
+        {
+            ATTR_ENTITY_ID: state.entity_id,
+            "is_volume_muted": False,
+        },
+        blocking=True,
+    )
+    assert len(mock_api.remote_unmute.mock_calls) == 1
+
+
+async def test_browse_media(
+    hass: HomeAssistant,
+    hass_ws_client: ClientSession,
+    init_integration: MockConfigEntry,
+    mock_jellyfin: MagicMock,
+    mock_api: MagicMock,
+) -> None:
+    """Test Jellyfin browse media."""
+    client = await hass_ws_client()
+
+    # browse root folder
+    await client.send_json(
+        {
+            "id": 1,
+            "type": "media_player/browse_media",
+            "entity_id": "media_player.jellyfin_device",
+        }
+    )
+    response = await client.receive_json()
+    assert response["success"]
+    expected_child_item = {
+        "title": "COLLECTION FOLDER",
+        "media_class": MediaClass.DIRECTORY.value,
+        "media_content_type": "collection",
+        "media_content_id": "COLLECTION-FOLDER-UUID",
+        "can_play": False,
+        "can_expand": True,
+        "thumbnail": "http://localhost/Items/c22fd826-17fc-44f4-9b04-1eb3e8fb9173/Images/Backdrop.jpg",
+        "children_media_class": None,
+    }
+
+    assert response["result"]["media_content_id"] == ""
+    assert response["result"]["media_content_type"] == "root"
+    assert response["result"]["title"] == "Jellyfin"
+    assert response["result"]["children"][0] == expected_child_item
+
+    # browse collection folder
+    await client.send_json(
+        {
+            "id": 2,
+            "type": "media_player/browse_media",
+            "entity_id": "media_player.jellyfin_device",
+            "media_content_type": "collection",
+            "media_content_id": "COLLECTION-FOLDER-UUID",
+        }
+    )
+
+    response = await client.receive_json()
+    expected_child_item = {
+        "title": "EPISODE",
+        "media_class": MediaClass.EPISODE.value,
+        "media_content_type": MediaType.EPISODE.value,
+        "media_content_id": "EPISODE-UUID",
+        "can_play": True,
+        "can_expand": False,
+        "thumbnail": "http://localhost/Items/c22fd826-17fc-44f4-9b04-1eb3e8fb9173/Images/Backdrop.jpg",
+        "children_media_class": None,
+    }
+
+    assert response["success"]
+    assert response["result"]["media_content_id"] == "COLLECTION-FOLDER-UUID"
+    assert response["result"]["title"] == "FOLDER"
+    assert response["result"]["children"][0] == expected_child_item
+
+    # browse for collection without children
+    mock_api.user_items.side_effect = None
+    mock_api.user_items.return_value = {}
+
+    await client.send_json(
+        {
+            "id": 3,
+            "type": "media_player/browse_media",
+            "entity_id": "media_player.jellyfin_device",
+            "media_content_type": "collection",
+            "media_content_id": "COLLECTION-FOLDER-UUID",
+        }
+    )
+
+    response = await client.receive_json()
+    assert response["success"] is False
+    assert response["error"]
+    assert (
+        response["error"]["message"]
+        == "Media not found: collection / COLLECTION-FOLDER-UUID"
+    )
+
+    # browse for non-existent item
+    mock_api.get_item.side_effect = None
+    mock_api.get_item.return_value = {}
+
+    await client.send_json(
+        {
+            "id": 4,
+            "type": "media_player/browse_media",
+            "entity_id": "media_player.jellyfin_device",
+            "media_content_type": "collection",
+            "media_content_id": "COLLECTION-UUID-404",
+        }
+    )
+
+    response = await client.receive_json()
+    assert response["success"] is False
+    assert response["error"]
+    assert (
+        response["error"]["message"]
+        == "Media not found: collection / COLLECTION-UUID-404"
+    )
-- 
GitLab


From 559e28143147b85a606ef76e409b14a04c6bef3f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Mon, 24 Oct 2022 23:11:40 -0500
Subject: [PATCH 771/985] Add oralb integration (#80918)

* mapping

* update

* working

* tests

* fixes

* temp add binary sensor platform to make sure it works

* fixes

* 100% cover

* adjust

* disable chatty sensors by default

* remove binary sensor_platform for next PR

* time is much nicer than counter
---
 CODEOWNERS                                    |   2 +
 homeassistant/components/oralb/__init__.py    |  49 +++++
 homeassistant/components/oralb/config_flow.py |  94 +++++++++
 homeassistant/components/oralb/const.py       |   3 +
 homeassistant/components/oralb/device.py      |  31 +++
 homeassistant/components/oralb/manifest.json  |  15 ++
 homeassistant/components/oralb/sensor.py      | 119 +++++++++++
 homeassistant/components/oralb/strings.json   |  22 ++
 .../components/oralb/translations/en.json     |  22 ++
 homeassistant/generated/bluetooth.py          |   4 +
 homeassistant/generated/config_flows.py       |   1 +
 homeassistant/generated/integrations.json     |   6 +
 requirements_all.txt                          |   3 +
 requirements_test_all.txt                     |   3 +
 tests/components/oralb/__init__.py            |  24 +++
 tests/components/oralb/conftest.py            |   8 +
 tests/components/oralb/test_config_flow.py    | 192 ++++++++++++++++++
 tests/components/oralb/test_sensor.py         |  40 ++++
 18 files changed, 638 insertions(+)
 create mode 100644 homeassistant/components/oralb/__init__.py
 create mode 100644 homeassistant/components/oralb/config_flow.py
 create mode 100644 homeassistant/components/oralb/const.py
 create mode 100644 homeassistant/components/oralb/device.py
 create mode 100644 homeassistant/components/oralb/manifest.json
 create mode 100644 homeassistant/components/oralb/sensor.py
 create mode 100644 homeassistant/components/oralb/strings.json
 create mode 100644 homeassistant/components/oralb/translations/en.json
 create mode 100644 tests/components/oralb/__init__.py
 create mode 100644 tests/components/oralb/conftest.py
 create mode 100644 tests/components/oralb/test_config_flow.py
 create mode 100644 tests/components/oralb/test_sensor.py

diff --git a/CODEOWNERS b/CODEOWNERS
index 2d9bcf41db8..f4a311db29a 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -823,6 +823,8 @@ build.json @home-assistant/supervisor
 /tests/components/openweathermap/ @fabaff @freekode @nzapponi
 /homeassistant/components/opnsense/ @mtreinish
 /tests/components/opnsense/ @mtreinish
+/homeassistant/components/oralb/ @bdraco
+/tests/components/oralb/ @bdraco
 /homeassistant/components/oru/ @bvlaicu
 /homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne
 /tests/components/overkiz/ @imicknl @vlebourl @tetienne
diff --git a/homeassistant/components/oralb/__init__.py b/homeassistant/components/oralb/__init__.py
new file mode 100644
index 00000000000..61547b5e432
--- /dev/null
+++ b/homeassistant/components/oralb/__init__.py
@@ -0,0 +1,49 @@
+"""The OralB integration."""
+from __future__ import annotations
+
+import logging
+
+from oralb_ble import OralBBluetoothDeviceData
+
+from homeassistant.components.bluetooth import BluetoothScanningMode
+from homeassistant.components.bluetooth.passive_update_processor import (
+    PassiveBluetoothProcessorCoordinator,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+
+from .const import DOMAIN
+
+PLATFORMS: list[Platform] = [Platform.SENSOR]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up OralB BLE device from a config entry."""
+    address = entry.unique_id
+    assert address is not None
+    data = OralBBluetoothDeviceData()
+    coordinator = hass.data.setdefault(DOMAIN, {})[
+        entry.entry_id
+    ] = PassiveBluetoothProcessorCoordinator(
+        hass,
+        _LOGGER,
+        address=address,
+        mode=BluetoothScanningMode.PASSIVE,
+        update_method=data.update,
+    )
+    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+    entry.async_on_unload(
+        coordinator.async_start()
+    )  # only start after all platforms have had a chance to subscribe
+    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/oralb/config_flow.py b/homeassistant/components/oralb/config_flow.py
new file mode 100644
index 00000000000..28e16c7f8a7
--- /dev/null
+++ b/homeassistant/components/oralb/config_flow.py
@@ -0,0 +1,94 @@
+"""Config flow for oralb ble integration."""
+from __future__ import annotations
+
+from typing import Any
+
+from oralb_ble import OralBBluetoothDeviceData as DeviceData
+import voluptuous as vol
+
+from homeassistant.components.bluetooth import (
+    BluetoothServiceInfoBleak,
+    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
+
+
+class OralBConfigFlow(ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for oralb."""
+
+    VERSION = 1
+
+    def __init__(self) -> None:
+        """Initialize the config flow."""
+        self._discovery_info: BluetoothServiceInfoBleak | None = None
+        self._discovered_device: DeviceData | None = None
+        self._discovered_devices: dict[str, str] = {}
+
+    async def async_step_bluetooth(
+        self, discovery_info: BluetoothServiceInfoBleak
+    ) -> FlowResult:
+        """Handle the bluetooth discovery step."""
+        await self.async_set_unique_id(discovery_info.address)
+        self._abort_if_unique_id_configured()
+        device = DeviceData()
+        if not device.supported(discovery_info):
+            return self.async_abort(reason="not_supported")
+        self._discovery_info = discovery_info
+        self._discovered_device = 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._discovered_device is not None
+        device = self._discovered_device
+        assert self._discovery_info is not None
+        discovery_info = self._discovery_info
+        title = device.title or device.get_device_name() or discovery_info.name
+        if user_input is not None:
+            return self.async_create_entry(title=title, data={})
+
+        self._set_confirm_only()
+        placeholders = {"name": title}
+        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:
+            address = user_input[CONF_ADDRESS]
+            await self.async_set_unique_id(address, raise_on_progress=False)
+            self._abort_if_unique_id_configured()
+            return self.async_create_entry(
+                title=self._discovered_devices[address], data={}
+            )
+
+        current_addresses = self._async_current_ids()
+        for discovery_info in async_discovered_service_info(self.hass, False):
+            address = discovery_info.address
+            if address in current_addresses or address in self._discovered_devices:
+                continue
+            device = DeviceData()
+            if device.supported(discovery_info):
+                self._discovered_devices[address] = (
+                    device.title or device.get_device_name() or discovery_info.name
+                )
+
+        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_ADDRESS): vol.In(self._discovered_devices)}
+            ),
+        )
diff --git a/homeassistant/components/oralb/const.py b/homeassistant/components/oralb/const.py
new file mode 100644
index 00000000000..54ed2c49fba
--- /dev/null
+++ b/homeassistant/components/oralb/const.py
@@ -0,0 +1,3 @@
+"""Constants for the OralB integration."""
+
+DOMAIN = "oralb"
diff --git a/homeassistant/components/oralb/device.py b/homeassistant/components/oralb/device.py
new file mode 100644
index 00000000000..0b9da5c3779
--- /dev/null
+++ b/homeassistant/components/oralb/device.py
@@ -0,0 +1,31 @@
+"""Support for OralB devices."""
+from __future__ import annotations
+
+from oralb_ble import DeviceKey, SensorDeviceInfo
+
+from homeassistant.components.bluetooth.passive_update_processor import (
+    PassiveBluetoothEntityKey,
+)
+from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME
+from homeassistant.helpers.entity import DeviceInfo
+
+
+def device_key_to_bluetooth_entity_key(
+    device_key: DeviceKey,
+) -> PassiveBluetoothEntityKey:
+    """Convert a device key to an entity key."""
+    return PassiveBluetoothEntityKey(device_key.key, device_key.device_id)
+
+
+def sensor_device_info_to_hass(
+    sensor_device_info: SensorDeviceInfo,
+) -> DeviceInfo:
+    """Convert a oralb device info to a sensor device info."""
+    hass_device_info = DeviceInfo({})
+    if sensor_device_info.name is not None:
+        hass_device_info[ATTR_NAME] = sensor_device_info.name
+    if sensor_device_info.manufacturer is not None:
+        hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer
+    if sensor_device_info.model is not None:
+        hass_device_info[ATTR_MODEL] = sensor_device_info.model
+    return hass_device_info
diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json
new file mode 100644
index 00000000000..b3dfedde532
--- /dev/null
+++ b/homeassistant/components/oralb/manifest.json
@@ -0,0 +1,15 @@
+{
+  "domain": "oralb",
+  "name": "Oral-B",
+  "config_flow": true,
+  "documentation": "https://www.home-assistant.io/integrations/oralb",
+  "bluetooth": [
+    {
+      "manufacturer_id": 220
+    }
+  ],
+  "requirements": ["oralb-ble==0.5.0"],
+  "dependencies": ["bluetooth"],
+  "codeowners": ["@bdraco"],
+  "iot_class": "local_push"
+}
diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py
new file mode 100644
index 00000000000..6fbc19b092a
--- /dev/null
+++ b/homeassistant/components/oralb/sensor.py
@@ -0,0 +1,119 @@
+"""Support for OralB sensors."""
+from __future__ import annotations
+
+from typing import Optional, Union
+
+from oralb_ble import OralBSensor, SensorUpdate
+
+from homeassistant import config_entries
+from homeassistant.components.bluetooth.passive_update_processor import (
+    PassiveBluetoothDataProcessor,
+    PassiveBluetoothDataUpdate,
+    PassiveBluetoothProcessorCoordinator,
+    PassiveBluetoothProcessorEntity,
+)
+from homeassistant.components.sensor import (
+    SensorDeviceClass,
+    SensorEntity,
+    SensorEntityDescription,
+    SensorStateClass,
+)
+from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TIME_SECONDS
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import EntityCategory
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import DOMAIN
+from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass
+
+SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
+    OralBSensor.TIME: SensorEntityDescription(
+        key=OralBSensor.TIME,
+        device_class=SensorDeviceClass.DURATION,
+        state_class=SensorStateClass.TOTAL_INCREASING,
+        native_unit_of_measurement=TIME_SECONDS,
+    ),
+    OralBSensor.SECTOR: SensorEntityDescription(
+        key=OralBSensor.SECTOR,
+    ),
+    OralBSensor.NUMBER_OF_SECTORS: SensorEntityDescription(
+        key=OralBSensor.NUMBER_OF_SECTORS,
+    ),
+    OralBSensor.SECTOR_TIMER: SensorEntityDescription(
+        key=OralBSensor.SECTOR_TIMER,
+        entity_registry_enabled_default=False,
+    ),
+    OralBSensor.TOOTHBRUSH_STATE: SensorEntityDescription(
+        key=OralBSensor.TOOTHBRUSH_STATE
+    ),
+    OralBSensor.PRESSURE: SensorEntityDescription(key=OralBSensor.PRESSURE),
+    OralBSensor.MODE: SensorEntityDescription(
+        key=OralBSensor.MODE,
+    ),
+    OralBSensor.SIGNAL_STRENGTH: SensorEntityDescription(
+        key=OralBSensor.SIGNAL_STRENGTH,
+        device_class=SensorDeviceClass.SIGNAL_STRENGTH,
+        native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+        state_class=SensorStateClass.MEASUREMENT,
+        entity_category=EntityCategory.DIAGNOSTIC,
+        entity_registry_enabled_default=False,
+    ),
+}
+
+
+def sensor_update_to_bluetooth_data_update(
+    sensor_update: SensorUpdate,
+) -> PassiveBluetoothDataUpdate:
+    """Convert a sensor update to a bluetooth data update."""
+    return PassiveBluetoothDataUpdate(
+        devices={
+            device_id: sensor_device_info_to_hass(device_info)
+            for device_id, device_info in sensor_update.devices.items()
+        },
+        entity_descriptions={
+            device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[
+                device_key.key
+            ]
+            for device_key in sensor_update.entity_descriptions
+        },
+        entity_data={
+            device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value
+            for device_key, sensor_values in sensor_update.entity_values.items()
+        },
+        entity_names={
+            device_key_to_bluetooth_entity_key(device_key): sensor_values.name
+            for device_key, sensor_values in sensor_update.entity_values.items()
+        },
+    )
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    entry: config_entries.ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up the OralB BLE sensors."""
+    coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
+        entry.entry_id
+    ]
+    processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
+    entry.async_on_unload(
+        processor.async_add_entities_listener(
+            OralBBluetoothSensorEntity, async_add_entities
+        )
+    )
+    entry.async_on_unload(coordinator.async_register_processor(processor))
+
+
+class OralBBluetoothSensorEntity(
+    PassiveBluetoothProcessorEntity[
+        PassiveBluetoothDataProcessor[Optional[Union[str, int]]]
+    ],
+    SensorEntity,
+):
+    """Representation of a OralB sensor."""
+
+    @property
+    def native_value(self) -> str | int | None:
+        """Return the native value."""
+        return self.processor.entity_data.get(self.entity_key)
diff --git a/homeassistant/components/oralb/strings.json b/homeassistant/components/oralb/strings.json
new file mode 100644
index 00000000000..a045d84771e
--- /dev/null
+++ b/homeassistant/components/oralb/strings.json
@@ -0,0 +1,22 @@
+{
+  "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": {
+      "not_supported": "Device not supported",
+      "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/oralb/translations/en.json b/homeassistant/components/oralb/translations/en.json
new file mode 100644
index 00000000000..ebd9760c161
--- /dev/null
+++ b/homeassistant/components/oralb/translations/en.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Device not supported"
+        },
+        "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 afb35e40e83..c4dd22cef17 100644
--- a/homeassistant/generated/bluetooth.py
+++ b/homeassistant/generated/bluetooth.py
@@ -227,6 +227,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
         "local_name": "Moat_S*",
         "connectable": False,
     },
+    {
+        "domain": "oralb",
+        "manufacturer_id": 220,
+    },
     {
         "domain": "qingping",
         "local_name": "Qingping*",
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index b56e712ae27..6cac800d7c8 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -280,6 +280,7 @@ FLOWS = {
         "opentherm_gw",
         "openuv",
         "openweathermap",
+        "oralb",
         "overkiz",
         "ovo_energy",
         "owntracks",
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 033465408f2..16bceb1fb2b 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -3802,6 +3802,12 @@
       "config_flow": false,
       "iot_class": "local_polling"
     },
+    "oralb": {
+      "name": "Oral-B",
+      "integration_type": "hub",
+      "config_flow": true,
+      "iot_class": "local_push"
+    },
     "oru": {
       "name": "Orange and Rockland Utility (ORU)",
       "integration_type": "hub",
diff --git a/requirements_all.txt b/requirements_all.txt
index 7770c84193e..97a1f5abca1 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1237,6 +1237,9 @@ openwrt-luci-rpc==1.1.11
 # homeassistant.components.ubus
 openwrt-ubus-rpc==0.0.2
 
+# homeassistant.components.oralb
+oralb-ble==0.5.0
+
 # homeassistant.components.oru
 oru==0.1.11
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 1679aee7e6d..f351402b4f0 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -882,6 +882,9 @@ open-meteo==0.2.1
 # homeassistant.components.openerz
 openerz-api==0.1.0
 
+# homeassistant.components.oralb
+oralb-ble==0.5.0
+
 # homeassistant.components.ovo_energy
 ovoenergy==1.2.0
 
diff --git a/tests/components/oralb/__init__.py b/tests/components/oralb/__init__.py
new file mode 100644
index 00000000000..567b9d7328e
--- /dev/null
+++ b/tests/components/oralb/__init__.py
@@ -0,0 +1,24 @@
+"""Tests for the OralB integration."""
+
+
+from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
+
+NOT_ORALB_SERVICE_INFO = BluetoothServiceInfo(
+    name="Not it",
+    address="61DE521B-F0BF-9F44-64D4-75BBE1738105",
+    rssi=-63,
+    manufacturer_data={3234: b"\x00\x01"},
+    service_data={},
+    service_uuids=[],
+    source="local",
+)
+
+ORALB_SERVICE_INFO = BluetoothServiceInfo(
+    name="78:DB:2F:C2:48:BE",
+    address="78:DB:2F:C2:48:BE",
+    rssi=-63,
+    manufacturer_data={220: b"\x02\x01\x08\x03\x00\x00\x00\x01\x01\x00\x04"},
+    service_uuids=[],
+    service_data={},
+    source="local",
+)
diff --git a/tests/components/oralb/conftest.py b/tests/components/oralb/conftest.py
new file mode 100644
index 00000000000..454cb7af726
--- /dev/null
+++ b/tests/components/oralb/conftest.py
@@ -0,0 +1,8 @@
+"""OralB session fixtures."""
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def mock_bluetooth(enable_bluetooth):
+    """Auto mock bluetooth."""
diff --git a/tests/components/oralb/test_config_flow.py b/tests/components/oralb/test_config_flow.py
new file mode 100644
index 00000000000..e4af11faddb
--- /dev/null
+++ b/tests/components/oralb/test_config_flow.py
@@ -0,0 +1,192 @@
+"""Test the OralB config flow."""
+
+from unittest.mock import patch
+
+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 tests.common import MockConfigEntry
+
+
+async def test_async_step_bluetooth_valid_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_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"] == "Smart Series 7000 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(
+        DOMAIN,
+        context={"source": config_entries.SOURCE_BLUETOOTH},
+        data=NOT_ORALB_SERVICE_INFO,
+    )
+    assert result["type"] == FlowResultType.ABORT
+    assert result["reason"] == "not_supported"
+
+
+async def test_async_step_user_no_devices_found(hass):
+    """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):
+    """Test setup from service info cache with devices found."""
+    with patch(
+        "homeassistant.components.oralb.config_flow.async_discovered_service_info",
+        return_value=[ORALB_SERVICE_INFO],
+    ):
+        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"
+    with patch("homeassistant.components.oralb.async_setup_entry", return_value=True):
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            user_input={"address": "78:DB:2F:C2:48:BE"},
+        )
+    assert result2["type"] == FlowResultType.CREATE_ENTRY
+    assert result2["title"] == "Smart Series 7000 48BE"
+    assert result2["data"] == {}
+    assert result2["result"].unique_id == "78:DB:2F:C2:48:BE"
+
+
+async def test_async_step_user_device_added_between_steps(hass):
+    """Test the device gets added via another flow between steps."""
+    with patch(
+        "homeassistant.components.oralb.config_flow.async_discovered_service_info",
+        return_value=[ORALB_SERVICE_INFO],
+    ):
+        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="78:DB:2F:C2:48:BE",
+    )
+    entry.add_to_hass(hass)
+
+    with patch("homeassistant.components.oralb.async_setup_entry", return_value=True):
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            user_input={"address": "78:DB:2F:C2:48:BE"},
+        )
+    assert result2["type"] == FlowResultType.ABORT
+    assert result2["reason"] == "already_configured"
+
+
+async def test_async_step_user_with_found_devices_already_setup(hass):
+    """Test setup from service info cache with devices found."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        unique_id="78:DB:2F:C2:48:BE",
+    )
+    entry.add_to_hass(hass)
+
+    with patch(
+        "homeassistant.components.oralb.config_flow.async_discovered_service_info",
+        return_value=[ORALB_SERVICE_INFO],
+    ):
+        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):
+    """Test we can't start a flow if there is already a config entry."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        unique_id="78:DB:2F:C2:48:BE",
+    )
+    entry.add_to_hass(hass)
+
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": config_entries.SOURCE_BLUETOOTH},
+        data=ORALB_SERVICE_INFO,
+    )
+    assert result["type"] == FlowResultType.ABORT
+    assert result["reason"] == "already_configured"
+
+
+async def test_async_step_bluetooth_already_in_progress(hass):
+    """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=ORALB_SERVICE_INFO,
+    )
+    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=ORALB_SERVICE_INFO,
+    )
+    assert result["type"] == FlowResultType.ABORT
+    assert result["reason"] == "already_in_progress"
+
+
+async def test_async_step_user_takes_precedence_over_discovery(hass):
+    """Test manual setup takes precedence over discovery."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": config_entries.SOURCE_BLUETOOTH},
+        data=ORALB_SERVICE_INFO,
+    )
+    assert result["type"] == FlowResultType.FORM
+    assert result["step_id"] == "bluetooth_confirm"
+
+    with patch(
+        "homeassistant.components.oralb.config_flow.async_discovered_service_info",
+        return_value=[ORALB_SERVICE_INFO],
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_USER},
+        )
+        assert result["type"] == FlowResultType.FORM
+
+    with patch("homeassistant.components.oralb.async_setup_entry", return_value=True):
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            user_input={"address": "78:DB:2F:C2:48:BE"},
+        )
+    assert result2["type"] == FlowResultType.CREATE_ENTRY
+    assert result2["title"] == "Smart Series 7000 48BE"
+    assert result2["data"] == {}
+    assert result2["result"].unique_id == "78:DB:2F:C2:48:BE"
+
+    # Verify the original one was aborted
+    assert not hass.config_entries.flow.async_progress(DOMAIN)
diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py
new file mode 100644
index 00000000000..4e37005f65a
--- /dev/null
+++ b/tests/components/oralb/test_sensor.py
@@ -0,0 +1,40 @@
+"""Test the OralB sensors."""
+
+
+from homeassistant.components.oralb.const import DOMAIN
+from homeassistant.const import ATTR_FRIENDLY_NAME
+
+from . import ORALB_SERVICE_INFO
+
+from tests.common import MockConfigEntry
+from tests.components.bluetooth import inject_bluetooth_service_info
+
+
+async def test_sensors(hass, entity_registry_enabled_by_default):
+    """Test setting up creates the sensors."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        unique_id=ORALB_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_SERVICE_INFO)
+    await hass.async_block_till_done()
+    assert len(hass.states.async_all("sensor")) == 8
+
+    toothbrush_sensor = hass.states.get(
+        "sensor.smart_series_7000_48be_toothbrush_state"
+    )
+    toothbrush_sensor_attrs = toothbrush_sensor.attributes
+    assert toothbrush_sensor.state == "running"
+    assert (
+        toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME]
+        == "Smart Series 7000 48BE Toothbrush State"
+    )
+
+    assert await hass.config_entries.async_unload(entry.entry_id)
+    await hass.async_block_till_done()
-- 
GitLab


From 644b00ca2d7d067cf6f9eedc699dc3822d982a2c Mon Sep 17 00:00:00 2001
From: definitio <37266727+definitio@users.noreply.github.com>
Date: Tue, 25 Oct 2022 09:44:54 +0300
Subject: [PATCH 772/985] Add apcupsd laststest sensor (#80773)

---
 homeassistant/components/apcupsd/sensor.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py
index 5f76e6c1afa..b55f672264d 100644
--- a/homeassistant/components/apcupsd/sensor.py
+++ b/homeassistant/components/apcupsd/sensor.py
@@ -172,6 +172,11 @@ SENSORS: dict[str, SensorEntityDescription] = {
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
     ),
+    "laststest": SensorEntityDescription(
+        key="laststest",
+        name="UPS Last Self Test",
+        icon="mdi:calendar-clock",
+    ),
     "lastxfer": SensorEntityDescription(
         key="lastxfer",
         name="UPS Last Transfer",
@@ -331,8 +336,8 @@ SENSORS: dict[str, SensorEntityDescription] = {
     ),
     "selftest": SensorEntityDescription(
         key="selftest",
-        name="UPS Last Self Test",
-        icon="mdi:calendar-clock",
+        name="UPS Self Test result",
+        icon="mdi:information-outline",
     ),
     "sense": SensorEntityDescription(
         key="sense",
-- 
GitLab


From 4f5c9be84f2bbe3cf2f8113b9d44053c2a9c37d4 Mon Sep 17 00:00:00 2001
From: Quentame <polletquentin74@me.com>
Date: Tue, 25 Oct 2022 09:00:56 +0200
Subject: [PATCH 773/985] Use EntityDescription in Freebox switch (#80858)

* Freebox switch: use EntityDescription

* unique_id base on key later with migration step

* Keep the same name

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Update homeassistant/components/freebox/switch.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Removing specific FreeboxSwitchEntityDescription as there is one sensor, for now

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
---
 homeassistant/components/freebox/switch.py | 61 ++++++++++------------
 1 file changed, 27 insertions(+), 34 deletions(-)

diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py
index ab5a1160b71..9bef539bfd8 100644
--- a/homeassistant/components/freebox/switch.py
+++ b/homeassistant/components/freebox/switch.py
@@ -6,10 +6,10 @@ from typing import Any
 
 from freebox_api.exceptions import InsufficientPermissionsError
 
-from homeassistant.components.switch import SwitchEntity
+from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import DOMAIN
@@ -18,49 +18,43 @@ from .router import FreeboxRouter
 _LOGGER = logging.getLogger(__name__)
 
 
+SWITCH_DESCRIPTIONS = [
+    SwitchEntityDescription(
+        key="wifi",
+        name="Freebox WiFi",
+        entity_category=EntityCategory.CONFIG,
+    )
+]
+
+
 async def async_setup_entry(
     hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
 ) -> None:
     """Set up the switch."""
     router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id]
-    async_add_entities([FreeboxWifiSwitch(router)], True)
+    entities = [
+        FreeboxSwitch(router, entity_description)
+        for entity_description in SWITCH_DESCRIPTIONS
+    ]
+    async_add_entities(entities, True)
 
 
-class FreeboxWifiSwitch(SwitchEntity):
-    """Representation of a freebox wifi switch."""
+class FreeboxSwitch(SwitchEntity):
+    """Representation of a freebox switch."""
 
-    def __init__(self, router: FreeboxRouter) -> None:
-        """Initialize the Wifi switch."""
-        self._name = "Freebox WiFi"
-        self._state: bool | None = None
+    def __init__(
+        self, router: FreeboxRouter, entity_description: SwitchEntityDescription
+    ) -> None:
+        """Initialize the switch."""
+        self.entity_description = entity_description
         self._router = router
-        self._unique_id = f"{self._router.mac} {self._name}"
-
-    @property
-    def unique_id(self) -> str:
-        """Return a unique ID."""
-        return self._unique_id
-
-    @property
-    def name(self) -> str:
-        """Return the name of the switch."""
-        return self._name
-
-    @property
-    def is_on(self) -> bool | None:
-        """Return true if device is on."""
-        return self._state
-
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return the device information."""
-        return self._router.device_info
+        self._attr_device_info = self._router.device_info
+        self._attr_unique_id = f"{self._router.mac} {self.entity_description.name}"
 
     async def _async_set_state(self, enabled: bool):
         """Turn the switch on or off."""
-        wifi_config = {"enabled": enabled}
         try:
-            await self._router.wifi.set_global_config(wifi_config)
+            await self._router.wifi.set_global_config({"enabled": enabled})
         except InsufficientPermissionsError:
             _LOGGER.warning(
                 "Home Assistant does not have permissions to modify the Freebox settings. Please refer to documentation"
@@ -77,5 +71,4 @@ class FreeboxWifiSwitch(SwitchEntity):
     async def async_update(self) -> None:
         """Get the state and update it."""
         datas = await self._router.wifi.get_global_config()
-        active = datas["enabled"]
-        self._state = bool(active)
+        self._attr_is_on = bool(datas["enabled"])
-- 
GitLab


From f85a2fb57a28234081d939b3928996bc9c60bc84 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
Date: Tue, 25 Oct 2022 09:13:36 +0200
Subject: [PATCH 774/985] Add integration_type to the velbus component (#80924)

---
 homeassistant/components/velbus/manifest.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json
index 875ff9d497d..86e67ca7767 100644
--- a/homeassistant/components/velbus/manifest.json
+++ b/homeassistant/components/velbus/manifest.json
@@ -6,6 +6,7 @@
   "config_flow": true,
   "codeowners": ["@Cereal2nd", "@brefra"],
   "dependencies": ["usb"],
+  "integration_type": "hub",
   "iot_class": "local_push",
   "usb": [
     {
-- 
GitLab


From 97d31d05f05070dcfe09dd0534ff240bb0103506 Mon Sep 17 00:00:00 2001
From: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Date: Tue, 25 Oct 2022 20:15:28 +1300
Subject: [PATCH 775/985] Use ESPHome manufacturer name from device if provided
 (#80928)

---
 homeassistant/components/esphome/__init__.py   | 2 ++
 homeassistant/components/esphome/manifest.json | 2 +-
 requirements_all.txt                           | 2 +-
 requirements_test_all.txt                      | 2 +-
 4 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index f2924558716..a5428f7d6c5 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -307,6 +307,8 @@ def _async_setup_device_registry(
         configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}"
 
     manufacturer = "espressif"
+    if device_info.manufacturer:
+        manufacturer = device_info.manufacturer
     model = device_info.model
     hw_version = None
     if device_info.project_name:
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index 75aad44894f..ab33ed8585a 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.1"],
+  "requirements": ["aioesphomeapi==11.2.0"],
   "zeroconf": ["_esphomelib._tcp.local."],
   "dhcp": [{ "registered_devices": true }],
   "codeowners": ["@OttoWinter", "@jesserockz"],
diff --git a/requirements_all.txt b/requirements_all.txt
index 97a1f5abca1..2f6caf82de0 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.1.1
+aioesphomeapi==11.2.0
 
 # homeassistant.components.flo
 aioflo==2021.11.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f351402b4f0..48a69f894a4 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.1.1
+aioesphomeapi==11.2.0
 
 # homeassistant.components.flo
 aioflo==2021.11.0
-- 
GitLab


From 3d3349240fb79ba387885f92c8640810956c71c2 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Tue, 25 Oct 2022 09:50:01 +0200
Subject: [PATCH 776/985] Don't trigger statistics issues when sensor units are
 equivalent (#80099)

---
 homeassistant/components/sensor/recorder.py |   2 +-
 tests/components/sensor/test_recorder.py    | 174 ++++++++++++++++++++
 2 files changed, 175 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py
index 6d5f08d6d56..5e853ec3c74 100644
--- a/homeassistant/components/sensor/recorder.py
+++ b/homeassistant/components/sensor/recorder.py
@@ -671,7 +671,7 @@ def validate_statistics(
             metadata_unit = metadata[1]["unit_of_measurement"]
             converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit)
             if not converter:
-                if state_unit != metadata_unit:
+                if not _equivalent_units({state_unit, metadata_unit}):
                     # The unit has changed, and it's not possible to convert
                     validation_result[entity_id].append(
                         statistics.ValidationIssue(
diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py
index e04d41f4c2a..05f8bd40597 100644
--- a/tests/components/sensor/test_recorder.py
+++ b/tests/components/sensor/test_recorder.py
@@ -4000,6 +4000,180 @@ async def test_validate_statistics_unit_change_no_conversion(
     await assert_validation_result(client, expected)
 
 
+@pytest.mark.parametrize(
+    "attributes, unit1, unit2",
+    [
+        (NONE_SENSOR_ATTRIBUTES, "m3", "m³"),
+        (NONE_SENSOR_ATTRIBUTES, "rpm", "RPM"),
+        (NONE_SENSOR_ATTRIBUTES, "RPM", "rpm"),
+    ],
+)
+async def test_validate_statistics_unit_change_equivalent_units(
+    recorder_mock, hass, hass_ws_client, attributes, unit1, unit2
+):
+    """Test validate_statistics.
+
+    This tests no validation issue is created when a sensor's unit changes to an
+    equivalent unit.
+    """
+    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, **{"unit_of_measurement": unit1}}
+    )
+    await assert_validation_result(client, {})
+
+    # Run statistics
+    await async_recorder_block_till_done(hass)
+    do_adhoc_statistics(hass, start=now)
+    await async_recorder_block_till_done(hass)
+    await assert_statistic_ids(
+        [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}]
+    )
+
+    # Units changed to an equivalent unit - empty response
+    hass.states.async_set(
+        "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit2}}
+    )
+    await assert_validation_result(client, {})
+
+    # Run statistics one hour later, metadata will be updated
+    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": unit2}]
+    )
+    await assert_validation_result(client, {})
+
+
+@pytest.mark.parametrize(
+    "attributes, unit1, unit2, supported_unit",
+    [
+        (NONE_SENSOR_ATTRIBUTES, "m³", "m3", "L, fl. oz., ft³, gal, mL, m³"),
+    ],
+)
+async def test_validate_statistics_unit_change_equivalent_units_2(
+    recorder_mock, hass, hass_ws_client, attributes, unit1, unit2, supported_unit
+):
+    """Test validate_statistics.
+
+    This tests a validation issue is created when a sensor's unit changes to an
+    equivalent unit which is not known to the unit converters.
+    """
+
+    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, **{"unit_of_measurement": unit1}}
+    )
+    await assert_validation_result(client, {})
+
+    # Run statistics
+    await async_recorder_block_till_done(hass)
+    do_adhoc_statistics(hass, start=now)
+    await async_recorder_block_till_done(hass)
+    await assert_statistic_ids(
+        [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}]
+    )
+
+    # Units changed to an equivalent unit which is not known by the unit converters
+    hass.states.async_set(
+        "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit2}}
+    )
+    expected = {
+        "sensor.test": [
+            {
+                "data": {
+                    "metadata_unit": unit1,
+                    "state_unit": unit2,
+                    "statistic_id": "sensor.test",
+                    "supported_unit": supported_unit,
+                },
+                "type": "units_changed",
+            }
+        ],
+    }
+    await assert_validation_result(client, expected)
+
+    # Run statistics one hour later, metadata will not be updated
+    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": unit1}]
+    )
+    await assert_validation_result(client, expected)
+
+
 async def test_validate_statistics_other_domain(recorder_mock, hass, hass_ws_client):
     """Test sensor does not raise issues for statistics for other domains."""
     id = 1
-- 
GitLab


From 6b1f503a79d134439ba82910b97b5211e4a0a059 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 25 Oct 2022 09:53:28 +0200
Subject: [PATCH 777/985] Allow gas units to be overriden (#80884)

---
 homeassistant/components/sensor/__init__.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index e353f089951..fbe12652f35 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -376,9 +376,11 @@ STATE_CLASS_TOTAL: Final = "total"
 STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing"
 STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
 
-UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
+# Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in
+# `entity-registry-settings.ts`
+UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
     SensorDeviceClass.DISTANCE: DistanceConverter,
-    SensorDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter,
+    SensorDeviceClass.GAS: VolumeConverter,
     SensorDeviceClass.PRESSURE: PressureConverter,
     SensorDeviceClass.SPEED: SpeedConverter,
     SensorDeviceClass.TEMPERATURE: TemperatureConverter,
-- 
GitLab


From e5716efa9ce508ff8c9e21cfc8123b612e80206b Mon Sep 17 00:00:00 2001
From: Dave T <17680170+davet2001@users.noreply.github.com>
Date: Tue, 25 Oct 2022 09:03:19 +0100
Subject: [PATCH 778/985] Add visual image preview during generic camera
 options flow (#80392)

Co-authored-by: Dave T <davet2001@users.noreply.github.com>
---
 .../components/generic/config_flow.py         | 47 ++++++++++++++-----
 homeassistant/components/generic/strings.json |  7 +++
 tests/components/generic/test_config_flow.py  | 38 +++++++++++++--
 3 files changed, 78 insertions(+), 14 deletions(-)

diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py
index 19ab7666b7c..09f52705734 100644
--- a/homeassistant/components/generic/config_flow.py
+++ b/homeassistant/components/generic/config_flow.py
@@ -394,8 +394,7 @@ class GenericOptionsFlowHandler(OptionsFlow):
     def __init__(self, config_entry: ConfigEntry) -> None:
         """Initialize Generic IP Camera options flow."""
         self.config_entry = config_entry
-        self.cached_user_input: dict[str, Any] = {}
-        self.cached_title = ""
+        self.user_input: dict[str, Any] = {}
 
     async def async_step_init(
         self, user_input: dict[str, Any] | None = None
@@ -410,9 +409,7 @@ class GenericOptionsFlowHandler(OptionsFlow):
             )
             errors = errors | await async_test_stream(hass, user_input)
             still_url = user_input.get(CONF_STILL_IMAGE_URL)
-            stream_url = user_input.get(CONF_STREAM_SOURCE)
             if not errors:
-                title = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME
                 if still_url is None:
                     # If user didn't specify a still image URL,
                     # The automatically generated still image that stream generates
@@ -438,10 +435,10 @@ class GenericOptionsFlowHandler(OptionsFlow):
                         ),
                     ),
                 }
-                return self.async_create_entry(
-                    title=title,
-                    data=data,
-                )
+                self.user_input = data
+                # temporary preview for user to check the image
+                self.context["preview_cam"] = data
+                return await self.async_step_confirm_still()
         return self.async_show_form(
             step_id="init",
             data_schema=build_schema(
@@ -452,6 +449,30 @@ class GenericOptionsFlowHandler(OptionsFlow):
             errors=errors,
         )
 
+    async def async_step_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_init()
+            return self.async_create_entry(
+                title=self.config_entry.title,
+                data=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="confirm_still",
+            data_schema=vol.Schema(
+                {
+                    vol.Required(CONF_CONFIRMED_OK, default=False): bool,
+                }
+            ),
+            description_placeholders={"preview_url": preview_url},
+            errors=None,
+        )
+
 
 class CameraImagePreview(HomeAssistantView):
     """Camera view to temporarily serve an image."""
@@ -468,9 +489,13 @@ class CameraImagePreview(HomeAssistantView):
         """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
+            flow = self.hass.config_entries.flow.async_get(flow_id)
+        except UnknownFlow:
+            try:
+                flow = self.hass.config_entries.options.async_get(flow_id)
+            except UnknownFlow as exc:
+                _LOGGER.warning("Unknown flow while getting image preview")
+                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:
diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json
index 7ada90e3c90..7c7e44a67e4 100644
--- a/homeassistant/components/generic/strings.json
+++ b/homeassistant/components/generic/strings.json
@@ -73,6 +73,13 @@
         "data": {
           "content_type": "[%key:component::generic::config::step::content_type::data::content_type%]"
         }
+      },
+      "confirm_still": {
+        "title": "[%key:component::generic::config::step::user_confirm_still::title%]",
+        "description": "[%key:component::generic::config::step::user_confirm_still::description%]",
+        "data": {
+          "confirmed_ok": "[%key:component::generic::config::step::user_confirm_still::data::confirmed_ok%]"
+        }
       }
     },
     "error": {
diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py
index 6ec1ce0c32b..ba4ff4dbd0c 100644
--- a/tests/components/generic/test_config_flow.py
+++ b/tests/components/generic/test_config_flow.py
@@ -596,7 +596,13 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream
             result["flow_id"],
             user_input=data,
         )
-        assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+        assert result2["type"] == data_entry_flow.FlowResultType.FORM
+        assert result2["step_id"] == "confirm_still"
+
+        result2a = await hass.config_entries.options.async_configure(
+            result2["flow_id"], user_input={CONF_CONFIRMED_OK: True}
+        )
+        assert result2a["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
 
         result3 = await hass.config_entries.options.async_init(mock_entry.entry_id)
         assert result3["type"] == data_entry_flow.FlowResultType.FORM
@@ -681,10 +687,16 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream):
 
     # try updating the config options
     with mock_create_stream:
-        result3 = await hass.config_entries.options.async_configure(
+        result2 = await hass.config_entries.options.async_configure(
             result["flow_id"],
             user_input=data,
         )
+    assert result2["type"] == data_entry_flow.FlowResultType.FORM
+    assert result2["step_id"] == "confirm_still"
+
+    result3 = await hass.config_entries.options.async_configure(
+        result2["flow_id"], user_input={CONF_CONFIRMED_OK: True}
+    )
     assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
     assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg"
 
@@ -809,4 +821,24 @@ async def test_use_wallclock_as_timestamps_option(
             result["flow_id"],
             user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA},
         )
-    assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+    assert result2["type"] == data_entry_flow.FlowResultType.FORM
+    # Test what happens if user rejects the preview
+    result3 = await hass.config_entries.options.async_configure(
+        result2["flow_id"], user_input={CONF_CONFIRMED_OK: False}
+    )
+    assert result3["type"] == data_entry_flow.FlowResultType.FORM
+    assert result3["step_id"] == "init"
+    with patch(
+        "homeassistant.components.generic.async_setup_entry", return_value=True
+    ), mock_create_stream:
+        result4 = await hass.config_entries.options.async_configure(
+            result3["flow_id"],
+            user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA},
+        )
+    assert result4["type"] == data_entry_flow.FlowResultType.FORM
+    assert result4["step_id"] == "confirm_still"
+    result5 = await hass.config_entries.options.async_configure(
+        result4["flow_id"],
+        user_input={CONF_CONFIRMED_OK: True},
+    )
+    assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
-- 
GitLab


From 02b0b19dd492b21c2fae4074770d6e3b333dad79 Mon Sep 17 00:00:00 2001
From: osono-design <73175269+osono-design@users.noreply.github.com>
Date: Tue, 25 Oct 2022 10:07:59 +0200
Subject: [PATCH 779/985] Add tuya access control devices (binary status of
 lock) (#79793)

---
 homeassistant/components/tuya/binary_sensor.py | 9 +++++++++
 homeassistant/components/tuya/const.py         | 1 +
 2 files changed, 10 insertions(+)

diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py
index 44d37050229..7da59c8cc76 100644
--- a/homeassistant/components/tuya/binary_sensor.py
+++ b/homeassistant/components/tuya/binary_sensor.py
@@ -201,6 +201,15 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
         ),
         TAMPER_BINARY_SENSOR,
     ),
+    # Access Control
+    # https://developer.tuya.com/en/docs/iot/s?id=Kb0o2xhlkxbet
+    "mk": (
+        TuyaBinarySensorEntityDescription(
+            key=DPCode.CLOSED_OPENED_KIT,
+            device_class=BinarySensorDeviceClass.LOCK,
+            on_value={"AQAB"},
+        ),
+    ),
     # Luminance Sensor
     # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8
     "ldcg": (
diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py
index 792036e49ff..50e55cd8d77 100644
--- a/homeassistant/components/tuya/const.py
+++ b/homeassistant/components/tuya/const.py
@@ -182,6 +182,7 @@ class DPCode(StrEnum):
     CLEAN_TIME = "clean_time"
     CLICK_SUSTAIN_TIME = "click_sustain_time"
     CLOUD_RECIPE_NUMBER = "cloud_recipe_number"
+    CLOSED_OPENED_KIT = "closed_opened_kit"
     CO_STATE = "co_state"
     CO_STATUS = "co_status"
     CO_VALUE = "co_value"
-- 
GitLab


From beeee8b60eaa2415add69cd0b385dec56182a407 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Tue, 25 Oct 2022 10:38:07 +0200
Subject: [PATCH 780/985] Use start helper in recorder (#79559)

---
 homeassistant/components/recorder/core.py | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py
index 833064cee63..0ef8c20d5c7 100644
--- a/homeassistant/components/recorder/core.py
+++ b/homeassistant/components/recorder/core.py
@@ -26,18 +26,18 @@ from homeassistant.components import persistent_notification
 from homeassistant.const import (
     ATTR_ENTITY_ID,
     EVENT_HOMEASSISTANT_FINAL_WRITE,
-    EVENT_HOMEASSISTANT_STARTED,
     EVENT_HOMEASSISTANT_STOP,
     EVENT_STATE_CHANGED,
     MATCH_ALL,
 )
-from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
+from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
 from homeassistant.helpers.event import (
     async_track_time_change,
     async_track_time_interval,
     async_track_utc_time_change,
 )
 from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS
+from homeassistant.helpers.start import async_at_started
 from homeassistant.helpers.typing import UNDEFINED, UndefinedType
 import homeassistant.util.dt as dt_util
 
@@ -407,7 +407,7 @@ class Recorder(threading.Thread):
         await self.hass.async_add_executor_job(self.join)
 
     @callback
-    def _async_hass_started(self, event: Event) -> None:
+    def _async_hass_started(self, hass: HomeAssistant) -> None:
         """Notify that hass has started."""
         self._hass_started.set_result(None)
 
@@ -417,10 +417,7 @@ class Recorder(threading.Thread):
         bus = self.hass.bus
         bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._empty_queue)
         bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown)
-        if self.hass.state == CoreState.running:
-            self._hass_started.set_result(None)
-            return
-        bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_hass_started)
+        async_at_started(self.hass, self._async_hass_started)
 
     @callback
     def async_connection_failed(self) -> None:
-- 
GitLab


From 52f109f6abe6f7c6b6d2bbe1901bb13b4e1c1caf Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 25 Oct 2022 12:09:22 +0200
Subject: [PATCH 781/985] Add IKEA SYMFONISK as virtual integration (#80833)

---
 homeassistant/brands/ikea.json                |  5 +++++
 .../components/symfonisk/__init__.py          |  1 +
 .../components/symfonisk/manifest.json        |  6 ++++++
 homeassistant/generated/integrations.json     | 21 +++++++++++++------
 4 files changed, 27 insertions(+), 6 deletions(-)
 create mode 100644 homeassistant/brands/ikea.json
 create mode 100644 homeassistant/components/symfonisk/__init__.py
 create mode 100644 homeassistant/components/symfonisk/manifest.json

diff --git a/homeassistant/brands/ikea.json b/homeassistant/brands/ikea.json
new file mode 100644
index 00000000000..702a59ad4d1
--- /dev/null
+++ b/homeassistant/brands/ikea.json
@@ -0,0 +1,5 @@
+{
+  "domain": "ikea",
+  "name": "IKEA",
+  "integrations": ["symfonisk", "tradfri"]
+}
diff --git a/homeassistant/components/symfonisk/__init__.py b/homeassistant/components/symfonisk/__init__.py
new file mode 100644
index 00000000000..5b7ce387e29
--- /dev/null
+++ b/homeassistant/components/symfonisk/__init__.py
@@ -0,0 +1 @@
+"""Virtual integration: IKEA SYMFONISK."""
diff --git a/homeassistant/components/symfonisk/manifest.json b/homeassistant/components/symfonisk/manifest.json
new file mode 100644
index 00000000000..89c05fa49b0
--- /dev/null
+++ b/homeassistant/components/symfonisk/manifest.json
@@ -0,0 +1,6 @@
+{
+  "domain": "symfonisk",
+  "name": "IKEA SYMFONISK",
+  "integration_type": "virtual",
+  "supported_by": "sonos"
+}
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 16bceb1fb2b..99177d10e5a 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2363,6 +2363,21 @@
       "config_flow": false,
       "iot_class": "local_push"
     },
+    "ikea": {
+      "name": "IKEA",
+      "integrations": {
+        "symfonisk": {
+          "integration_type": "virtual",
+          "name": "IKEA SYMFONISK"
+        },
+        "tradfri": {
+          "integration_type": "hub",
+          "config_flow": true,
+          "iot_class": "local_polling",
+          "name": "IKEA TR\u00c5DFRI"
+        }
+      }
+    },
     "imap": {
       "name": "IMAP",
       "integration_type": "hub",
@@ -5489,12 +5504,6 @@
       "config_flow": true,
       "iot_class": "cloud_push"
     },
-    "tradfri": {
-      "name": "IKEA TR\u00c5DFRI",
-      "integration_type": "hub",
-      "config_flow": true,
-      "iot_class": "local_polling"
-    },
     "trafikverket": {
       "name": "Trafikverket",
       "integrations": {
-- 
GitLab


From 398d18eeee9f8de6ffbf52975b412b78da554650 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 25 Oct 2022 12:10:40 +0200
Subject: [PATCH 782/985] Load themes from themes folder by default (#80937)

---
 homeassistant/config.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/homeassistant/config.py b/homeassistant/config.py
index ab3ad0eb5c1..e56dff4e491 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -87,6 +87,10 @@ DEFAULT_CONFIG = f"""
 # Loads default set of integrations. Do not remove.
 default_config:
 
+# Load frontend themes from the themes folder
+frontend:
+  themes: !include_dir_merge_named themes
+
 # Text to speech
 tts:
   - platform: google_translate
-- 
GitLab


From 326344db1263e00b2bf65083afc44d383ff9d7dc Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 25 Oct 2022 12:11:14 +0200
Subject: [PATCH 783/985] Rename entry_id template method to config_entry_id
 (#80935)

---
 homeassistant/helpers/template.py |  8 ++++----
 tests/helpers/test_template.py    | 14 ++++++++------
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index f04231ce78a..0db755e5115 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -1063,8 +1063,8 @@ 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."""
+def config_entry_id(hass: HomeAssistant, entity_id: str) -> str | None:
+    """Get an config 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
@@ -2090,8 +2090,8 @@ 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["config_entry_id"] = hassfunction(config_entry_id)
+        self.filters["config_entry_id"] = pass_context(self.globals["config_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 2edb592ce39..28130bdb3bf 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -2474,8 +2474,8 @@ async def test_integration_entities(hass):
     assert info.rate_limit is None
 
 
-async def test_entry_id(hass):
-    """Test entry_id function."""
+async def test_config_entry_id(hass):
+    """Test config_entry_id function."""
     config_entry = MockConfigEntry(domain="light", title="Some integration")
     config_entry.add_to_hass(hass)
     entity_registry = mock_registry(hass)
@@ -2483,17 +2483,19 @@ async def test_entry_id(hass):
         "sensor", "test", "test", suggested_object_id="test", config_entry=config_entry
     )
 
-    info = render_to_info(hass, "{{ 'sensor.fail' | entry_id }}")
+    info = render_to_info(hass, "{{ 'sensor.fail' | config_entry_id }}")
     assert_result_info(info, None)
     assert info.rate_limit is None
 
-    info = render_to_info(hass, "{{ 56 | entry_id }}")
+    info = render_to_info(hass, "{{ 56 | config_entry_id }}")
     assert_result_info(info, None)
 
-    info = render_to_info(hass, "{{ 'not_a_real_entity_id' | entry_id }}")
+    info = render_to_info(hass, "{{ 'not_a_real_entity_id' | config_entry_id }}")
     assert_result_info(info, None)
 
-    info = render_to_info(hass, f"{{{{ entry_id('{entity_entry.entity_id}') }}}}")
+    info = render_to_info(
+        hass, f"{{{{ config_entry_id('{entity_entry.entity_id}') }}}}"
+    )
     assert_result_info(info, config_entry.entry_id)
     assert info.rate_limit is None
 
-- 
GitLab


From e77025ac8dd5276031b417b73b3c3d9ef7f12fc8 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Tue, 25 Oct 2022 13:16:08 +0300
Subject: [PATCH 784/985] Bump aioshelly to 4.1.1 (#80939)

---
 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 54396d7171e..07499ce1e9d 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.0"],
+  "requirements": ["aioshelly==4.1.1"],
   "dependencies": ["http"],
   "zeroconf": [
     {
diff --git a/requirements_all.txt b/requirements_all.txt
index 2f6caf82de0..dbc147173be 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.0
+aioshelly==4.1.1
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 48a69f894a4..23dcd128835 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.0
+aioshelly==4.1.1
 
 # homeassistant.components.skybell
 aioskybell==22.7.0
-- 
GitLab


From fe7402375d2f899a7edd6ac326d2c1998b4c43da Mon Sep 17 00:00:00 2001
From: HarvsG <11440490+HarvsG@users.noreply.github.com>
Date: Tue, 25 Oct 2022 11:42:59 +0100
Subject: [PATCH 785/985] Bayesian - support `unique_id:` (#79879)

* support unique_id

* adds test for unique_ids
---
 homeassistant/components/bayesian/binary_sensor.py | 12 +++++++++++-
 tests/components/bayesian/test_binary_sensor.py    | 10 ++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py
index f3a7d08ffb9..28af050e85e 100644
--- a/homeassistant/components/bayesian/binary_sensor.py
+++ b/homeassistant/components/bayesian/binary_sensor.py
@@ -22,6 +22,7 @@ from homeassistant.const import (
     CONF_NAME,
     CONF_PLATFORM,
     CONF_STATE,
+    CONF_UNIQUE_ID,
     CONF_VALUE_TEMPLATE,
     STATE_UNAVAILABLE,
     STATE_UNKNOWN,
@@ -100,6 +101,7 @@ TEMPLATE_SCHEMA = vol.Schema(
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
     {
         vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+        vol.Optional(CONF_UNIQUE_ID): cv.string,
         vol.Optional(CONF_DEVICE_CLASS): cv.string,
         vol.Required(CONF_OBSERVATIONS): vol.Schema(
             vol.All(
@@ -134,6 +136,7 @@ async def async_setup_platform(
     await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
 
     name: str = config[CONF_NAME]
+    unique_id: str | None = config.get(CONF_UNIQUE_ID)
     observations: list[ConfigType] = config[CONF_OBSERVATIONS]
     prior: float = config[CONF_PRIOR]
     probability_threshold: float = config[CONF_PROBABILITY_THRESHOLD]
@@ -152,7 +155,12 @@ async def async_setup_platform(
     async_add_entities(
         [
             BayesianBinarySensor(
-                name, prior, observations, probability_threshold, device_class
+                name,
+                unique_id,
+                prior,
+                observations,
+                probability_threshold,
+                device_class,
             )
         ]
     )
@@ -166,6 +174,7 @@ class BayesianBinarySensor(BinarySensorEntity):
     def __init__(
         self,
         name: str,
+        unique_id: str | None,
         prior: float,
         observations: list[ConfigType],
         probability_threshold: float,
@@ -173,6 +182,7 @@ class BayesianBinarySensor(BinarySensorEntity):
     ) -> None:
         """Initialize the Bayesian sensor."""
         self._attr_name = name
+        self._attr_unique_id = unique_id and f"bayesian-{unique_id}"
         self._observations = [
             Observation(
                 entity_id=observation.get(CONF_ENTITY_ID),
diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py
index e16033c66a2..4dd42705026 100644
--- a/tests/components/bayesian/test_binary_sensor.py
+++ b/tests/components/bayesian/test_binary_sensor.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
     STATE_UNKNOWN,
 )
 from homeassistant.core import Context, callback
+from homeassistant.helpers.entity_registry import async_get as async_get_entities
 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
@@ -31,6 +32,8 @@ async def test_load_values_when_added_to_hass(hass):
         "binary_sensor": {
             "name": "Test_Binary",
             "platform": "bayesian",
+            "unique_id": "3b4c9563-5e84-4167-8fe7-8f507e796d72",
+            "device_class": "connectivity",
             "observations": [
                 {
                     "platform": "state",
@@ -51,7 +54,14 @@ async def test_load_values_when_added_to_hass(hass):
     assert await async_setup_component(hass, "binary_sensor", config)
     await hass.async_block_till_done()
 
+    entity_registry = async_get_entities(hass)
+    assert (
+        entity_registry.entities["binary_sensor.test_binary"].unique_id
+        == "bayesian-3b4c9563-5e84-4167-8fe7-8f507e796d72"
+    )
+
     state = hass.states.get("binary_sensor.test_binary")
+    assert state.attributes.get("device_class") == "connectivity"
     assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8
     assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4
 
-- 
GitLab


From aea0067e496baee4e564da6c6d9d208da934ab59 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Tue, 25 Oct 2022 12:51:23 +0200
Subject: [PATCH 786/985] Add additional rules for converting distances
 (#80940)

* Add additional rules for converting distances

* Convert in to mm

* Adjust existing tests

* Add test
---
 homeassistant/util/unit_system.py | 31 ++++++++++++++++------
 tests/helpers/test_template.py    |  2 +-
 tests/util/test_unit_system.py    | 43 ++++++++++++++++++++++++++-----
 3 files changed, 60 insertions(+), 16 deletions(-)

diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index db0e0e49e17..72779fe8c84 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -9,8 +9,14 @@ import voluptuous as vol
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
     LENGTH,
+    LENGTH_CENTIMETERS,
+    LENGTH_FEET,
+    LENGTH_INCHES,
     LENGTH_KILOMETERS,
+    LENGTH_METERS,
     LENGTH_MILES,
+    LENGTH_MILLIMETERS,
+    LENGTH_YARD,
     MASS,
     MASS_GRAMS,
     MASS_KILOGRAMS,
@@ -92,8 +98,8 @@ class UnitSystem:
         name: str,
         *,
         accumulated_precipitation: str,
+        conversions: dict[tuple[str | None, str | None], str],
         length: str,
-        length_conversions: dict[str | None, str],
         mass: str,
         pressure: str,
         temperature: str,
@@ -126,7 +132,7 @@ class UnitSystem:
         self.pressure_unit = pressure
         self.volume_unit = volume
         self.wind_speed_unit = wind_speed
-        self._length_conversions = length_conversions
+        self._conversions = conversions
 
     @property
     def name(self) -> str:
@@ -226,10 +232,7 @@ class UnitSystem:
         original_unit: str | None,
     ) -> str | None:
         """Return converted unit given a device class or an original unit."""
-        if device_class == "distance":
-            return self._length_conversions.get(original_unit)
-
-        return None
+        return self._conversions.get((device_class, original_unit))
 
 
 def get_unit_system(key: str) -> UnitSystem:
@@ -259,8 +262,14 @@ validate_unit_system = vol.All(
 METRIC_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_METRIC,
     accumulated_precipitation=PRECIPITATION_MILLIMETERS,
+    conversions={
+        # Convert non-metric distances
+        ("distance", LENGTH_FEET): LENGTH_METERS,
+        ("distance", LENGTH_INCHES): LENGTH_MILLIMETERS,
+        ("distance", LENGTH_MILES): LENGTH_KILOMETERS,
+        ("distance", LENGTH_YARD): LENGTH_METERS,
+    },
     length=LENGTH_KILOMETERS,
-    length_conversions={LENGTH_MILES: LENGTH_KILOMETERS},
     mass=MASS_GRAMS,
     pressure=PRESSURE_PA,
     temperature=TEMP_CELSIUS,
@@ -271,8 +280,14 @@ METRIC_SYSTEM = UnitSystem(
 US_CUSTOMARY_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_US_CUSTOMARY,
     accumulated_precipitation=PRECIPITATION_INCHES,
+    conversions={
+        # Convert non-USCS distances
+        ("distance", LENGTH_CENTIMETERS): LENGTH_INCHES,
+        ("distance", LENGTH_KILOMETERS): LENGTH_MILES,
+        ("distance", LENGTH_METERS): LENGTH_FEET,
+        ("distance", LENGTH_MILLIMETERS): LENGTH_INCHES,
+    },
     length=LENGTH_MILES,
-    length_conversions={LENGTH_KILOMETERS: LENGTH_MILES},
     mass=MASS_POUNDS,
     pressure=PRESSURE_PSI,
     temperature=TEMP_FAHRENHEIT,
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index 28130bdb3bf..e42f001818b 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -44,8 +44,8 @@ def _set_up_units(hass):
     hass.config.units = UnitSystem(
         "custom",
         accumulated_precipitation=LENGTH_MILLIMETERS,
+        conversions={},
         length=LENGTH_METERS,
-        length_conversions={},
         mass=MASS_GRAMS,
         pressure=PRESSURE_PA,
         temperature=TEMP_CELSIUS,
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 6734abba7ac..dd584a8dd3b 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -4,9 +4,14 @@ import pytest
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
     LENGTH,
+    LENGTH_CENTIMETERS,
+    LENGTH_FEET,
+    LENGTH_INCHES,
     LENGTH_KILOMETERS,
     LENGTH_METERS,
+    LENGTH_MILES,
     LENGTH_MILLIMETERS,
+    LENGTH_YARD,
     MASS,
     MASS_GRAMS,
     PRESSURE,
@@ -40,8 +45,8 @@ def test_invalid_units():
         UnitSystem(
             SYSTEM_NAME,
             accumulated_precipitation=LENGTH_MILLIMETERS,
+            conversions={},
             length=LENGTH_METERS,
-            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
             temperature=INVALID_UNIT,
@@ -53,8 +58,8 @@ def test_invalid_units():
         UnitSystem(
             SYSTEM_NAME,
             accumulated_precipitation=LENGTH_MILLIMETERS,
+            conversions={},
             length=INVALID_UNIT,
-            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
             temperature=TEMP_CELSIUS,
@@ -66,8 +71,8 @@ def test_invalid_units():
         UnitSystem(
             SYSTEM_NAME,
             accumulated_precipitation=LENGTH_MILLIMETERS,
+            conversions={},
             length=LENGTH_METERS,
-            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
             temperature=TEMP_CELSIUS,
@@ -79,8 +84,8 @@ def test_invalid_units():
         UnitSystem(
             SYSTEM_NAME,
             accumulated_precipitation=LENGTH_MILLIMETERS,
+            conversions={},
             length=LENGTH_METERS,
-            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
             temperature=TEMP_CELSIUS,
@@ -92,8 +97,8 @@ def test_invalid_units():
         UnitSystem(
             SYSTEM_NAME,
             accumulated_precipitation=LENGTH_MILLIMETERS,
+            conversions={},
             length=LENGTH_METERS,
-            length_conversions={},
             mass=INVALID_UNIT,
             pressure=PRESSURE_PA,
             temperature=TEMP_CELSIUS,
@@ -105,8 +110,8 @@ def test_invalid_units():
         UnitSystem(
             SYSTEM_NAME,
             accumulated_precipitation=LENGTH_MILLIMETERS,
+            conversions={},
             length=LENGTH_METERS,
-            length_conversions={},
             mass=MASS_GRAMS,
             pressure=INVALID_UNIT,
             temperature=TEMP_CELSIUS,
@@ -118,8 +123,8 @@ def test_invalid_units():
         UnitSystem(
             SYSTEM_NAME,
             accumulated_precipitation=INVALID_UNIT,
+            conversions={},
             length=LENGTH_METERS,
-            length_conversions={},
             mass=MASS_GRAMS,
             pressure=PRESSURE_PA,
             temperature=TEMP_CELSIUS,
@@ -374,3 +379,27 @@ def test_get_unit_system_invalid(key: str) -> None:
     """Test get_unit_system with an invalid key."""
     with pytest.raises(ValueError, match=f"`{key}` is not a valid unit system key"):
         _ = get_unit_system(key)
+
+
+@pytest.mark.parametrize(
+    "unit_system, device_class, original_unit, state_unit",
+    (
+        (METRIC_SYSTEM, "distance", LENGTH_FEET, LENGTH_METERS),
+        (METRIC_SYSTEM, "distance", LENGTH_INCHES, LENGTH_MILLIMETERS),
+        (METRIC_SYSTEM, "distance", LENGTH_MILES, LENGTH_KILOMETERS),
+        (METRIC_SYSTEM, "distance", LENGTH_YARD, LENGTH_METERS),
+        (METRIC_SYSTEM, "distance", LENGTH_KILOMETERS, None),
+        (METRIC_SYSTEM, "distance", "very_long", None),
+        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_CENTIMETERS, LENGTH_INCHES),
+        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_KILOMETERS, LENGTH_MILES),
+        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_METERS, LENGTH_FEET),
+        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_MILLIMETERS, LENGTH_INCHES),
+        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_MILES, None),
+        (US_CUSTOMARY_SYSTEM, "distance", "very_long", None),
+    ),
+)
+def test_get_converted_unit(
+    unit_system, device_class, original_unit, state_unit
+) -> None:
+    """Test unit conversion rules."""
+    assert unit_system.get_converted_unit(device_class, original_unit) == state_unit
-- 
GitLab


From dbfca8def81b6aac9b4bd0a494c96a0b15f1a82c Mon Sep 17 00:00:00 2001
From: Parham Ghazanfari <pg53@vt.edu>
Date: Tue, 25 Oct 2022 07:21:25 -0400
Subject: [PATCH 787/985] Add support for EventBridge to aws integration
 (#77573)

* Added EventBridge support to aws integration

* Added type hints for all aws notification services + Added unit tests for EventBridge AWS integration

* Increase line coverage for unit tests for aws integration.
---
 homeassistant/components/aws/__init__.py |   2 +-
 homeassistant/components/aws/notify.py   |  59 +++++++++++-
 tests/components/aws/test_init.py        | 113 ++++++++++++++++++++++-
 3 files changed, 169 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py
index 156684f084c..0638ae94de2 100644
--- a/homeassistant/components/aws/__init__.py
+++ b/homeassistant/components/aws/__init__.py
@@ -51,7 +51,7 @@ DEFAULT_CREDENTIAL = [
     {CONF_NAME: "default", CONF_PROFILE_NAME: "default", CONF_VALIDATE: False}
 ]
 
-SUPPORTED_SERVICES = ["lambda", "sns", "sqs"]
+SUPPORTED_SERVICES = ["lambda", "sns", "sqs", "events"]
 
 NOTIFY_PLATFORM_SCHEMA = vol.Schema(
     {
diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py
index c4e450a4aab..e11b9db6c5e 100644
--- a/homeassistant/components/aws/notify.py
+++ b/homeassistant/components/aws/notify.py
@@ -3,6 +3,7 @@ import asyncio
 import base64
 import json
 import logging
+from typing import Any
 
 from aiobotocore.session import AioSession
 
@@ -105,6 +106,9 @@ async def async_get_service(hass, config, discovery_info=None):
     if service == "sqs":
         return AWSSQS(session, aws_config)
 
+    if service == "events":
+        return AWSEventBridge(session, aws_config)
+
     # should not reach here since service was checked in schema
     return None
 
@@ -128,7 +132,7 @@ class AWSLambda(AWSNotify):
         super().__init__(session, aws_config)
         self.context = context
 
-    async def async_send_message(self, message="", **kwargs):
+    async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
         """Send notification to specified LAMBDA ARN."""
         if not kwargs.get(ATTR_TARGET):
             _LOGGER.error("At least one target is required")
@@ -161,7 +165,7 @@ class AWSSNS(AWSNotify):
 
     service = "sns"
 
-    async def async_send_message(self, message="", **kwargs):
+    async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
         """Send notification to specified SNS ARN."""
         if not kwargs.get(ATTR_TARGET):
             _LOGGER.error("At least one target is required")
@@ -199,7 +203,7 @@ class AWSSQS(AWSNotify):
 
     service = "sqs"
 
-    async def async_send_message(self, message="", **kwargs):
+    async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
         """Send notification to specified SQS ARN."""
         if not kwargs.get(ATTR_TARGET):
             _LOGGER.error("At least one target is required")
@@ -231,3 +235,52 @@ class AWSSQS(AWSNotify):
 
             if tasks:
                 await asyncio.gather(*tasks)
+
+
+class AWSEventBridge(AWSNotify):
+    """Implement the notification service for the AWS EventBridge service."""
+
+    service = "events"
+
+    async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
+        """Send notification to specified EventBus."""
+
+        cleaned_kwargs = {k: v for k, v in kwargs.items() if v is not None}
+        data = cleaned_kwargs.get(ATTR_DATA, {})
+        detail = (
+            json.dumps(data["detail"])
+            if "detail" in data
+            else json.dumps({"message": message})
+        )
+
+        async with self.session.create_client(
+            self.service, **self.aws_config
+        ) as client:
+            tasks = []
+            entries = []
+            for target in kwargs.get(ATTR_TARGET, [None]):
+                entry = {
+                    "Source": data.get("source", "homeassistant"),
+                    "Resources": data.get("resources", []),
+                    "Detail": detail,
+                    "DetailType": data.get("detail_type", ""),
+                }
+                if target:
+                    entry["EventBusName"] = target
+
+                entries.append(entry)
+            for i in range(0, len(entries), 10):
+                tasks.append(
+                    client.put_events(Entries=entries[i : min(i + 10, len(entries))])
+                )
+
+            if tasks:
+                results = await asyncio.gather(*tasks)
+                for result in results:
+                    for entry in result["Entries"]:
+                        if len(entry.get("EventId", "")) == 0:
+                            _LOGGER.error(
+                                "Failed to send event: ErrorCode=%s ErrorMessage=%s",
+                                entry["ErrorCode"],
+                                entry["ErrorMessage"],
+                            )
diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py
index 0ad1de9a7a5..3b1dcaea1e5 100644
--- a/tests/components/aws/test_init.py
+++ b/tests/components/aws/test_init.py
@@ -1,5 +1,6 @@
 """Tests for the aws component config and setup."""
-from unittest.mock import AsyncMock, MagicMock, patch as async_patch
+import json
+from unittest.mock import AsyncMock, MagicMock, call, patch as async_patch
 
 from homeassistant.setup import async_setup_component
 
@@ -13,6 +14,7 @@ class MockAioSession:
         self.invoke = AsyncMock()
         self.publish = AsyncMock()
         self.send_message = AsyncMock()
+        self.put_events = AsyncMock()
 
     def create_client(self, *args, **kwargs):
         """Create a mocked client."""
@@ -23,6 +25,7 @@ class MockAioSession:
                     invoke=self.invoke,  # lambda
                     publish=self.publish,  # sns
                     send_message=self.send_message,  # sqs
+                    put_events=self.put_events,  # events
                 )
             ),
             __aexit__=AsyncMock(),
@@ -289,3 +292,111 @@ async def test_service_call_extra_data(hass):
             "AWS.SNS.SMS.SenderID": {"StringValue": "HA-notify", "DataType": "String"}
         },
     )
+
+
+async def test_events_service_call(hass):
+    """Test events service (EventBridge) call works as expected."""
+    mock_session = MockAioSession()
+    with async_patch(
+        "homeassistant.components.aws.AioSession", return_value=mock_session
+    ):
+        await async_setup_component(
+            hass,
+            "aws",
+            {
+                "aws": {
+                    "notify": [
+                        {
+                            "service": "events",
+                            "name": "Events Test",
+                            "region_name": "us-east-1",
+                        }
+                    ]
+                }
+            },
+        )
+        await hass.async_block_till_done()
+
+    assert hass.services.has_service("notify", "events_test") is True
+
+    mock_session.put_events.return_value = {
+        "Entries": [{"EventId": "", "ErrorCode": 0, "ErrorMessage": "test-error"}]
+    }
+
+    await hass.services.async_call(
+        "notify",
+        "events_test",
+        {
+            "message": "test",
+            "target": "ARN",
+            "data": {},
+        },
+        blocking=True,
+    )
+
+    mock_session.put_events.assert_called_once_with(
+        Entries=[
+            {
+                "EventBusName": "ARN",
+                "Detail": json.dumps({"message": "test"}),
+                "DetailType": "",
+                "Source": "homeassistant",
+                "Resources": [],
+            }
+        ]
+    )
+
+
+async def test_events_service_call_10_targets(hass):
+    """Test events service (EventBridge) call works with more than 10 targets."""
+    mock_session = MockAioSession()
+    with async_patch(
+        "homeassistant.components.aws.AioSession", return_value=mock_session
+    ):
+        await async_setup_component(
+            hass,
+            "aws",
+            {
+                "aws": {
+                    "notify": [
+                        {
+                            "service": "events",
+                            "name": "Events Test",
+                            "region_name": "us-east-1",
+                        }
+                    ]
+                }
+            },
+        )
+        await hass.async_block_till_done()
+
+    assert hass.services.has_service("notify", "events_test") is True
+    await hass.services.async_call(
+        "notify",
+        "events_test",
+        {
+            "message": "",
+            "target": [f"eventbus{i}" for i in range(11)],
+            "data": {
+                "detail_type": "test_event",
+                "detail": {"eventkey": "eventvalue"},
+                "source": "HomeAssistant-test",
+                "resources": ["resource1", "resource2"],
+            },
+        },
+        blocking=True,
+    )
+
+    entry = {
+        "Detail": json.dumps({"eventkey": "eventvalue"}),
+        "DetailType": "test_event",
+        "Source": "HomeAssistant-test",
+        "Resources": ["resource1", "resource2"],
+    }
+
+    mock_session.put_events.assert_has_calls(
+        [
+            call(Entries=[entry | {"EventBusName": f"eventbus{i}"} for i in range(10)]),
+            call(Entries=[entry | {"EventBusName": "eventbus10"}]),
+        ]
+    )
-- 
GitLab


From 5b32540a844ad95aac1a5ae5cee342c0f1bc836f Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 25 Oct 2022 13:21:47 +0200
Subject: [PATCH 788/985] Set water device class to flo, homewizard,
 p1_monitor, toon (#80944)

---
 homeassistant/components/flo/sensor.py        | 1 +
 homeassistant/components/homewizard/sensor.py | 2 +-
 homeassistant/components/p1_monitor/sensor.py | 3 ++-
 homeassistant/components/toon/sensor.py       | 3 +++
 tests/components/homewizard/test_sensor.py    | 2 +-
 5 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py
index 9f793b749e4..9cb070a1c62 100644
--- a/homeassistant/components/flo/sensor.py
+++ b/homeassistant/components/flo/sensor.py
@@ -71,6 +71,7 @@ class FloDailyUsageSensor(FloEntity, SensorEntity):
     _attr_icon = WATER_ICON
     _attr_native_unit_of_measurement = VOLUME_GALLONS
     _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING
+    _attr_device_class = SensorDeviceClass.WATER
 
     def __init__(self, device):
         """Initialize the daily water usage sensor."""
diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py
index 6fc1d38ec12..4baaff8835d 100644
--- a/homeassistant/components/homewizard/sensor.py
+++ b/homeassistant/components/homewizard/sensor.py
@@ -131,7 +131,7 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = (
         name="Total water usage",
         native_unit_of_measurement=VOLUME_CUBIC_METERS,
         icon="mdi:gauge",
-        device_class=SensorDeviceClass.VOLUME,
+        device_class=SensorDeviceClass.WATER,
         state_class=SensorStateClass.TOTAL_INCREASING,
     ),
 )
diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py
index e55c8dacea5..8a1c9d68b5a 100644
--- a/homeassistant/components/p1_monitor/sensor.py
+++ b/homeassistant/components/p1_monitor/sensor.py
@@ -222,13 +222,14 @@ SENSORS_WATERMETER: tuple[SensorEntityDescription, ...] = (
         name="Consumption Day",
         state_class=SensorStateClass.TOTAL_INCREASING,
         native_unit_of_measurement=VOLUME_LITERS,
-        device_class=SensorDeviceClass.VOLUME,
+        device_class=SensorDeviceClass.WATER,
     ),
     SensorEntityDescription(
         key="consumption_total",
         name="Consumption Total",
         state_class=SensorStateClass.TOTAL_INCREASING,
         native_unit_of_measurement=VOLUME_CUBIC_METERS,
+        device_class=SensorDeviceClass.WATER,
     ),
     SensorEntityDescription(
         key="pulse_count",
diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py
index 371672e184e..9178cc6c01a 100644
--- a/homeassistant/components/toon/sensor.py
+++ b/homeassistant/components/toon/sensor.py
@@ -316,6 +316,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = (
         icon="mdi:water",
         entity_registry_enabled_default=False,
         cls=ToonWaterMeterDeviceSensor,
+        device_class=SensorDeviceClass.WATER,
     ),
     ToonSensorEntityDescription(
         key="water_daily_usage",
@@ -326,6 +327,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = (
         icon="mdi:water",
         entity_registry_enabled_default=False,
         cls=ToonWaterMeterDeviceSensor,
+        device_class=SensorDeviceClass.WATER,
     ),
     ToonSensorEntityDescription(
         key="water_meter_reading",
@@ -337,6 +339,7 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = (
         entity_registry_enabled_default=False,
         state_class=SensorStateClass.TOTAL_INCREASING,
         cls=ToonWaterMeterDeviceSensor,
+        device_class=SensorDeviceClass.WATER,
     ),
     ToonSensorEntityDescription(
         key="water_value",
diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py
index c8238c75643..d405a713edb 100644
--- a/tests/components/homewizard/test_sensor.py
+++ b/tests/components/homewizard/test_sensor.py
@@ -609,7 +609,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_DEVICE_CLASS) == SensorDeviceClass.WATER
     assert state.attributes.get(ATTR_ICON) == "mdi:gauge"
 
 
-- 
GitLab


From f977b264a1c8725a8b717021af1d626ddb6c2626 Mon Sep 17 00:00:00 2001
From: hackerESQ <corey@coreyvarma.com>
Date: Tue, 25 Oct 2022 06:22:09 -0500
Subject: [PATCH 789/985] Add tplink dhcp entry for EP25 model (#80650)

Co-authored-by: Franck Nijhof <git@frenck.dev>
---
 homeassistant/components/tplink/manifest.json | 4 ++++
 homeassistant/generated/dhcp.py               | 1 +
 2 files changed, 5 insertions(+)

diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json
index dfb873564c5..a88f96e3fa4 100644
--- a/homeassistant/components/tplink/manifest.json
+++ b/homeassistant/components/tplink/manifest.json
@@ -18,6 +18,10 @@
       "hostname": "ep*",
       "macaddress": "E848B8*"
     },
+    {
+      "hostname": "ep*",
+      "macaddress": "1C61B4*"
+    },
     {
       "hostname": "ep*",
       "macaddress": "003192*"
diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py
index 7b2eb4eacf6..4b8dee0d956 100644
--- a/homeassistant/generated/dhcp.py
+++ b/homeassistant/generated/dhcp.py
@@ -143,6 +143,7 @@ DHCP: list[dict[str, str | bool]] = [
     {"domain": "tplink", "registered_devices": True},
     {"domain": "tplink", "hostname": "es*", "macaddress": "54AF97*"},
     {"domain": "tplink", "hostname": "ep*", "macaddress": "E848B8*"},
+    {"domain": "tplink", "hostname": "ep*", "macaddress": "1C61B4*"},
     {"domain": "tplink", "hostname": "ep*", "macaddress": "003192*"},
     {"domain": "tplink", "hostname": "hs*", "macaddress": "1C3BF3*"},
     {"domain": "tplink", "hostname": "hs*", "macaddress": "50C7BF*"},
-- 
GitLab


From d50f5e49c8c8491868c204237c5220aed4e1dcb1 Mon Sep 17 00:00:00 2001
From: jjlawren <jjlawren@users.noreply.github.com>
Date: Tue, 25 Oct 2022 06:38:28 -0500
Subject: [PATCH 790/985] Extend Sonos queue operation timeouts (#80804)

---
 homeassistant/components/sonos/media_player.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index 1b0d8dc6ed1..4195f284ffe 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -438,7 +438,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
             soco.play_uri(uri, title=favorite.title)
         else:
             soco.clear_queue()
-            soco.add_to_queue(favorite.reference)
+            soco.add_to_queue(favorite.reference, timeout=LONG_SERVICE_TIMEOUT)
             soco.play_from_queue(0)
 
     @property
@@ -586,13 +586,15 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
             media_id = async_process_play_media_url(self.hass, media_id)
 
             if enqueue == MediaPlayerEnqueue.ADD:
-                soco.add_uri_to_queue(media_id)
+                soco.add_uri_to_queue(media_id, timeout=LONG_SERVICE_TIMEOUT)
             elif enqueue in (
                 MediaPlayerEnqueue.NEXT,
                 MediaPlayerEnqueue.PLAY,
             ):
                 pos = (self.media.queue_position or 0) + 1
-                new_pos = soco.add_uri_to_queue(media_id, position=pos)
+                new_pos = soco.add_uri_to_queue(
+                    media_id, position=pos, timeout=LONG_SERVICE_TIMEOUT
+                )
                 if enqueue == MediaPlayerEnqueue.PLAY:
                     soco.play_from_queue(new_pos - 1)
             elif enqueue == MediaPlayerEnqueue.REPLACE:
@@ -609,7 +611,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
                 _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
             else:
                 soco.clear_queue()
-                soco.add_to_queue(playlist)
+                soco.add_to_queue(playlist, timeout=LONG_SERVICE_TIMEOUT)
                 soco.play_from_queue(0)
         elif media_type in PLAYABLE_MEDIA_TYPES:
             item = media_browser.get_media(self.media.library, media_id, media_type)
-- 
GitLab


From 623abb4325e01f8a70b390dd8eae784f4c00dbf4 Mon Sep 17 00:00:00 2001
From: Rami Mosleh <engrbm87@gmail.com>
Date: Tue, 25 Oct 2022 14:39:38 +0300
Subject: [PATCH 791/985] Remove deprecated YAML in Pushover (#80876)

---
 homeassistant/components/pushover/config_flow.py   |  4 ----
 homeassistant/components/pushover/notify.py        | 14 +++-----------
 homeassistant/components/pushover/strings.json     |  6 +++---
 .../components/pushover/translations/en.json       |  6 +++---
 tests/components/pushover/test_config_flow.py      | 13 -------------
 tests/components/pushover/test_init.py             |  4 ++--
 6 files changed, 11 insertions(+), 36 deletions(-)

diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py
index ddb61d4bbc3..5119d91a174 100644
--- a/homeassistant/components/pushover/config_flow.py
+++ b/homeassistant/components/pushover/config_flow.py
@@ -44,10 +44,6 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
 
     _reauth_entry: config_entries.ConfigEntry | None
 
-    async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
-        """Handle import from config."""
-        return await self.async_step_user(import_config)
-
     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(
diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py
index bcf47264108..fa4b35da2fa 100644
--- a/homeassistant/components/pushover/notify.py
+++ b/homeassistant/components/pushover/notify.py
@@ -15,7 +15,6 @@ from homeassistant.components.notify import (
     PLATFORM_SCHEMA,
     BaseNotificationService,
 )
-from homeassistant.config_entries import SOURCE_IMPORT
 from homeassistant.const import CONF_API_KEY
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import HomeAssistantError
@@ -57,18 +56,11 @@ async def async_get_service(
         async_create_issue(
             hass,
             DOMAIN,
-            "deprecated_yaml",
+            "removed_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,
-            )
+            translation_key="removed_yaml",
         )
         return None
 
@@ -89,7 +81,7 @@ class PushoverNotificationService(BaseNotificationService):
         self._user_key = user_key
         self.pushover = pushover
 
-    def send_message(self, message: str = "", **kwargs: dict[str, Any]) -> None:
+    def send_message(self, message: str = "", **kwargs: Any) -> None:
         """Send a message to a user."""
 
         # Extract params from data dict
diff --git a/homeassistant/components/pushover/strings.json b/homeassistant/components/pushover/strings.json
index c309a1ec01f..3c2ab66bf39 100644
--- a/homeassistant/components/pushover/strings.json
+++ b/homeassistant/components/pushover/strings.json
@@ -26,9 +26,9 @@
     }
   },
   "issues": {
-    "deprecated_yaml": {
-      "title": "The Pushover YAML configuration is being removed",
-      "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
+    "removed_yaml": {
+      "title": "The Pushover YAML configuration has been removed",
+      "description": "Configuring Pushover using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
     }
   }
 }
diff --git a/homeassistant/components/pushover/translations/en.json b/homeassistant/components/pushover/translations/en.json
index 33826000dc3..926b58726f0 100644
--- a/homeassistant/components/pushover/translations/en.json
+++ b/homeassistant/components/pushover/translations/en.json
@@ -26,9 +26,9 @@
         }
     },
     "issues": {
-        "deprecated_yaml": {
-            "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
-            "title": "The Pushover YAML configuration is being removed"
+        "removed_yaml": {
+            "title": "The Pushover YAML configuration has been removed",
+            "description": "Configuring Pushover using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
         }
     }
 }
\ No newline at end of file
diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py
index 1e919167c6a..642f1b1b1bb 100644
--- a/tests/components/pushover/test_config_flow.py
+++ b/tests/components/pushover/test_config_flow.py
@@ -140,19 +140,6 @@ async def test_flow_conn_err(hass: HomeAssistant, mock_pushover: MagicMock) -> N
     assert result["errors"] == {"base": "cannot_connect"}
 
 
-async def test_import(hass: HomeAssistant) -> None:
-    """Test user initialized flow with unreachable server."""
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN,
-        context={"source": config_entries.SOURCE_IMPORT},
-        data=MOCK_CONFIG,
-    )
-
-    assert result["type"] == FlowResultType.CREATE_ENTRY
-    assert result["title"] == "Pushover"
-    assert result["data"] == MOCK_CONFIG
-
-
 async def test_reauth_success(hass: HomeAssistant) -> None:
     """Test we can reauth."""
     entry = MockConfigEntry(
diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py
index ff85e88f07e..635aec520b5 100644
--- a/tests/components/pushover/test_init.py
+++ b/tests/components/pushover/test_init.py
@@ -52,10 +52,10 @@ async def test_setup(
         },
     )
     await hass.async_block_till_done()
-    assert hass.config_entries.async_entries(DOMAIN)
+    assert not hass.config_entries.async_entries(DOMAIN)
     issues = await get_repairs(hass, hass_ws_client)
     assert len(issues) == 1
-    assert issues[0]["issue_id"] == "deprecated_yaml"
+    assert issues[0]["issue_id"] == "removed_yaml"
 
 
 async def test_async_setup_entry_success(
-- 
GitLab


From 64eb316908f26c023d7f787b3d655c968e08cdad Mon Sep 17 00:00:00 2001
From: On Freund <onfreund@gmail.com>
Date: Tue, 25 Oct 2022 14:43:09 +0300
Subject: [PATCH 792/985] Add alarmed binary sensor to Risco integration
 (#77315)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 homeassistant/components/risco/__init__.py    | 11 +--
 .../components/risco/binary_sensor.py         | 72 ++++++++++++++-----
 tests/components/risco/test_binary_sensor.py  | 49 +++++++++++++
 3 files changed, 112 insertions(+), 20 deletions(-)

diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py
index 179ddd5cad6..0e631cc4a93 100644
--- a/homeassistant/components/risco/__init__.py
+++ b/homeassistant/components/risco/__init__.py
@@ -28,6 +28,7 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.dispatcher import async_dispatcher_send
 from homeassistant.helpers.storage import Store
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
@@ -50,7 +51,6 @@ class LocalData:
     """A data class for local data passed to the platforms."""
 
     system: RiscoLocal
-    zone_updates: dict[int, Callable[[], Any]] = field(default_factory=dict)
     partition_updates: dict[int, Callable[[], Any]] = field(default_factory=dict)
 
 
@@ -59,6 +59,11 @@ def is_local(entry: ConfigEntry) -> bool:
     return entry.data.get(CONF_TYPE) == TYPE_LOCAL
 
 
+def zone_update_signal(zone_id: int) -> str:
+    """Return a signal for the dispatch of a zone update."""
+    return f"risco_zone_update_{zone_id}"
+
+
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Set up Risco from a config entry."""
     if is_local(entry):
@@ -95,9 +100,7 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
 
     async def _zone(zone_id: int, zone: Zone) -> None:
         _LOGGER.debug("Risco zone update for %d", zone_id)
-        callback = local_data.zone_updates.get(zone_id)
-        if callback:
-            callback()
+        async_dispatcher_send(hass, zone_update_signal(zone_id))
 
     entry.async_on_unload(risco.add_zone_handler(_zone))
 
diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py
index 9f98be09f0d..bc021c2c364 100644
--- a/homeassistant/components/risco/binary_sensor.py
+++ b/homeassistant/components/risco/binary_sensor.py
@@ -1,7 +1,7 @@
 """Support for Risco alarm zones."""
 from __future__ import annotations
 
-from collections.abc import Callable, Mapping
+from collections.abc import Mapping
 from typing import Any
 
 from pyrisco.common import Zone
@@ -13,10 +13,11 @@ from homeassistant.components.binary_sensor import (
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import entity_platform
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
-from . import LocalData, RiscoDataUpdateCoordinator, is_local
+from . import LocalData, RiscoDataUpdateCoordinator, is_local, zone_update_signal
 from .const import DATA_COORDINATOR, DOMAIN
 from .entity import RiscoEntity, binary_sensor_unique_id
 
@@ -24,6 +25,10 @@ SERVICE_BYPASS_ZONE = "bypass_zone"
 SERVICE_UNBYPASS_ZONE = "unbypass_zone"
 
 
+def _unique_id_for_local(system_id: str, zone_id: int) -> str:
+    return f"{system_id}_zone_{zone_id}_local"
+
+
 async def async_setup_entry(
     hass: HomeAssistant,
     config_entry: ConfigEntry,
@@ -39,9 +44,11 @@ async def async_setup_entry(
     if is_local(config_entry):
         local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id]
         async_add_entities(
-            RiscoLocalBinarySensor(
-                local_data.system.id, zone_id, zone, local_data.zone_updates
-            )
+            RiscoLocalBinarySensor(local_data.system.id, zone_id, zone)
+            for zone_id, zone in local_data.system.zones.items()
+        )
+        async_add_entities(
+            RiscoLocalAlarmedBinarySensor(local_data.system.id, zone_id, zone)
             for zone_id, zone in local_data.system.zones.items()
         )
     else:
@@ -118,18 +125,10 @@ class RiscoLocalBinarySensor(RiscoBinarySensor):
 
     _attr_should_poll = False
 
-    def __init__(
-        self,
-        system_id: str,
-        zone_id: int,
-        zone: Zone,
-        zone_updates: dict[int, Callable[[], Any]],
-    ) -> None:
+    def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None:
         """Init the zone."""
         super().__init__(zone_id=zone_id, zone=zone)
-        self._system_id = system_id
-        self._zone_updates = zone_updates
-        self._attr_unique_id = f"{system_id}_zone_{zone_id}_local"
+        self._attr_unique_id = _unique_id_for_local(system_id, zone_id)
         self._attr_device_info = DeviceInfo(
             identifiers={(DOMAIN, self._attr_unique_id)},
             manufacturer="Risco",
@@ -138,7 +137,10 @@ class RiscoLocalBinarySensor(RiscoBinarySensor):
 
     async def async_added_to_hass(self) -> None:
         """Subscribe to updates."""
-        self._zone_updates[self._zone_id] = self.async_write_ha_state
+        signal = zone_update_signal(self._zone_id)
+        self.async_on_remove(
+            async_dispatcher_connect(self.hass, signal, self.async_write_ha_state)
+        )
 
     @property
     def extra_state_attributes(self) -> Mapping[str, Any] | None:
@@ -150,3 +152,41 @@ class RiscoLocalBinarySensor(RiscoBinarySensor):
 
     async def _bypass(self, bypass: bool) -> None:
         await self._zone.bypass(bypass)
+
+
+class RiscoLocalAlarmedBinarySensor(BinarySensorEntity):
+    """Representation whether a zone in Risco local is currently triggering an alarm."""
+
+    _attr_should_poll = False
+
+    def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None:
+        """Init the zone."""
+        super().__init__()
+        self._zone_id = zone_id
+        self._zone = zone
+        self._attr_has_entity_name = True
+        self._attr_name = "Alarmed"
+        device_unique_id = _unique_id_for_local(system_id, zone_id)
+        self._attr_unique_id = device_unique_id + "_alarmed"
+        self._attr_device_info = DeviceInfo(
+            identifiers={(DOMAIN, device_unique_id)},
+            manufacturer="Risco",
+            name=self._zone.name,
+        )
+
+    async def async_added_to_hass(self) -> None:
+        """Subscribe to updates."""
+        signal = zone_update_signal(self._zone_id)
+        self.async_on_remove(
+            async_dispatcher_connect(self.hass, signal, self.async_write_ha_state)
+        )
+
+    @property
+    def extra_state_attributes(self) -> Mapping[str, Any] | None:
+        """Return the state attributes."""
+        return {"zone_id": self._zone_id}
+
+    @property
+    def is_on(self) -> bool | None:
+        """Return true if sensor is on."""
+        return self._zone.alarmed
diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py
index 2325d88c03f..71cbd04f391 100644
--- a/tests/components/risco/test_binary_sensor.py
+++ b/tests/components/risco/test_binary_sensor.py
@@ -13,6 +13,8 @@ from .util import TEST_SITE_UUID, zone_mock
 
 FIRST_ENTITY_ID = "binary_sensor.zone_0"
 SECOND_ENTITY_ID = "binary_sensor.zone_1"
+FIRST_ALARMED_ENTITY_ID = FIRST_ENTITY_ID + "_alarmed"
+SECOND_ALARMED_ENTITY_ID = SECOND_ENTITY_ID + "_alarmed"
 
 
 @pytest.fixture
@@ -23,10 +25,14 @@ def two_zone_local():
         zone_mocks[0], "id", new_callable=PropertyMock(return_value=0)
     ), patch.object(
         zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0")
+    ), patch.object(
+        zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False)
     ), patch.object(
         zone_mocks[1], "id", new_callable=PropertyMock(return_value=1)
     ), patch.object(
         zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1")
+    ), patch.object(
+        zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False)
     ), patch(
         "homeassistant.components.risco.RiscoLocal.partitions",
         new_callable=PropertyMock(return_value={}),
@@ -126,6 +132,8 @@ async def test_error_on_connect(hass, connect_with_error, local_config_entry):
     registry = er.async_get(hass)
     assert not registry.async_is_registered(FIRST_ENTITY_ID)
     assert not registry.async_is_registered(SECOND_ENTITY_ID)
+    assert not registry.async_is_registered(FIRST_ALARMED_ENTITY_ID)
+    assert not registry.async_is_registered(SECOND_ALARMED_ENTITY_ID)
 
 
 async def test_local_setup(hass, two_zone_local, setup_risco_local):
@@ -133,6 +141,8 @@ async def test_local_setup(hass, two_zone_local, setup_risco_local):
     registry = er.async_get(hass)
     assert registry.async_is_registered(FIRST_ENTITY_ID)
     assert registry.async_is_registered(SECOND_ENTITY_ID)
+    assert registry.async_is_registered(FIRST_ALARMED_ENTITY_ID)
+    assert registry.async_is_registered(SECOND_ALARMED_ENTITY_ID)
 
     registry = dr.async_get(hass)
     device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0_local")})
@@ -157,6 +167,7 @@ async def _check_local_state(
         new_callable=PropertyMock(return_value=bypassed),
     ):
         await callback(zone_id, zones[zone_id])
+        await hass.async_block_till_done()
 
         expected_triggered = STATE_ON if triggered else STATE_OFF
         assert hass.states.get(entity_id).state == expected_triggered
@@ -164,6 +175,22 @@ async def _check_local_state(
         assert hass.states.get(entity_id).attributes["zone_id"] == zone_id
 
 
+async def _check_alarmed_local_state(
+    hass, zones, alarmed, entity_id, zone_id, callback
+):
+    with patch.object(
+        zones[zone_id],
+        "alarmed",
+        new_callable=PropertyMock(return_value=alarmed),
+    ):
+        await callback(zone_id, zones[zone_id])
+        await hass.async_block_till_done()
+
+        expected_alarmed = STATE_ON if alarmed else STATE_OFF
+        assert hass.states.get(entity_id).state == expected_alarmed
+        assert hass.states.get(entity_id).attributes["zone_id"] == zone_id
+
+
 @pytest.fixture
 def _mock_zone_handler():
     with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock:
@@ -204,6 +231,28 @@ async def test_local_states(
     )
 
 
+async def test_alarmed_local_states(
+    hass, two_zone_local, _mock_zone_handler, setup_risco_local
+):
+    """Test the various alarm states."""
+    callback = _mock_zone_handler.call_args.args[0]
+
+    assert callback is not None
+
+    await _check_alarmed_local_state(
+        hass, two_zone_local, True, FIRST_ALARMED_ENTITY_ID, 0, callback
+    )
+    await _check_alarmed_local_state(
+        hass, two_zone_local, False, FIRST_ALARMED_ENTITY_ID, 0, callback
+    )
+    await _check_alarmed_local_state(
+        hass, two_zone_local, True, SECOND_ALARMED_ENTITY_ID, 1, callback
+    )
+    await _check_alarmed_local_state(
+        hass, two_zone_local, False, SECOND_ALARMED_ENTITY_ID, 1, callback
+    )
+
+
 async def test_local_bypass(hass, two_zone_local, setup_risco_local):
     """Test bypassing a zone."""
     with patch.object(two_zone_local[0], "bypass") as mock:
-- 
GitLab


From 36bb0bbc1a7962c265147ea206998582022d69d9 Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
Date: Tue, 25 Oct 2022 13:43:40 +0200
Subject: [PATCH 793/985] Fix `integrations.json` creation, make
 `iot_standards` a list (#80945)

---
 homeassistant/components/ultraloq/__init__.py   | 1 +
 homeassistant/components/ultraloq/manifest.json | 2 +-
 homeassistant/generated/integrations.json       | 4 ++++
 script/hassfest/config_flow.py                  | 8 ++++++--
 script/hassfest/manifest.py                     | 6 +++---
 script/hassfest/model.py                        | 4 ++--
 6 files changed, 17 insertions(+), 8 deletions(-)
 create mode 100644 homeassistant/components/ultraloq/__init__.py

diff --git a/homeassistant/components/ultraloq/__init__.py b/homeassistant/components/ultraloq/__init__.py
new file mode 100644
index 00000000000..b650c59a5de
--- /dev/null
+++ b/homeassistant/components/ultraloq/__init__.py
@@ -0,0 +1 @@
+"""Virtual integration: Ultraloq."""
diff --git a/homeassistant/components/ultraloq/manifest.json b/homeassistant/components/ultraloq/manifest.json
index 53e4efc99da..4775ba6caa3 100644
--- a/homeassistant/components/ultraloq/manifest.json
+++ b/homeassistant/components/ultraloq/manifest.json
@@ -2,5 +2,5 @@
   "domain": "ultraloq",
   "name": "Ultraloq",
   "integration_type": "virtual",
-  "iot_standard": "zwave"
+  "iot_standards": ["zwave"]
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 99177d10e5a..49597366b99 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2368,6 +2368,7 @@
       "integrations": {
         "symfonisk": {
           "integration_type": "virtual",
+          "supported_by": "sonos",
           "name": "IKEA SYMFONISK"
         },
         "tradfri": {
@@ -5607,6 +5608,9 @@
       "integrations": {
         "ultraloq": {
           "integration_type": "virtual",
+          "iot_standards": [
+            "zwave"
+          ],
           "name": "Ultraloq"
         }
       }
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index fdf8ac0474d..0e055e69768 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -113,6 +113,10 @@ def _populate_brand_integrations(
             metadata["config_flow"] = integration.config_flow
         if integration.iot_class:
             metadata["iot_class"] = integration.iot_class
+        if integration.supported_by:
+            metadata["supported_by"] = integration.supported_by
+        if integration.iot_standards:
+            metadata["iot_standards"] = integration.iot_standards
         if integration.translated_name:
             integration_data["translated_name"].add(domain)
         else:
@@ -185,8 +189,8 @@ def _generate_integrations(
             if integration.integration_type == "virtual":
                 if integration.supported_by:
                     metadata["supported_by"] = integration.supported_by
-                if integration.iot_standard:
-                    metadata["iot_standard"] = integration.iot_standard
+                if integration.iot_standards:
+                    metadata["iot_standards"] = integration.iot_standards
             else:
                 metadata["config_flow"] = integration.config_flow
                 if integration.iot_class:
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index 1df017da22d..874fb069818 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -263,9 +263,9 @@ VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema(
         vol.Required("domain"): str,
         vol.Required("name"): str,
         vol.Required("integration_type"): "virtual",
-        vol.Exclusive("iot_standard", "virtual_integration"): vol.Any(
-            "homekit", "zigbee", "zwave"
-        ),
+        vol.Exclusive("iot_standards", "virtual_integration"): [
+            vol.Any("homekit", "zigbee", "zwave")
+        ],
         vol.Exclusive("supported_by", "virtual_integration"): str,
     }
 )
diff --git a/script/hassfest/model.py b/script/hassfest/model.py
index 61004bae006..65d3b1144e8 100644
--- a/script/hassfest/model.py
+++ b/script/hassfest/model.py
@@ -186,9 +186,9 @@ class Integration:
         return self.manifest.get("iot_class")
 
     @property
-    def iot_standard(self) -> str:
+    def iot_standards(self) -> list[str]:
         """Return the IoT standard supported by this virtual integration."""
-        return self.manifest.get("iot_standard", {})
+        return self.manifest.get("iot_standards", [])
 
     def add_error(self, *args: Any, **kwargs: Any) -> None:
         """Add an error."""
-- 
GitLab


From 1c8156bd3337ed69124b17e00a6a4e972ea1b46e Mon Sep 17 00:00:00 2001
From: Lars <flabbamann@users.noreply.github.com>
Date: Tue, 25 Oct 2022 13:45:02 +0200
Subject: [PATCH 794/985] Update Fritz! lights to use kelvin (#79733)

---
 homeassistant/components/fritzbox/light.py | 25 +++++++++-------------
 tests/components/fritzbox/test_light.py    | 23 ++++++++++++++------
 2 files changed, 26 insertions(+), 22 deletions(-)

diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py
index e01fb76ec11..0f7037843d8 100644
--- a/homeassistant/components/fritzbox/light.py
+++ b/homeassistant/components/fritzbox/light.py
@@ -7,7 +7,7 @@ from requests.exceptions import HTTPError
 
 from homeassistant.components.light import (
     ATTR_BRIGHTNESS,
-    ATTR_COLOR_TEMP,
+    ATTR_COLOR_TEMP_KELVIN,
     ATTR_HS_COLOR,
     ColorMode,
     LightEntity,
@@ -15,7 +15,6 @@ from homeassistant.components.light import (
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util import color
 
 from . import FritzBoxEntity
 from .const import (
@@ -67,17 +66,13 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
         coordinator: FritzboxDataUpdateCoordinator,
         ain: str,
         supported_colors: dict,
-        supported_color_temps: list[str],
+        supported_color_temps: list[int],
     ) -> None:
         """Initialize the FritzboxLight entity."""
         super().__init__(coordinator, ain, None)
 
-        max_kelvin = int(max(supported_color_temps))
-        min_kelvin = int(min(supported_color_temps))
-
-        # max kelvin is min mireds and min kelvin is max mireds
-        self._attr_min_mireds = color.color_temperature_kelvin_to_mired(max_kelvin)
-        self._attr_max_mireds = color.color_temperature_kelvin_to_mired(min_kelvin)
+        self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
+        self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
 
         # Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
         # Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
@@ -112,13 +107,12 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
         return (hue, float(saturation) * 100.0 / 255.0)
 
     @property
-    def color_temp(self) -> int | None:
+    def color_temp_kelvin(self) -> int | None:
         """Return the CT color value."""
         if self.device.color_mode != COLOR_TEMP_MODE:
             return None
 
-        kelvin = self.device.color_temp
-        return color.color_temperature_kelvin_to_mired(kelvin)
+        return self.device.color_temp  # type: ignore [no-any-return]
 
     @property
     def color_mode(self) -> ColorMode:
@@ -166,9 +160,10 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
                     self.device.set_color, (hue, saturation)
                 )
 
-        if kwargs.get(ATTR_COLOR_TEMP) is not None:
-            kelvin = color.color_temperature_kelvin_to_mired(kwargs[ATTR_COLOR_TEMP])
-            await self.hass.async_add_executor_job(self.device.set_color_temp, kelvin)
+        if kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None:
+            await self.hass.async_add_executor_job(
+                self.device.set_color_temp, kwargs[ATTR_COLOR_TEMP_KELVIN]
+            )
 
         await self.hass.async_add_executor_job(self.device.set_state_on)
         await self.coordinator.async_refresh()
diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py
index 0759beb6849..8078722246e 100644
--- a/tests/components/fritzbox/test_light.py
+++ b/tests/components/fritzbox/test_light.py
@@ -1,6 +1,6 @@
 """Tests for AVM Fritz!Box light component."""
 from datetime import timedelta
-from unittest.mock import Mock
+from unittest.mock import Mock, call
 
 from requests.exceptions import HTTPError
 
@@ -11,8 +11,10 @@ from homeassistant.components.fritzbox.const import (
 )
 from homeassistant.components.light import (
     ATTR_BRIGHTNESS,
-    ATTR_COLOR_TEMP,
+    ATTR_COLOR_TEMP_KELVIN,
     ATTR_HS_COLOR,
+    ATTR_MAX_COLOR_TEMP_KELVIN,
+    ATTR_MIN_COLOR_TEMP_KELVIN,
     DOMAIN,
 )
 from homeassistant.const import (
@@ -24,7 +26,6 @@ from homeassistant.const import (
     STATE_ON,
 )
 from homeassistant.core import HomeAssistant
-from homeassistant.util import color
 import homeassistant.util.dt as dt_util
 
 from . import FritzDeviceLightMock, setup_config_entry
@@ -53,9 +54,9 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
     assert state
     assert state.state == STATE_ON
     assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name"
-    assert state.attributes[ATTR_COLOR_TEMP] == color.color_temperature_kelvin_to_mired(
-        2700
-    )
+    assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 2700
+    assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2700
+    assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6500
 
 
 async def test_setup_color(hass: HomeAssistant, fritz: Mock):
@@ -95,12 +96,14 @@ async def test_turn_on(hass: HomeAssistant, fritz: Mock):
     assert await hass.services.async_call(
         DOMAIN,
         SERVICE_TURN_ON,
-        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP: 300},
+        {ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP_KELVIN: 3000},
         True,
     )
     assert device.set_state_on.call_count == 1
     assert device.set_level.call_count == 1
     assert device.set_color_temp.call_count == 1
+    assert device.set_color_temp.call_args_list == [call(3000)]
+    assert device.set_level.call_args_list == [call(100)]
 
 
 async def test_turn_on_color(hass: HomeAssistant, fritz: Mock):
@@ -122,6 +125,10 @@ async def test_turn_on_color(hass: HomeAssistant, fritz: Mock):
     assert device.set_state_on.call_count == 1
     assert device.set_level.call_count == 1
     assert device.set_unmapped_color.call_count == 1
+    assert device.set_level.call_args_list == [call(100)]
+    assert device.set_unmapped_color.call_args_list == [
+        call((100, round(70 * 255.0 / 100.0)))
+    ]
 
 
 async def test_turn_on_color_unsupported_api_method(hass: HomeAssistant, fritz: Mock):
@@ -150,6 +157,8 @@ async def test_turn_on_color_unsupported_api_method(hass: HomeAssistant, fritz:
     assert device.set_state_on.call_count == 1
     assert device.set_level.call_count == 1
     assert device.set_color.call_count == 1
+    assert device.set_level.call_args_list == [call(100)]
+    assert device.set_color.call_args_list == [call((100, 70))]
 
 
 async def test_turn_off(hass: HomeAssistant, fritz: Mock):
-- 
GitLab


From 93d4d02aac89a89a374b25a3f8ca6df3e6ec4dfb Mon Sep 17 00:00:00 2001
From: mtdcr <obi@saftware.de>
Date: Tue, 25 Oct 2022 14:38:42 +0200
Subject: [PATCH 795/985] Remove myself from edl21 codeowners (#80947)

---
 CODEOWNERS                                   | 1 -
 homeassistant/components/edl21/manifest.json | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/CODEOWNERS b/CODEOWNERS
index f4a311db29a..35faab87e86 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -285,7 +285,6 @@ build.json @home-assistant/supervisor
 /homeassistant/components/ecovacs/ @OverloadUT @mib1185
 /homeassistant/components/ecowitt/ @pvizeli
 /tests/components/ecowitt/ @pvizeli
-/homeassistant/components/edl21/ @mtdcr
 /homeassistant/components/efergy/ @tkdrob
 /tests/components/efergy/ @tkdrob
 /homeassistant/components/egardia/ @jeroenterheerdt
diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json
index cac35a10152..4a8e209d3a0 100644
--- a/homeassistant/components/edl21/manifest.json
+++ b/homeassistant/components/edl21/manifest.json
@@ -3,7 +3,7 @@
   "name": "EDL21",
   "documentation": "https://www.home-assistant.io/integrations/edl21",
   "requirements": ["pysml==0.0.8"],
-  "codeowners": ["@mtdcr"],
+  "codeowners": [],
   "iot_class": "local_push",
   "loggers": ["sml"]
 }
-- 
GitLab


From b07e1281da06ac006cd84713fe719238bcc5145f Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Tue, 25 Oct 2022 14:53:59 +0200
Subject: [PATCH 796/985] Add rules for converting speeds (#80943)

* Add rules for converting speeds

* Update metoffice wind speed sensors to prefer mph

* Don't convert speeds measured in knots
---
 homeassistant/components/metoffice/sensor.py |  2 ++
 homeassistant/util/unit_system.py            |  8 ++++++++
 tests/util/test_unit_system.py               | 20 ++++++++++++++++++++
 3 files changed, 30 insertions(+)

diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py
index 82a6712af90..ef9643be96a 100644
--- a/homeassistant/components/metoffice/sensor.py
+++ b/homeassistant/components/metoffice/sensor.py
@@ -83,6 +83,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key="wind_speed",
         name="Wind speed",
         native_unit_of_measurement=SPEED_MILES_PER_HOUR,
+        suggested_unit_of_measurement=SPEED_MILES_PER_HOUR,
         device_class=SensorDeviceClass.SPEED,
         icon="mdi:weather-windy",
         entity_registry_enabled_default=True,
@@ -98,6 +99,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key="wind_gust",
         name="Wind gust",
         native_unit_of_measurement=SPEED_MILES_PER_HOUR,
+        suggested_unit_of_measurement=SPEED_MILES_PER_HOUR,
         device_class=SensorDeviceClass.SPEED,
         icon="mdi:weather-windy",
         entity_registry_enabled_default=False,
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 72779fe8c84..9e1c20022b2 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -27,6 +27,8 @@ from homeassistant.const import (
     PRESSURE,
     PRESSURE_PA,
     PRESSURE_PSI,
+    SPEED_FEET_PER_SECOND,
+    SPEED_KILOMETERS_PER_HOUR,
     SPEED_METERS_PER_SECOND,
     SPEED_MILES_PER_HOUR,
     TEMP_CELSIUS,
@@ -268,6 +270,9 @@ METRIC_SYSTEM = UnitSystem(
         ("distance", LENGTH_INCHES): LENGTH_MILLIMETERS,
         ("distance", LENGTH_MILES): LENGTH_KILOMETERS,
         ("distance", LENGTH_YARD): LENGTH_METERS,
+        # Convert non-metric speeds except knots to km/h
+        ("speed", SPEED_FEET_PER_SECOND): SPEED_KILOMETERS_PER_HOUR,
+        ("speed", SPEED_MILES_PER_HOUR): SPEED_KILOMETERS_PER_HOUR,
     },
     length=LENGTH_KILOMETERS,
     mass=MASS_GRAMS,
@@ -286,6 +291,9 @@ US_CUSTOMARY_SYSTEM = UnitSystem(
         ("distance", LENGTH_KILOMETERS): LENGTH_MILES,
         ("distance", LENGTH_METERS): LENGTH_FEET,
         ("distance", LENGTH_MILLIMETERS): LENGTH_INCHES,
+        # Convert non-USCS speeds except knots to mph
+        ("speed", SPEED_METERS_PER_SECOND): SPEED_MILES_PER_HOUR,
+        ("speed", SPEED_KILOMETERS_PER_HOUR): SPEED_MILES_PER_HOUR,
     },
     length=LENGTH_MILES,
     mass=MASS_POUNDS,
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index dd584a8dd3b..82d13ae6baa 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -16,7 +16,11 @@ from homeassistant.const import (
     MASS_GRAMS,
     PRESSURE,
     PRESSURE_PA,
+    SPEED_FEET_PER_SECOND,
+    SPEED_KILOMETERS_PER_HOUR,
+    SPEED_KNOTS,
     SPEED_METERS_PER_SECOND,
+    SPEED_MILES_PER_HOUR,
     TEMP_CELSIUS,
     TEMPERATURE,
     VOLUME,
@@ -384,18 +388,34 @@ def test_get_unit_system_invalid(key: str) -> None:
 @pytest.mark.parametrize(
     "unit_system, device_class, original_unit, state_unit",
     (
+        # Test distance conversion
         (METRIC_SYSTEM, "distance", LENGTH_FEET, LENGTH_METERS),
         (METRIC_SYSTEM, "distance", LENGTH_INCHES, LENGTH_MILLIMETERS),
         (METRIC_SYSTEM, "distance", LENGTH_MILES, LENGTH_KILOMETERS),
         (METRIC_SYSTEM, "distance", LENGTH_YARD, LENGTH_METERS),
         (METRIC_SYSTEM, "distance", LENGTH_KILOMETERS, None),
         (METRIC_SYSTEM, "distance", "very_long", None),
+        # Test speed conversion
+        (METRIC_SYSTEM, "speed", SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR),
+        (METRIC_SYSTEM, "speed", SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR),
+        (METRIC_SYSTEM, "speed", SPEED_KILOMETERS_PER_HOUR, None),
+        (METRIC_SYSTEM, "speed", SPEED_KNOTS, None),
+        (METRIC_SYSTEM, "speed", SPEED_METERS_PER_SECOND, None),
+        (METRIC_SYSTEM, "speed", "very_fast", None),
+        # Test distance conversion
         (US_CUSTOMARY_SYSTEM, "distance", LENGTH_CENTIMETERS, LENGTH_INCHES),
         (US_CUSTOMARY_SYSTEM, "distance", LENGTH_KILOMETERS, LENGTH_MILES),
         (US_CUSTOMARY_SYSTEM, "distance", LENGTH_METERS, LENGTH_FEET),
         (US_CUSTOMARY_SYSTEM, "distance", LENGTH_MILLIMETERS, LENGTH_INCHES),
         (US_CUSTOMARY_SYSTEM, "distance", LENGTH_MILES, None),
         (US_CUSTOMARY_SYSTEM, "distance", "very_long", None),
+        # Test speed conversion
+        (US_CUSTOMARY_SYSTEM, "speed", SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR),
+        (US_CUSTOMARY_SYSTEM, "speed", SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR),
+        (US_CUSTOMARY_SYSTEM, "speed", SPEED_FEET_PER_SECOND, None),
+        (US_CUSTOMARY_SYSTEM, "speed", SPEED_KNOTS, None),
+        (US_CUSTOMARY_SYSTEM, "speed", SPEED_MILES_PER_HOUR, None),
+        (US_CUSTOMARY_SYSTEM, "speed", "very_fast", None),
     ),
 )
 def test_get_converted_unit(
-- 
GitLab


From 3aa64aaaf1f699b754306faaf0a324b3938ffc76 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Tue, 25 Oct 2022 15:30:46 +0200
Subject: [PATCH 797/985] Mark some integrations as system integrations
 (#80948)

---
 homeassistant/components/cloud/manifest.json  |  3 +-
 .../homeassistant_alerts/manifest.json        |  3 +-
 homeassistant/components/map/manifest.json    |  3 +-
 .../persistent_notification/manifest.json     |  3 +-
 homeassistant/components/stream/manifest.json |  3 +-
 .../components/system_log/manifest.json       |  3 +-
 homeassistant/components/zone/manifest.json   |  3 +-
 homeassistant/generated/integrations.json     | 38 -------------------
 8 files changed, 14 insertions(+), 45 deletions(-)

diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json
index 3a6d942f5ea..97f581d3bf0 100644
--- a/homeassistant/components/cloud/manifest.json
+++ b/homeassistant/components/cloud/manifest.json
@@ -7,5 +7,6 @@
   "after_dependencies": ["google_assistant", "alexa"],
   "codeowners": ["@home-assistant/cloud"],
   "iot_class": "cloud_push",
-  "loggers": ["hass_nabucasa"]
+  "loggers": ["hass_nabucasa"],
+  "integration_type": "system"
 }
diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json
index 20e6447dadf..d6f6d9ab614 100644
--- a/homeassistant/components/homeassistant_alerts/manifest.json
+++ b/homeassistant/components/homeassistant_alerts/manifest.json
@@ -4,5 +4,6 @@
   "config_flow": false,
   "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts",
   "codeowners": ["@home-assistant/core"],
-  "quality_scale": "internal"
+  "quality_scale": "internal",
+  "integration_type": "system"
 }
diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json
index f78dcfd20ba..ed45ab069fa 100644
--- a/homeassistant/components/map/manifest.json
+++ b/homeassistant/components/map/manifest.json
@@ -4,5 +4,6 @@
   "documentation": "https://www.home-assistant.io/integrations/map",
   "dependencies": ["frontend"],
   "codeowners": [],
-  "quality_scale": "internal"
+  "quality_scale": "internal",
+  "integration_type": "system"
 }
diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json
index c21e8150d8a..c60746e35b1 100644
--- a/homeassistant/components/persistent_notification/manifest.json
+++ b/homeassistant/components/persistent_notification/manifest.json
@@ -4,5 +4,6 @@
   "documentation": "https://www.home-assistant.io/integrations/persistent_notification",
   "codeowners": ["@home-assistant/core"],
   "quality_scale": "internal",
-  "iot_class": "local_push"
+  "iot_class": "local_push",
+  "integration_type": "system"
 }
diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json
index fad6b80c3fb..1f79da20542 100644
--- a/homeassistant/components/stream/manifest.json
+++ b/homeassistant/components/stream/manifest.json
@@ -6,5 +6,6 @@
   "dependencies": ["http"],
   "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"],
   "quality_scale": "internal",
-  "iot_class": "local_push"
+  "iot_class": "local_push",
+  "integration_type": "system"
 }
diff --git a/homeassistant/components/system_log/manifest.json b/homeassistant/components/system_log/manifest.json
index d31ce0d8485..abbc637037b 100644
--- a/homeassistant/components/system_log/manifest.json
+++ b/homeassistant/components/system_log/manifest.json
@@ -4,5 +4,6 @@
   "documentation": "https://www.home-assistant.io/integrations/system_log",
   "dependencies": [],
   "codeowners": [],
-  "quality_scale": "internal"
+  "quality_scale": "internal",
+  "integration_type": "system"
 }
diff --git a/homeassistant/components/zone/manifest.json b/homeassistant/components/zone/manifest.json
index 019049a3b71..fe039817c64 100644
--- a/homeassistant/components/zone/manifest.json
+++ b/homeassistant/components/zone/manifest.json
@@ -4,5 +4,6 @@
   "config_flow": false,
   "documentation": "https://www.home-assistant.io/integrations/zone",
   "codeowners": ["@home-assistant/core"],
-  "quality_scale": "internal"
+  "quality_scale": "internal",
+  "integration_type": "system"
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 49597366b99..a26114843b6 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -758,12 +758,6 @@
         }
       }
     },
-    "cloud": {
-      "name": "Home Assistant Cloud",
-      "integration_type": "hub",
-      "config_flow": false,
-      "iot_class": "cloud_push"
-    },
     "cloudflare": {
       "name": "Cloudflare",
       "integration_type": "hub",
@@ -2182,11 +2176,6 @@
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
-    "homeassistant_alerts": {
-      "name": "Home Assistant Alerts",
-      "integration_type": "hub",
-      "config_flow": false
-    },
     "homeassistant_sky_connect": {
       "name": "Home Assistant Sky Connect",
       "integration_type": "hardware",
@@ -2972,11 +2961,6 @@
       "config_flow": false,
       "iot_class": "calculated"
     },
-    "map": {
-      "name": "Map",
-      "integration_type": "hub",
-      "config_flow": false
-    },
     "marantz": {
       "name": "Marantz",
       "integration_type": "virtual",
@@ -3921,12 +3905,6 @@
       "config_flow": false,
       "iot_class": "local_polling"
     },
-    "persistent_notification": {
-      "name": "Persistent Notification",
-      "integration_type": "hub",
-      "config_flow": false,
-      "iot_class": "local_push"
-    },
     "philips": {
       "name": "Philips",
       "integrations": {
@@ -5069,12 +5047,6 @@
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
-    "stream": {
-      "name": "Stream",
-      "integration_type": "hub",
-      "config_flow": false,
-      "iot_class": "local_push"
-    },
     "streamlabswater": {
       "name": "StreamLabs",
       "integration_type": "hub",
@@ -5197,11 +5169,6 @@
       "config_flow": true,
       "iot_class": "local_push"
     },
-    "system_log": {
-      "name": "System Log",
-      "integration_type": "hub",
-      "config_flow": false
-    },
     "systemmonitor": {
       "name": "System Monitor",
       "integration_type": "hub",
@@ -6191,11 +6158,6 @@
       "config_flow": false,
       "iot_class": "local_polling"
     },
-    "zone": {
-      "name": "Zone",
-      "integration_type": "hub",
-      "config_flow": false
-    },
     "zoneminder": {
       "name": "ZoneMinder",
       "integration_type": "hub",
-- 
GitLab


From 001893914203175cef049bef1ea934ef1197cef3 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 25 Oct 2022 15:56:18 +0200
Subject: [PATCH 798/985] Adjust unit_system type hints (#80946)

---
 homeassistant/util/unit_system.py |  2 +-
 tests/util/test_unit_system.py    | 93 ++++++++++++++++++++++---------
 2 files changed, 67 insertions(+), 28 deletions(-)

diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 9e1c20022b2..1ab1004e676 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -100,7 +100,7 @@ class UnitSystem:
         name: str,
         *,
         accumulated_precipitation: str,
-        conversions: dict[tuple[str | None, str | None], str],
+        conversions: dict[tuple[SensorDeviceClass | str | None, str | None], str],
         length: str,
         mass: str,
         pressure: str,
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 82d13ae6baa..fc8da540d3d 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -1,6 +1,9 @@
 """Test the unit system helper."""
+from __future__ import annotations
+
 import pytest
 
+from homeassistant.components.sensor import SensorDeviceClass
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
     LENGTH,
@@ -388,38 +391,74 @@ def test_get_unit_system_invalid(key: str) -> None:
 @pytest.mark.parametrize(
     "unit_system, device_class, original_unit, state_unit",
     (
-        # Test distance conversion
-        (METRIC_SYSTEM, "distance", LENGTH_FEET, LENGTH_METERS),
-        (METRIC_SYSTEM, "distance", LENGTH_INCHES, LENGTH_MILLIMETERS),
-        (METRIC_SYSTEM, "distance", LENGTH_MILES, LENGTH_KILOMETERS),
-        (METRIC_SYSTEM, "distance", LENGTH_YARD, LENGTH_METERS),
-        (METRIC_SYSTEM, "distance", LENGTH_KILOMETERS, None),
-        (METRIC_SYSTEM, "distance", "very_long", None),
+        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_FEET, LENGTH_METERS),
+        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_INCHES, LENGTH_MILLIMETERS),
+        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_MILES, LENGTH_KILOMETERS),
+        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_YARD, LENGTH_METERS),
+        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_KILOMETERS, None),
+        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, "very_long", None),
         # Test speed conversion
-        (METRIC_SYSTEM, "speed", SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR),
-        (METRIC_SYSTEM, "speed", SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR),
-        (METRIC_SYSTEM, "speed", SPEED_KILOMETERS_PER_HOUR, None),
-        (METRIC_SYSTEM, "speed", SPEED_KNOTS, None),
-        (METRIC_SYSTEM, "speed", SPEED_METERS_PER_SECOND, None),
-        (METRIC_SYSTEM, "speed", "very_fast", None),
-        # Test distance conversion
-        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_CENTIMETERS, LENGTH_INCHES),
-        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_KILOMETERS, LENGTH_MILES),
-        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_METERS, LENGTH_FEET),
-        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_MILLIMETERS, LENGTH_INCHES),
-        (US_CUSTOMARY_SYSTEM, "distance", LENGTH_MILES, None),
-        (US_CUSTOMARY_SYSTEM, "distance", "very_long", None),
+        (
+            METRIC_SYSTEM,
+            SensorDeviceClass.SPEED,
+            SPEED_FEET_PER_SECOND,
+            SPEED_KILOMETERS_PER_HOUR,
+        ),
+        (
+            METRIC_SYSTEM,
+            SensorDeviceClass.SPEED,
+            SPEED_MILES_PER_HOUR,
+            SPEED_KILOMETERS_PER_HOUR,
+        ),
+        (METRIC_SYSTEM, SensorDeviceClass.SPEED, SPEED_KILOMETERS_PER_HOUR, None),
+        (METRIC_SYSTEM, SensorDeviceClass.SPEED, SPEED_KNOTS, None),
+        (METRIC_SYSTEM, SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, None),
+        (METRIC_SYSTEM, SensorDeviceClass.SPEED, "very_fast", None),
+        (
+            US_CUSTOMARY_SYSTEM,
+            SensorDeviceClass.DISTANCE,
+            LENGTH_CENTIMETERS,
+            LENGTH_INCHES,
+        ),
+        (
+            US_CUSTOMARY_SYSTEM,
+            SensorDeviceClass.DISTANCE,
+            LENGTH_KILOMETERS,
+            LENGTH_MILES,
+        ),
+        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_METERS, LENGTH_FEET),
+        (
+            US_CUSTOMARY_SYSTEM,
+            SensorDeviceClass.DISTANCE,
+            LENGTH_MILLIMETERS,
+            LENGTH_INCHES,
+        ),
+        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_MILES, None),
+        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.DISTANCE, "very_long", None),
         # Test speed conversion
-        (US_CUSTOMARY_SYSTEM, "speed", SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR),
-        (US_CUSTOMARY_SYSTEM, "speed", SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR),
-        (US_CUSTOMARY_SYSTEM, "speed", SPEED_FEET_PER_SECOND, None),
-        (US_CUSTOMARY_SYSTEM, "speed", SPEED_KNOTS, None),
-        (US_CUSTOMARY_SYSTEM, "speed", SPEED_MILES_PER_HOUR, None),
-        (US_CUSTOMARY_SYSTEM, "speed", "very_fast", None),
+        (
+            US_CUSTOMARY_SYSTEM,
+            SensorDeviceClass.SPEED,
+            SPEED_METERS_PER_SECOND,
+            SPEED_MILES_PER_HOUR,
+        ),
+        (
+            US_CUSTOMARY_SYSTEM,
+            SensorDeviceClass.SPEED,
+            SPEED_KILOMETERS_PER_HOUR,
+            SPEED_MILES_PER_HOUR,
+        ),
+        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, SPEED_FEET_PER_SECOND, None),
+        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, SPEED_KNOTS, None),
+        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, None),
+        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, "very_fast", None),
     ),
 )
 def test_get_converted_unit(
-    unit_system, device_class, original_unit, state_unit
+    unit_system: UnitSystem,
+    device_class: SensorDeviceClass,
+    original_unit: str,
+    state_unit: str | None,
 ) -> None:
     """Test unit conversion rules."""
     assert unit_system.get_converted_unit(device_class, original_unit) == state_unit
-- 
GitLab


From e9a3982560728884ff8d718d6ae6311755d21d64 Mon Sep 17 00:00:00 2001
From: shbatm <support@shbatm.com>
Date: Tue, 25 Oct 2022 09:07:31 -0500
Subject: [PATCH 799/985] Add additional sensors to RainMachine (#80914)

---
 .../components/rainmachine/sensor.py          | 106 +++++++++++++-----
 1 file changed, 80 insertions(+), 26 deletions(-)

diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py
index 97772c6033a..191ace64625 100644
--- a/homeassistant/components/rainmachine/sensor.py
+++ b/homeassistant/components/rainmachine/sensor.py
@@ -14,11 +14,11 @@ from homeassistant.components.sensor import (
     SensorStateClass,
 )
 from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS
+from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS, VOLUME_LITERS
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.util.dt import utcnow
+from homeassistant.util.dt import utc_from_timestamp, utcnow
 
 from . import RainMachineData, RainMachineEntity
 from .const import (
@@ -45,10 +45,14 @@ DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5)
 
 TYPE_FLOW_SENSOR_CLICK_M3 = "flow_sensor_clicks_cubic_meter"
 TYPE_FLOW_SENSOR_CONSUMED_LITERS = "flow_sensor_consumed_liters"
+TYPE_FLOW_SENSOR_LEAK_CLICKS = "flow_sensor_leak_clicks"
+TYPE_FLOW_SENSOR_LEAK_VOLUME = "flow_sensor_leak_volume"
 TYPE_FLOW_SENSOR_START_INDEX = "flow_sensor_start_index"
 TYPE_FLOW_SENSOR_WATERING_CLICKS = "flow_sensor_watering_clicks"
 TYPE_FREEZE_TEMP = "freeze_protect_temp"
+TYPE_LAST_LEAK_DETECTED = "last_leak_detected"
 TYPE_PROGRAM_RUN_COMPLETION_TIME = "program_run_completion_time"
+TYPE_RAIN_SENSOR_RAIN_START = "rain_sensor_rain_start"
 TYPE_ZONE_RUN_COMPLETION_TIME = "zone_run_completion_time"
 
 
@@ -67,7 +71,7 @@ class RainMachineSensorCompletionTimerDescription(
     RainMachineEntityDescription,
     RainMachineEntityDescriptionMixinUid,
 ):
-    """Describe a RainMachine sensor."""
+    """Describe a RainMachine completion timer sensor."""
 
 
 SENSOR_DESCRIPTIONS = (
@@ -87,12 +91,34 @@ SENSOR_DESCRIPTIONS = (
         name="Flow sensor consumed liters",
         icon="mdi:water-pump",
         entity_category=EntityCategory.DIAGNOSTIC,
-        native_unit_of_measurement="liter",
+        native_unit_of_measurement=VOLUME_LITERS,
         entity_registry_enabled_default=False,
         state_class=SensorStateClass.TOTAL_INCREASING,
         api_category=DATA_PROVISION_SETTINGS,
         data_key="flowSensorWateringClicks",
     ),
+    RainMachineSensorDataDescription(
+        key=TYPE_FLOW_SENSOR_LEAK_CLICKS,
+        name="Flow sensor leak clicks",
+        icon="mdi:pipe-leak",
+        entity_category=EntityCategory.DIAGNOSTIC,
+        native_unit_of_measurement="clicks",
+        entity_registry_enabled_default=False,
+        state_class=SensorStateClass.TOTAL_INCREASING,
+        api_category=DATA_PROVISION_SETTINGS,
+        data_key="flowSensorLeakClicks",
+    ),
+    RainMachineSensorDataDescription(
+        key=TYPE_FLOW_SENSOR_LEAK_VOLUME,
+        name="Flow sensor leak volume",
+        icon="mdi:pipe-leak",
+        entity_category=EntityCategory.DIAGNOSTIC,
+        native_unit_of_measurement=VOLUME_LITERS,
+        entity_registry_enabled_default=False,
+        state_class=SensorStateClass.TOTAL_INCREASING,
+        api_category=DATA_PROVISION_SETTINGS,
+        data_key="flowSensorLeakClicks",
+    ),
     RainMachineSensorDataDescription(
         key=TYPE_FLOW_SENSOR_START_INDEX,
         name="Flow sensor start index",
@@ -110,7 +136,7 @@ SENSOR_DESCRIPTIONS = (
         entity_category=EntityCategory.DIAGNOSTIC,
         native_unit_of_measurement="clicks",
         entity_registry_enabled_default=False,
-        state_class=SensorStateClass.MEASUREMENT,
+        state_class=SensorStateClass.TOTAL_INCREASING,
         api_category=DATA_PROVISION_SETTINGS,
         data_key="flowSensorWateringClicks",
     ),
@@ -125,6 +151,28 @@ SENSOR_DESCRIPTIONS = (
         api_category=DATA_RESTRICTIONS_UNIVERSAL,
         data_key="freezeProtectTemp",
     ),
+    RainMachineSensorDataDescription(
+        key=TYPE_LAST_LEAK_DETECTED,
+        name="Last leak detected",
+        icon="mdi:pipe-leak",
+        entity_category=EntityCategory.DIAGNOSTIC,
+        entity_registry_enabled_default=False,
+        device_class=SensorDeviceClass.TIMESTAMP,
+        state_class=SensorStateClass.MEASUREMENT,
+        api_category=DATA_PROVISION_SETTINGS,
+        data_key="lastLeakDetected",
+    ),
+    RainMachineSensorDataDescription(
+        key=TYPE_RAIN_SENSOR_RAIN_START,
+        name="Rain sensor rain start",
+        icon="mdi:weather-pouring",
+        entity_category=EntityCategory.DIAGNOSTIC,
+        entity_registry_enabled_default=False,
+        device_class=SensorDeviceClass.TIMESTAMP,
+        state_class=SensorStateClass.MEASUREMENT,
+        api_category=DATA_PROVISION_SETTINGS,
+        data_key="rainSensorRainStart",
+    ),
 )
 
 
@@ -293,30 +341,36 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
     @callback
     def update_from_latest_data(self) -> None:
         """Update the state."""
-        if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3:
-            self._attr_native_value = self.coordinator.data.get("system", {}).get(
-                "flowSensorClicksPerCubicMeter"
-            )
-        elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
-            clicks = self.coordinator.data.get("system", {}).get(
-                "flowSensorWateringClicks"
-            )
-            clicks_per_m3 = self.coordinator.data.get("system", {}).get(
-                "flowSensorClicksPerCubicMeter"
-            )
+        system = self.coordinator.data.get("system", {})
+        new_value = system.get(self.entity_description.data_key)
 
-            if clicks and clicks_per_m3:
-                self._attr_native_value = (clicks * 1000) / clicks_per_m3
+        # Calculate volumetric sensors
+        if (
+            self.entity_description.key
+            in {
+                TYPE_FLOW_SENSOR_CONSUMED_LITERS,
+                TYPE_FLOW_SENSOR_LEAK_VOLUME,
+            }
+            and new_value
+        ):
+            if clicks_per_m3 := system.get("flowSensorClicksPerCubicMeter"):
+                self._attr_native_value = round((new_value * 1000) / clicks_per_m3, 1)
+                return
+
+        # Convert timestamp sensors to datetime
+        if self.entity_description.key in {
+            TYPE_LAST_LEAK_DETECTED,
+            TYPE_RAIN_SENSOR_RAIN_START,
+        }:
+            # Timestamp may return 0 instead of null, explicitly set to None
+            if new_value:
+                self._attr_native_value = utc_from_timestamp(new_value)
             else:
                 self._attr_native_value = None
-        elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX:
-            self._attr_native_value = self.coordinator.data.get("system", {}).get(
-                "flowSensorStartIndex"
-            )
-        elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS:
-            self._attr_native_value = self.coordinator.data.get("system", {}).get(
-                "flowSensorWateringClicks"
-            )
+            return
+
+        # Return all other sensor values or None
+        self._attr_native_value = new_value
 
 
 class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity):
-- 
GitLab


From 727eccfec4a537e245227bc4c581480c4ec0a145 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Tue, 25 Oct 2022 16:43:00 +0200
Subject: [PATCH 800/985] Add GJ as supported unit for energy sensors (#80870)

* Add GJ as supported unit for energy sensors

* Update homeassistant/const.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
---
 homeassistant/components/energy/sensor.py   | 17 +++++++++++++++--
 homeassistant/components/energy/validate.py |  3 +++
 homeassistant/components/sensor/__init__.py |  2 +-
 homeassistant/const.py                      |  3 ++-
 homeassistant/util/unit_conversion.py       |  3 +++
 tests/components/energy/test_sensor.py      |  3 +++
 tests/components/energy/test_validate.py    |  2 ++
 tests/util/test_unit_conversion.py          |  4 ++++
 8 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py
index db156a2d6cc..c60c5c11519 100644
--- a/homeassistant/components/energy/sensor.py
+++ b/homeassistant/components/energy/sensor.py
@@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
 from homeassistant.components.sensor.recorder import reset_detected
 from homeassistant.const import (
     ATTR_UNIT_OF_MEASUREMENT,
+    ENERGY_GIGA_JOULE,
     ENERGY_KILO_WATT_HOUR,
     ENERGY_MEGA_WATT_HOUR,
     ENERGY_WATT_HOUR,
@@ -44,7 +45,12 @@ SUPPORTED_STATE_CLASSES = [
     SensorStateClass.TOTAL,
     SensorStateClass.TOTAL_INCREASING,
 ]
-VALID_ENERGY_UNITS = [ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR]
+VALID_ENERGY_UNITS = [
+    ENERGY_WATT_HOUR,
+    ENERGY_KILO_WATT_HOUR,
+    ENERGY_MEGA_WATT_HOUR,
+    ENERGY_GIGA_JOULE,
+]
 VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
 _LOGGER = logging.getLogger(__name__)
 
@@ -233,7 +239,7 @@ class EnergyCostSensor(SensorEntity):
         self.async_write_ha_state()
 
     @callback
-    def _update_cost(self) -> None:
+    def _update_cost(self) -> None:  # noqa: C901
         """Update incurred costs."""
         energy_state = self.hass.states.get(
             cast(str, self._config[self._adapter.stat_energy_key])
@@ -289,6 +295,11 @@ class EnergyCostSensor(SensorEntity):
             ):
                 energy_price /= 1000.0
 
+            if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
+                f"/{ENERGY_GIGA_JOULE}"
+            ):
+                energy_price /= 1000 / 3.6
+
         else:
             energy_price_state = None
             energy_price = cast(float, self._config["number_energy_price"])
@@ -312,6 +323,8 @@ class EnergyCostSensor(SensorEntity):
             energy_price /= 1000
         elif energy_unit == ENERGY_MEGA_WATT_HOUR:
             energy_price *= 1000
+        elif energy_unit == ENERGY_GIGA_JOULE:
+            energy_price *= 1000 / 3.6
 
         if energy_unit is None:
             if not self._wrong_unit_reported:
diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py
index b704330fa28..17ac7227bf8 100644
--- a/homeassistant/components/energy/validate.py
+++ b/homeassistant/components/energy/validate.py
@@ -9,6 +9,7 @@ from typing import Any
 from homeassistant.components import recorder, sensor
 from homeassistant.const import (
     ATTR_DEVICE_CLASS,
+    ENERGY_GIGA_JOULE,
     ENERGY_KILO_WATT_HOUR,
     ENERGY_MEGA_WATT_HOUR,
     ENERGY_WATT_HOUR,
@@ -28,6 +29,7 @@ ENERGY_USAGE_UNITS = {
         ENERGY_KILO_WATT_HOUR,
         ENERGY_MEGA_WATT_HOUR,
         ENERGY_WATT_HOUR,
+        ENERGY_GIGA_JOULE,
     )
 }
 ENERGY_PRICE_UNITS = tuple(
@@ -44,6 +46,7 @@ GAS_USAGE_UNITS = {
         ENERGY_WATT_HOUR,
         ENERGY_KILO_WATT_HOUR,
         ENERGY_MEGA_WATT_HOUR,
+        ENERGY_GIGA_JOULE,
     ),
     sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
 }
diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index fbe12652f35..d0f8148c2e8 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -147,7 +147,7 @@ class SensorDeviceClass(StrEnum):
     ENERGY = "energy"
     """Energy.
 
-    Unit of measurement: `Wh`, `kWh`, `MWh`
+    Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ`
     """
 
     FREQUENCY = "frequency"
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 90a2dd34550..e853584df80 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -487,9 +487,10 @@ POWER_BTU_PER_HOUR: Final = "BTU/h"
 POWER_VOLT_AMPERE_REACTIVE: Final = "var"
 
 # Energy units
-ENERGY_WATT_HOUR: Final = "Wh"
+ENERGY_GIGA_JOULE: Final = "GJ"
 ENERGY_KILO_WATT_HOUR: Final = "kWh"
 ENERGY_MEGA_WATT_HOUR: Final = "MWh"
+ENERGY_WATT_HOUR: Final = "Wh"
 
 # Electric_current units
 ELECTRIC_CURRENT_MILLIAMPERE: Final = "mA"
diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py
index 0bf895f34a0..96b90065954 100644
--- a/homeassistant/util/unit_conversion.py
+++ b/homeassistant/util/unit_conversion.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 from homeassistant.const import (
+    ENERGY_GIGA_JOULE,
     ENERGY_KILO_WATT_HOUR,
     ENERGY_MEGA_WATT_HOUR,
     ENERGY_WATT_HOUR,
@@ -158,11 +159,13 @@ class EnergyConverter(BaseUnitConverter):
         ENERGY_WATT_HOUR: 1 * 1000,
         ENERGY_KILO_WATT_HOUR: 1,
         ENERGY_MEGA_WATT_HOUR: 1 / 1000,
+        ENERGY_GIGA_JOULE: 3.6 / 1000,
     }
     VALID_UNITS = {
         ENERGY_WATT_HOUR,
         ENERGY_KILO_WATT_HOUR,
         ENERGY_MEGA_WATT_HOUR,
+        ENERGY_GIGA_JOULE,
     }
 
 
diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py
index 138c2494d66..20dc533e70f 100644
--- a/tests/components/energy/test_sensor.py
+++ b/tests/components/energy/test_sensor.py
@@ -16,6 +16,7 @@ from homeassistant.components.sensor.recorder import compile_statistics
 from homeassistant.const import (
     ATTR_DEVICE_CLASS,
     ATTR_UNIT_OF_MEASUREMENT,
+    ENERGY_GIGA_JOULE,
     ENERGY_KILO_WATT_HOUR,
     ENERGY_MEGA_WATT_HOUR,
     ENERGY_WATT_HOUR,
@@ -703,6 +704,7 @@ async def test_cost_sensor_price_entity_total_no_reset(
         (ENERGY_WATT_HOUR, 1000),
         (ENERGY_KILO_WATT_HOUR, 1),
         (ENERGY_MEGA_WATT_HOUR, 0.001),
+        (ENERGY_GIGA_JOULE, 0.001 * 3.6),
     ],
 )
 async def test_cost_sensor_handle_energy_units(
@@ -768,6 +770,7 @@ async def test_cost_sensor_handle_energy_units(
         (f"EUR/{ENERGY_WATT_HOUR}", 0.001),
         (f"EUR/{ENERGY_KILO_WATT_HOUR}", 1),
         (f"EUR/{ENERGY_MEGA_WATT_HOUR}", 1000),
+        (f"EUR/{ENERGY_GIGA_JOULE}", 1000 / 3.6),
     ],
 )
 async def test_cost_sensor_handle_price_units(
diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py
index a6d1578c789..729e7685ac5 100644
--- a/tests/components/energy/test_validate.py
+++ b/tests/components/energy/test_validate.py
@@ -5,6 +5,7 @@ import pytest
 
 from homeassistant.components.energy import async_get_manager, validate
 from homeassistant.const import (
+    ENERGY_GIGA_JOULE,
     ENERGY_KILO_WATT_HOUR,
     ENERGY_MEGA_WATT_HOUR,
     ENERGY_WATT_HOUR,
@@ -73,6 +74,7 @@ async def test_validation_empty_config(hass):
         ("total", ENERGY_KILO_WATT_HOUR, {}),
         ("total", ENERGY_KILO_WATT_HOUR, {"last_reset": "abc"}),
         ("measurement", ENERGY_KILO_WATT_HOUR, {"last_reset": "abc"}),
+        ("total_increasing", ENERGY_GIGA_JOULE, {}),
     ],
 )
 async def test_validation(
diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py
index 98be32584c6..486d2199e18 100644
--- a/tests/util/test_unit_conversion.py
+++ b/tests/util/test_unit_conversion.py
@@ -2,6 +2,7 @@
 import pytest
 
 from homeassistant.const import (
+    ENERGY_GIGA_JOULE,
     ENERGY_KILO_WATT_HOUR,
     ENERGY_MEGA_WATT_HOUR,
     ENERGY_WATT_HOUR,
@@ -78,6 +79,7 @@ INVALID_SYMBOL = "bob"
         (EnergyConverter, ENERGY_WATT_HOUR),
         (EnergyConverter, ENERGY_KILO_WATT_HOUR),
         (EnergyConverter, ENERGY_MEGA_WATT_HOUR),
+        (EnergyConverter, ENERGY_GIGA_JOULE),
         (MassConverter, MASS_GRAMS),
         (MassConverter, MASS_KILOGRAMS),
         (MassConverter, MASS_MICROGRAMS),
@@ -268,6 +270,8 @@ def test_distance_convert(
         (10, ENERGY_KILO_WATT_HOUR, 0.01, ENERGY_MEGA_WATT_HOUR),
         (10, ENERGY_MEGA_WATT_HOUR, 10000000, ENERGY_WATT_HOUR),
         (10, ENERGY_MEGA_WATT_HOUR, 10000, ENERGY_KILO_WATT_HOUR),
+        (10, ENERGY_GIGA_JOULE, 10000 / 3.6, ENERGY_KILO_WATT_HOUR),
+        (10, ENERGY_GIGA_JOULE, 10 / 3.6, ENERGY_MEGA_WATT_HOUR),
     ],
 )
 def test_energy_convert(
-- 
GitLab


From 7838bb3ebe97762fd6646ba2ab49f067f4d81370 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 25 Oct 2022 16:45:45 +0200
Subject: [PATCH 801/985] Replace new PRECIPITATION_INTENSITY with enum
 (#80653)

---
 homeassistant/components/aemet/const.py       |  8 ++--
 .../components/ambient_station/sensor.py      |  4 +-
 homeassistant/components/buienradar/sensor.py |  6 +--
 homeassistant/components/ecowitt/sensor.py    |  7 ++--
 homeassistant/components/isy994/const.py      | 13 +++----
 homeassistant/components/rfxtrx/sensor.py     |  4 +-
 homeassistant/components/sensor/__init__.py   |  8 ++--
 .../components/tellduslive/sensor.py          |  4 +-
 .../zwave_js/discovery_data_template.py       |  7 ++--
 homeassistant/const.py                        | 37 +++++++++++++------
 homeassistant/util/unit_conversion.py         | 21 +++++------
 tests/util/test_unit_conversion.py            | 33 ++++++++---------
 12 files changed, 77 insertions(+), 75 deletions(-)

diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py
index 378168af4ec..46742a85beb 100644
--- a/homeassistant/components/aemet/const.py
+++ b/homeassistant/components/aemet/const.py
@@ -21,11 +21,11 @@ from homeassistant.components.weather import (
 from homeassistant.const import (
     DEGREE,
     PERCENTAGE,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     SPEED_KILOMETERS_PER_HOUR,
     TEMP_CELSIUS,
     Platform,
+    UnitOfVolumetricFlux,
 )
 
 ATTRIBUTION = "Powered by AEMET OpenData"
@@ -208,7 +208,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key=ATTR_API_FORECAST_PRECIPITATION,
         name="Precipitation",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
     ),
     SensorEntityDescription(
         key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
@@ -265,7 +265,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key=ATTR_API_RAIN,
         name="Rain",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
     ),
     SensorEntityDescription(
         key=ATTR_API_RAIN_PROB,
@@ -276,7 +276,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key=ATTR_API_SNOW,
         name="Snow",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
     ),
     SensorEntityDescription(
         key=ATTR_API_SNOW_PROB,
diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py
index da6eb143a52..48ee70f9c84 100644
--- a/homeassistant/components/ambient_station/sensor.py
+++ b/homeassistant/components/ambient_station/sensor.py
@@ -19,10 +19,10 @@ from homeassistant.const import (
     LIGHT_LUX,
     PERCENTAGE,
     PRECIPITATION_INCHES,
-    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
     PRESSURE_INHG,
     SPEED_MILES_PER_HOUR,
     TEMP_FAHRENHEIT,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.entity import EntityDescription
@@ -195,7 +195,7 @@ SENSOR_DESCRIPTIONS = (
         key=TYPE_HOURLYRAININ,
         name="Hourly rain rate",
         icon="mdi:water",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index ccd5b60132e..52e1129dfa8 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -38,10 +38,10 @@ from homeassistant.const import (
     LENGTH_KILOMETERS,
     LENGTH_MILLIMETERS,
     PERCENTAGE,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     SPEED_KILOMETERS_PER_HOUR,
     TEMP_CELSIUS,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -183,7 +183,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key="precipitation",
         name="Precipitation",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         icon="mdi:weather-pouring",
         state_class=SensorStateClass.MEASUREMENT,
     ),
@@ -197,7 +197,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
     SensorEntityDescription(
         key="precipitation_forecast_average",
         name="Precipitation forecast average",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         icon="mdi:weather-pouring",
     ),
     SensorEntityDescription(
diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py
index 8c55c8bf801..35e929ae2c2 100644
--- a/homeassistant/components/ecowitt/sensor.py
+++ b/homeassistant/components/ecowitt/sensor.py
@@ -27,8 +27,6 @@ from homeassistant.const import (
     LIGHT_LUX,
     PERCENTAGE,
     POWER_WATT,
-    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     PRESSURE_INHG,
     SPEED_KILOMETERS_PER_HOUR,
@@ -36,6 +34,7 @@ from homeassistant.const import (
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
     UV_INDEX,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -157,12 +156,12 @@ ECOWITT_SENSORS_MAPPING: Final = {
     ),
     EcoWittSensorTypes.RAIN_RATE_MM: SensorEntityDescription(
         key="RAIN_RATE_MM",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     EcoWittSensorTypes.RAIN_RATE_INCHES: SensorEntityDescription(
         key="RAIN_RATE_INCHES",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
     ),
     EcoWittSensorTypes.LIGHTNING_DISTANCE_KM: SensorEntityDescription(
diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py
index 36c1c9c5521..d9c78f904e2 100644
--- a/homeassistant/components/isy994/const.py
+++ b/homeassistant/components/isy994/const.py
@@ -37,10 +37,6 @@ from homeassistant.const import (
     PERCENTAGE,
     POWER_KILO_WATT,
     POWER_WATT,
-    PRECIPITATION_INTENSITY_INCHES_PER_DAY,
-    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     PRESSURE_INHG,
     PRESSURE_MBAR,
@@ -80,6 +76,7 @@ from homeassistant.const import (
     VOLUME_GALLONS,
     VOLUME_LITERS,
     Platform,
+    UnitOfVolumetricFlux,
 )
 
 _LOGGER = logging.getLogger(__package__)
@@ -342,7 +339,7 @@ UOM_FRIENDLY_NAME = {
     "21": "%AH",
     "22": "%RH",
     "23": PRESSURE_INHG,
-    "24": PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+    "24": UnitOfVolumetricFlux.INCHES_PER_HOUR,
     UOM_INDEX: UOM_INDEX,  # Index type. Use "node.formatted" for value
     "26": TEMP_KELVIN,
     "27": "keyword",
@@ -364,7 +361,7 @@ UOM_FRIENDLY_NAME = {
     "43": ELECTRIC_POTENTIAL_MILLIVOLT,
     "44": TIME_MINUTES,
     "45": TIME_MINUTES,
-    "46": PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+    "46": UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
     "47": TIME_MONTHS,
     "48": SPEED_MILES_PER_HOUR,
     "49": SPEED_METERS_PER_SECOND,
@@ -407,7 +404,7 @@ UOM_FRIENDLY_NAME = {
     "103": CURRENCY_DOLLAR,
     "104": CURRENCY_CENT,
     "105": LENGTH_INCHES,
-    "106": PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+    "106": UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
     "107": "",  # raw 1-byte unsigned value
     "108": "",  # raw 2-byte unsigned value
     "109": "",  # raw 3-byte unsigned value
@@ -420,7 +417,7 @@ UOM_FRIENDLY_NAME = {
     "117": PRESSURE_MBAR,
     "118": PRESSURE_HPA,
     "119": ENERGY_WATT_HOUR,
-    "120": PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+    "120": UnitOfVolumetricFlux.INCHES_PER_DAY,
 }
 
 UOM_TO_STATES = {
diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py
index 9f854924cf4..6be2d0cf725 100644
--- a/homeassistant/components/rfxtrx/sensor.py
+++ b/homeassistant/components/rfxtrx/sensor.py
@@ -25,12 +25,12 @@ from homeassistant.const import (
     LENGTH_MILLIMETERS,
     PERCENTAGE,
     POWER_WATT,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_HPA,
     SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
     SPEED_METERS_PER_SECOND,
     TEMP_CELSIUS,
     UV_INDEX,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.entity import Entity, EntityCategory
@@ -172,7 +172,7 @@ SENSOR_TYPES = (
         key="Rain rate",
         name="Rain rate",
         state_class=SensorStateClass.MEASUREMENT,
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
     ),
     RfxtrxSensorEntityDescription(
         key="Sound",
diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index d0f8148c2e8..ae384df881b 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -245,9 +245,9 @@ class SensorDeviceClass(StrEnum):
     PRECIPITATION_INTENSITY = "precipitation_intensity"
     """Precipitation intensity.
 
-    Unit of measurement:
-    - `in/d`, `in/h`
-    - `mm/d`, `mm/h`
+    Unit of measurement: UnitOfVolumetricFlux
+    - SI /metric: `mm/d`, `mm/h`
+    - USCS / imperial: `in/d`, `in/h`
     """
 
     PRESSURE = "pressure"
@@ -275,7 +275,7 @@ class SensorDeviceClass(StrEnum):
     SPEED = "speed"
     """Generic speed.
 
-    Unit of measurement: `SPEED_*` or `PRECIPITATION_INTENSITY_*` units
+    Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux`
     - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h`
     - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph`
     - Nautical: `kn`
diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py
index 192a256b48f..ecccba8b734 100644
--- a/homeassistant/components/tellduslive/sensor.py
+++ b/homeassistant/components/tellduslive/sensor.py
@@ -14,10 +14,10 @@ from homeassistant.const import (
     LIGHT_LUX,
     PERCENTAGE,
     POWER_WATT,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     SPEED_METERS_PER_SECOND,
     TEMP_CELSIUS,
     UV_INDEX,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -57,7 +57,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
     SENSOR_TYPE_RAINRATE: SensorEntityDescription(
         key=SENSOR_TYPE_RAINRATE,
         name="Rain rate",
-        native_unit_of_measurement=PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         icon="mdi:water",
         state_class=SensorStateClass.MEASUREMENT,
     ),
diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py
index 590e3965b0c..5b20572c2d8 100644
--- a/homeassistant/components/zwave_js/discovery_data_template.py
+++ b/homeassistant/components/zwave_js/discovery_data_template.py
@@ -109,8 +109,6 @@ from homeassistant.const import (
     PERCENTAGE,
     POWER_BTU_PER_HOUR,
     POWER_WATT,
-    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_INHG,
     PRESSURE_MMHG,
     PRESSURE_PSI,
@@ -127,6 +125,7 @@ from homeassistant.const import (
     VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
     VOLUME_GALLONS,
     VOLUME_LITERS,
+    UnitOfVolumetricFlux,
 )
 
 from .const import (
@@ -201,14 +200,14 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
     VOLUME_GALLONS: UNIT_GALLONS,
     FREQUENCY_HERTZ: UNIT_HERTZ,
     PRESSURE_INHG: UNIT_INCHES_OF_MERCURY,
-    PRECIPITATION_INTENSITY_INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR,
+    UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR,
     MASS_KILOGRAMS: UNIT_KILOGRAM,
     FREQUENCY_KILOHERTZ: UNIT_KILOHERTZ,
     VOLUME_LITERS: UNIT_LITER,
     LIGHT_LUX: UNIT_LUX,
     LENGTH_METERS: UNIT_METER,
     ELECTRIC_CURRENT_MILLIAMPERE: UNIT_MILLIAMPERE,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR: UNIT_MILLIMETER_HOUR,
+    UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: UNIT_MILLIMETER_HOUR,
     ELECTRIC_POTENTIAL_MILLIVOLT: UNIT_MILLIVOLT,
     SPEED_MILES_PER_HOUR: UNIT_MPH,
     SPEED_METERS_PER_SECOND: UNIT_M_S,
diff --git a/homeassistant/const.py b/homeassistant/const.py
index e853584df80..1bac1c10c5a 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -603,13 +603,26 @@ REVOLUTIONS_PER_MINUTE: Final = "rpm"
 IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²"
 IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = "BTU/(h×ft²)"
 
-# Precipitation intensity units
-# The derivation of these units is a volume of rain amassing in a container
-# with constant cross section in a given time
-PRECIPITATION_INTENSITY_INCHES_PER_DAY: Final = "in/d"
-PRECIPITATION_INTENSITY_INCHES_PER_HOUR: Final = "in/h"
-PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY: Final = "mm/d"
-PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR: Final = "mm/h"
+
+class UnitOfVolumetricFlux(StrEnum):
+    """Volumetric flux, commonly used for precipitation intensity.
+
+    The derivation of these units is a volume of rain amassing in a container
+    with constant cross section in a given time
+    """
+
+    INCHES_PER_DAY = "in/d"
+    """Derived from in³/(in².d)"""
+
+    INCHES_PER_HOUR = "in/h"
+    """Derived from in³/(in².h)"""
+
+    MILLIMETERS_PER_DAY = "mm/d"
+    """Derived from mm³/(mm².d)"""
+
+    MILLIMETERS_PER_HOUR = "mm/h"
+    """Derived from mm³/(mm².h)"""
+
 
 # Precipitation units
 # The derivation of these units is a volume of rain amassing in a container
@@ -618,10 +631,10 @@ PRECIPITATION_INCHES: Final = "in"
 PRECIPITATION_MILLIMETERS: Final = "mm"
 
 PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h"
-"""Deprecated: please use PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR"""
+"""Deprecated: please use UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR"""
 
 PRECIPITATION_INCHES_PER_HOUR: Final = "in/h"
-"""Deprecated: please use PRECIPITATION_INTENSITY_INCHES_PER_HOUR"""
+"""Deprecated: please use UnitOfVolumetricFlux.INCHES_PER_HOUR"""
 
 # Concentration units
 CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
@@ -639,13 +652,13 @@ SPEED_KNOTS: Final = "kn"
 SPEED_MILES_PER_HOUR: Final = "mph"
 
 SPEED_MILLIMETERS_PER_DAY: Final = "mm/d"
-"""Deprecated: please use PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY"""
+"""Deprecated: please use UnitOfVolumetricFlux.MILLIMETERS_PER_DAY"""
 
 SPEED_INCHES_PER_DAY: Final = "in/d"
-"""Deprecated: please use PRECIPITATION_INTENSITY_INCHES_PER_DAY"""
+"""Deprecated: please use UnitOfVolumetricFlux.INCHES_PER_DAY"""
 
 SPEED_INCHES_PER_HOUR: Final = "in/h"
-"""Deprecated: please use PRECIPITATION_INTENSITY_INCHES_PER_HOUR"""
+"""Deprecated: please use UnitOfVolumetricFlux.INCHES_PER_HOUR"""
 
 
 # Signal_strength units
diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py
index 96b90065954..cd27f79b045 100644
--- a/homeassistant/util/unit_conversion.py
+++ b/homeassistant/util/unit_conversion.py
@@ -22,10 +22,6 @@ from homeassistant.const import (
     MASS_POUNDS,
     POWER_KILO_WATT,
     POWER_WATT,
-    PRECIPITATION_INTENSITY_INCHES_PER_DAY,
-    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_BAR,
     PRESSURE_CBAR,
     PRESSURE_HPA,
@@ -50,6 +46,7 @@ from homeassistant.const import (
     VOLUME_GALLONS,
     VOLUME_LITERS,
     VOLUME_MILLILITERS,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.exceptions import HomeAssistantError
 
@@ -242,10 +239,10 @@ class SpeedConverter(BaseUnitConverter):
     UNIT_CLASS = "speed"
     NORMALIZED_UNIT = SPEED_METERS_PER_SECOND
     _UNIT_CONVERSION: dict[str, float] = {
-        PRECIPITATION_INTENSITY_INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
-        PRECIPITATION_INTENSITY_INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
-        PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
-        PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR: _HRS_TO_SECS / _MM_TO_M,
+        UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
+        UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
+        UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
+        UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: _HRS_TO_SECS / _MM_TO_M,
         SPEED_FEET_PER_SECOND: 1 / _FOOT_TO_M,
         SPEED_KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M,
         SPEED_KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M,
@@ -253,10 +250,10 @@ class SpeedConverter(BaseUnitConverter):
         SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M,
     }
     VALID_UNITS = {
-        PRECIPITATION_INTENSITY_INCHES_PER_DAY,
-        PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
-        PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
-        PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+        UnitOfVolumetricFlux.INCHES_PER_DAY,
+        UnitOfVolumetricFlux.INCHES_PER_HOUR,
+        UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
+        UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         SPEED_FEET_PER_SECOND,
         SPEED_KILOMETERS_PER_HOUR,
         SPEED_KNOTS,
diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py
index 486d2199e18..454f0708aac 100644
--- a/tests/util/test_unit_conversion.py
+++ b/tests/util/test_unit_conversion.py
@@ -22,10 +22,6 @@ from homeassistant.const import (
     MASS_POUNDS,
     POWER_KILO_WATT,
     POWER_WATT,
-    PRECIPITATION_INTENSITY_INCHES_PER_DAY,
-    PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
-    PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
     PRESSURE_CBAR,
     PRESSURE_HPA,
     PRESSURE_INHG,
@@ -48,6 +44,7 @@ from homeassistant.const import (
     VOLUME_GALLONS,
     VOLUME_LITERS,
     VOLUME_MILLILITERS,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.util.unit_conversion import (
@@ -96,10 +93,10 @@ INVALID_SYMBOL = "bob"
         (PressureConverter, PRESSURE_CBAR),
         (PressureConverter, PRESSURE_MMHG),
         (PressureConverter, PRESSURE_PSI),
-        (SpeedConverter, PRECIPITATION_INTENSITY_INCHES_PER_DAY),
-        (SpeedConverter, PRECIPITATION_INTENSITY_INCHES_PER_HOUR),
-        (SpeedConverter, PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY),
-        (SpeedConverter, PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR),
+        (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_DAY),
+        (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_HOUR),
+        (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY),
+        (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR),
         (SpeedConverter, SPEED_FEET_PER_SECOND),
         (SpeedConverter, SPEED_KILOMETERS_PER_HOUR),
         (SpeedConverter, SPEED_KNOTS),
@@ -397,42 +394,42 @@ def test_pressure_convert(
         # 5 in/day * 25.4 mm/in = 127 mm/day
         (
             5,
-            PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+            UnitOfVolumetricFlux.INCHES_PER_DAY,
             127,
-            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+            UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
         ),
         # 5 mm/day / 25.4 mm/in = 0.19685 in/day
         (
             5,
-            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+            UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
             pytest.approx(0.1968504),
-            PRECIPITATION_INTENSITY_INCHES_PER_DAY,
+            UnitOfVolumetricFlux.INCHES_PER_DAY,
         ),
         # 48 mm/day = 2 mm/h
         (
             48,
-            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+            UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
             pytest.approx(2),
-            PRECIPITATION_INTENSITY_MILLIMETERS_PER_HOUR,
+            UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         ),
         # 5 in/hr * 24 hr/day = 3048 mm/day
         (
             5,
-            PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+            UnitOfVolumetricFlux.INCHES_PER_HOUR,
             3048,
-            PRECIPITATION_INTENSITY_MILLIMETERS_PER_DAY,
+            UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
         ),
         # 5 m/s * 39.3701 in/m * 3600 s/hr = 708661
         (
             5,
             SPEED_METERS_PER_SECOND,
             pytest.approx(708661.42),
-            PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+            UnitOfVolumetricFlux.INCHES_PER_HOUR,
         ),
         # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s
         (
             5000,
-            PRECIPITATION_INTENSITY_INCHES_PER_HOUR,
+            UnitOfVolumetricFlux.INCHES_PER_HOUR,
             pytest.approx(0.0352778),
             SPEED_METERS_PER_SECOND,
         ),
-- 
GitLab


From 3346ddcd868e301ebdb1abd513d072c6aceaebc9 Mon Sep 17 00:00:00 2001
From: Thomas Dietrich <Thomas@Nurzen.de>
Date: Tue, 25 Oct 2022 16:46:47 +0200
Subject: [PATCH 802/985] Add sum-differences characteristics to statistics
 component (#79439)

---
 homeassistant/components/statistics/sensor.py | 36 +++++++++++--
 tests/components/statistics/test_sensor.py    | 50 +++++++++++++++++++
 2 files changed, 81 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py
index db50e4249f2..5e28777e673 100644
--- a/homeassistant/components/statistics/sensor.py
+++ b/homeassistant/components/statistics/sensor.py
@@ -79,6 +79,9 @@ STAT_MEDIAN = "median"
 STAT_NOISINESS = "noisiness"
 STAT_QUANTILES = "quantiles"
 STAT_STANDARD_DEVIATION = "standard_deviation"
+STAT_SUM = "sum"
+STAT_SUM_DIFFERENCES = "sum_differences"
+STAT_SUM_DIFFERENCES_NONNEGATIVE = "sum_differences_nonnegative"
 STAT_TOTAL = "total"
 STAT_VALUE_MAX = "value_max"
 STAT_VALUE_MIN = "value_min"
@@ -114,6 +117,9 @@ STATS_NUMERIC_SUPPORT = {
     STAT_NOISINESS,
     STAT_QUANTILES,
     STAT_STANDARD_DEVIATION,
+    STAT_SUM,
+    STAT_SUM_DIFFERENCES,
+    STAT_SUM_DIFFERENCES_NONNEGATIVE,
     STAT_TOTAL,
     STAT_VALUE_MAX,
     STAT_VALUE_MIN,
@@ -160,6 +166,9 @@ STAT_NUMERIC_RETAIN_UNIT = {
     STAT_MEDIAN,
     STAT_NOISINESS,
     STAT_STANDARD_DEVIATION,
+    STAT_SUM,
+    STAT_SUM_DIFFERENCES,
+    STAT_SUM_DIFFERENCES_NONNEGATIVE,
     STAT_TOTAL,
     STAT_VALUE_MAX,
     STAT_VALUE_MIN,
@@ -670,10 +679,7 @@ class StatisticsSensor(SensorEntity):
 
     def _stat_noisiness(self) -> StateType:
         if len(self.states) >= 2:
-            diff_sum = sum(
-                abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:])
-            )
-            return diff_sum / (len(self.states) - 1)
+            return cast(float, self._stat_sum_differences()) / (len(self.states) - 1)
         return None
 
     def _stat_quantiles(self) -> StateType:
@@ -695,11 +701,31 @@ class StatisticsSensor(SensorEntity):
             return statistics.stdev(self.states)
         return None
 
-    def _stat_total(self) -> StateType:
+    def _stat_sum(self) -> StateType:
         if len(self.states) > 0:
             return sum(self.states)
         return None
 
+    def _stat_sum_differences(self) -> StateType:
+        if len(self.states) >= 2:
+            diff_sum = sum(
+                abs(j - i) for i, j in zip(list(self.states), list(self.states)[1:])
+            )
+            return diff_sum
+        return None
+
+    def _stat_sum_differences_nonnegative(self) -> StateType:
+        if len(self.states) >= 2:
+            diff_sum_nn = sum(
+                (j - i if j >= i else j - 0)
+                for i, j in zip(list(self.states), list(self.states)[1:])
+            )
+            return diff_sum_nn
+        return None
+
+    def _stat_total(self) -> StateType:
+        return self._stat_sum()
+
     def _stat_value_max(self) -> StateType:
         if len(self.states) > 0:
             return max(self.states)
diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py
index 453d07fb69f..691159fe2fc 100644
--- a/tests/components/statistics/test_sensor.py
+++ b/tests/components/statistics/test_sensor.py
@@ -769,6 +769,56 @@ async def test_state_characteristics(hass: HomeAssistant):
             "value_9": float(round(statistics.stdev(VALUES_NUMERIC), 2)),
             "unit": "°C",
         },
+        {
+            "source_sensor_domain": "sensor",
+            "name": "sum",
+            "value_0": STATE_UNKNOWN,
+            "value_1": float(VALUES_NUMERIC[-1]),
+            "value_9": float(sum(VALUES_NUMERIC)),
+            "unit": "°C",
+        },
+        {
+            "source_sensor_domain": "sensor",
+            "name": "sum_differences",
+            "value_0": STATE_UNKNOWN,
+            "value_1": STATE_UNKNOWN,
+            "value_9": float(
+                sum(
+                    [
+                        abs(20 - 17),
+                        abs(15.2 - 20),
+                        abs(5 - 15.2),
+                        abs(3.8 - 5),
+                        abs(9.2 - 3.8),
+                        abs(6.7 - 9.2),
+                        abs(14 - 6.7),
+                        abs(6 - 14),
+                    ]
+                )
+            ),
+            "unit": "°C",
+        },
+        {
+            "source_sensor_domain": "sensor",
+            "name": "sum_differences_nonnegative",
+            "value_0": STATE_UNKNOWN,
+            "value_1": STATE_UNKNOWN,
+            "value_9": float(
+                sum(
+                    [
+                        20 - 17,
+                        15.2 - 0,
+                        5 - 0,
+                        3.8 - 0,
+                        9.2 - 3.8,
+                        6.7 - 0,
+                        14 - 6.7,
+                        6 - 0,
+                    ]
+                )
+            ),
+            "unit": "°C",
+        },
         {
             "source_sensor_domain": "sensor",
             "name": "total",
-- 
GitLab


From cc4656448869593e687fdd5207950776987f8787 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 25 Oct 2022 17:21:44 +0200
Subject: [PATCH 803/985] Add wind_speed sensor device class (#79789)

---
 homeassistant/components/sensor/__init__.py         | 10 ++++++++++
 homeassistant/components/sensor/device_condition.py |  2 ++
 homeassistant/components/sensor/device_trigger.py   |  2 ++
 3 files changed, 14 insertions(+)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index ae384df881b..e18bfbb178d 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -341,6 +341,15 @@ class SensorDeviceClass(StrEnum):
     - USCS / imperial: `oz`, `lb`
     """
 
+    WIND_SPEED = "wind_speed"
+    """Wind speed.
+
+    Unit of measurement: `SPEED_*` units
+    - SI /metric: `m/s`, `km/h`
+    - USCS / imperial: `ft/s`, `mph`
+    - Nautical: `kn`
+    """
+
 
 DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass))
 
@@ -387,6 +396,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
     SensorDeviceClass.VOLUME: VolumeConverter,
     SensorDeviceClass.WATER: VolumeConverter,
     SensorDeviceClass.WEIGHT: MassConverter,
+    SensorDeviceClass.WIND_SPEED: SpeedConverter,
 }
 
 # mypy: disallow-any-generics
diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py
index c1015636d40..e06355dda8c 100644
--- a/homeassistant/components/sensor/device_condition.py
+++ b/homeassistant/components/sensor/device_condition.py
@@ -65,6 +65,7 @@ CONF_IS_VOLTAGE = "is_voltage"
 CONF_IS_VOLUME = "is_volume"
 CONF_IS_WATER = "is_water"
 CONF_IS_WEIGHT = "is_weight"
+CONF_IS_WIND_SPEED = "is_wind_speed"
 
 ENTITY_CONDITIONS = {
     SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}],
@@ -104,6 +105,7 @@ ENTITY_CONDITIONS = {
     SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_IS_VOLUME}],
     SensorDeviceClass.WATER: [{CONF_TYPE: CONF_IS_WATER}],
     SensorDeviceClass.WEIGHT: [{CONF_TYPE: CONF_IS_WEIGHT}],
+    SensorDeviceClass.WIND_SPEED: [{CONF_TYPE: CONF_IS_WIND_SPEED}],
     DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}],
 }
 
diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py
index c2cda7d8e9d..4f36c8b78cb 100644
--- a/homeassistant/components/sensor/device_trigger.py
+++ b/homeassistant/components/sensor/device_trigger.py
@@ -64,6 +64,7 @@ CONF_VOLTAGE = "voltage"
 CONF_VOLUME = "volume"
 CONF_WATER = "water"
 CONF_WEIGHT = "weight"
+CONF_WIND_SPEED = "wind_speed"
 
 ENTITY_TRIGGERS = {
     SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_APPARENT_POWER}],
@@ -103,6 +104,7 @@ ENTITY_TRIGGERS = {
     SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_VOLUME}],
     SensorDeviceClass.WATER: [{CONF_TYPE: CONF_WATER}],
     SensorDeviceClass.WEIGHT: [{CONF_TYPE: CONF_WEIGHT}],
+    SensorDeviceClass.WIND_SPEED: [{CONF_TYPE: CONF_WIND_SPEED}],
     DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}],
 }
 
-- 
GitLab


From 870a5b6f378692792024f7a5b94799cf7ddcf3aa Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Tue, 25 Oct 2022 17:36:21 +0200
Subject: [PATCH 804/985] Adjust formatting in unit system tests (#80958)

* Adjust black formatting in unit system tests

* Split tests
---
 tests/util/test_unit_system.py | 106 ++++++++++++++-------------------
 1 file changed, 44 insertions(+), 62 deletions(-)

diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index fc8da540d3d..2331ff08412 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -389,76 +389,58 @@ def test_get_unit_system_invalid(key: str) -> None:
 
 
 @pytest.mark.parametrize(
-    "unit_system, device_class, original_unit, state_unit",
+    "device_class, original_unit, state_unit",
     (
-        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_FEET, LENGTH_METERS),
-        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_INCHES, LENGTH_MILLIMETERS),
-        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_MILES, LENGTH_KILOMETERS),
-        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_YARD, LENGTH_METERS),
-        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_KILOMETERS, None),
-        (METRIC_SYSTEM, SensorDeviceClass.DISTANCE, "very_long", None),
+        # Test distance conversion
+        (SensorDeviceClass.DISTANCE, LENGTH_FEET, LENGTH_METERS),
+        (SensorDeviceClass.DISTANCE, LENGTH_INCHES, LENGTH_MILLIMETERS),
+        (SensorDeviceClass.DISTANCE, LENGTH_MILES, LENGTH_KILOMETERS),
+        (SensorDeviceClass.DISTANCE, LENGTH_YARD, LENGTH_METERS),
+        (SensorDeviceClass.DISTANCE, LENGTH_KILOMETERS, None),
+        (SensorDeviceClass.DISTANCE, "very_long", None),
         # Test speed conversion
-        (
-            METRIC_SYSTEM,
-            SensorDeviceClass.SPEED,
-            SPEED_FEET_PER_SECOND,
-            SPEED_KILOMETERS_PER_HOUR,
-        ),
-        (
-            METRIC_SYSTEM,
-            SensorDeviceClass.SPEED,
-            SPEED_MILES_PER_HOUR,
-            SPEED_KILOMETERS_PER_HOUR,
-        ),
-        (METRIC_SYSTEM, SensorDeviceClass.SPEED, SPEED_KILOMETERS_PER_HOUR, None),
-        (METRIC_SYSTEM, SensorDeviceClass.SPEED, SPEED_KNOTS, None),
-        (METRIC_SYSTEM, SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, None),
-        (METRIC_SYSTEM, SensorDeviceClass.SPEED, "very_fast", None),
-        (
-            US_CUSTOMARY_SYSTEM,
-            SensorDeviceClass.DISTANCE,
-            LENGTH_CENTIMETERS,
-            LENGTH_INCHES,
-        ),
-        (
-            US_CUSTOMARY_SYSTEM,
-            SensorDeviceClass.DISTANCE,
-            LENGTH_KILOMETERS,
-            LENGTH_MILES,
-        ),
-        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_METERS, LENGTH_FEET),
-        (
-            US_CUSTOMARY_SYSTEM,
-            SensorDeviceClass.DISTANCE,
-            LENGTH_MILLIMETERS,
-            LENGTH_INCHES,
-        ),
-        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.DISTANCE, LENGTH_MILES, None),
-        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.DISTANCE, "very_long", None),
+        (SensorDeviceClass.SPEED, SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR),
+        (SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR),
+        (SensorDeviceClass.SPEED, SPEED_KILOMETERS_PER_HOUR, None),
+        (SensorDeviceClass.SPEED, SPEED_KNOTS, None),
+        (SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, None),
+        (SensorDeviceClass.SPEED, "very_fast", None),
+    ),
+)
+def test_get_metric_converted_unit_(
+    device_class: SensorDeviceClass,
+    original_unit: str,
+    state_unit: str | None,
+) -> None:
+    """Test unit conversion rules."""
+    unit_system = METRIC_SYSTEM
+    assert unit_system.get_converted_unit(device_class, original_unit) == state_unit
+
+
+@pytest.mark.parametrize(
+    "device_class, original_unit, state_unit",
+    (
+        # Test distance conversion
+        (SensorDeviceClass.DISTANCE, LENGTH_CENTIMETERS, LENGTH_INCHES),
+        (SensorDeviceClass.DISTANCE, LENGTH_KILOMETERS, LENGTH_MILES),
+        (SensorDeviceClass.DISTANCE, LENGTH_METERS, LENGTH_FEET),
+        (SensorDeviceClass.DISTANCE, LENGTH_MILLIMETERS, LENGTH_INCHES),
+        (SensorDeviceClass.DISTANCE, LENGTH_MILES, None),
+        (SensorDeviceClass.DISTANCE, "very_long", None),
         # Test speed conversion
-        (
-            US_CUSTOMARY_SYSTEM,
-            SensorDeviceClass.SPEED,
-            SPEED_METERS_PER_SECOND,
-            SPEED_MILES_PER_HOUR,
-        ),
-        (
-            US_CUSTOMARY_SYSTEM,
-            SensorDeviceClass.SPEED,
-            SPEED_KILOMETERS_PER_HOUR,
-            SPEED_MILES_PER_HOUR,
-        ),
-        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, SPEED_FEET_PER_SECOND, None),
-        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, SPEED_KNOTS, None),
-        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, None),
-        (US_CUSTOMARY_SYSTEM, SensorDeviceClass.SPEED, "very_fast", None),
+        (SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR),
+        (SensorDeviceClass.SPEED, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR),
+        (SensorDeviceClass.SPEED, SPEED_FEET_PER_SECOND, None),
+        (SensorDeviceClass.SPEED, SPEED_KNOTS, None),
+        (SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, None),
+        (SensorDeviceClass.SPEED, "very_fast", None),
     ),
 )
-def test_get_converted_unit(
-    unit_system: UnitSystem,
+def test_get_us_converted_unit(
     device_class: SensorDeviceClass,
     original_unit: str,
     state_unit: str | None,
 ) -> None:
     """Test unit conversion rules."""
+    unit_system = US_CUSTOMARY_SYSTEM
     assert unit_system.get_converted_unit(device_class, original_unit) == state_unit
-- 
GitLab


From 13e2bb1e223179f9b7844488ea925de7a70765d4 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Tue, 25 Oct 2022 19:26:56 +0200
Subject: [PATCH 805/985] Improve loading UniFi switch entities (#80910)

---
 homeassistant/components/unifi/const.py      |   5 +
 homeassistant/components/unifi/controller.py |   3 +-
 homeassistant/components/unifi/switch.py     | 116 ++++++++++++-------
 3 files changed, 83 insertions(+), 41 deletions(-)

diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py
index e84e2795a71..bf0aaef45dd 100644
--- a/homeassistant/components/unifi/const.py
+++ b/homeassistant/components/unifi/const.py
@@ -42,3 +42,8 @@ DEFAULT_TRACK_WIRED_CLIENTS = True
 DEFAULT_DETECTION_TIME = 300
 
 ATTR_MANUFACTURER = "Ubiquiti Networks"
+
+BLOCK_SWITCH = "block"
+DPI_SWITCH = "dpi"
+POE_SWITCH = "poe"
+OUTLET_SWITCH = "outlet"
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 3cc786f7c86..c421cb5391a 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -37,6 +37,7 @@ import homeassistant.util.dt as dt_util
 
 from .const import (
     ATTR_MANUFACTURER,
+    BLOCK_SWITCH,
     CONF_ALLOW_BANDWIDTH_SENSORS,
     CONF_ALLOW_UPTIME_SENSORS,
     CONF_BLOCK_CLIENT,
@@ -61,10 +62,10 @@ from .const import (
     DOMAIN as UNIFI_DOMAIN,
     LOGGER,
     PLATFORMS,
+    POE_SWITCH,
     UNIFI_WIRELESS_CLIENTS,
 )
 from .errors import AuthenticationRequired, CannotConnect
-from .switch import BLOCK_SWITCH, POE_SWITCH
 
 RETRY_TIMER = 15
 CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1)
diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index ed55b155559..fbfb6b7335a 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -4,11 +4,17 @@ Support for controlling power supply of clients which are powered over Ethernet
 Support for controlling network access of clients selected in option flow.
 Support for controlling deep packet inspection (DPI) restriction groups.
 """
+from __future__ import annotations
 
 import asyncio
-from typing import Any
+from collections.abc import Callable
+from dataclasses import dataclass
+from typing import Any, Generic, TypeVar
 
 from aiounifi.interfaces.api_handlers import ItemEvent
+from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups
+from aiounifi.interfaces.outlets import Outlets
+from aiounifi.interfaces.ports import Ports
 from aiounifi.models.api import SOURCE_EVENT
 from aiounifi.models.client import ClientBlockRequest
 from aiounifi.models.device import (
@@ -31,17 +37,34 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.restore_state import RestoreEntity
 
-from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN
+from .const import (
+    ATTR_MANUFACTURER,
+    BLOCK_SWITCH,
+    DOMAIN as UNIFI_DOMAIN,
+    DPI_SWITCH,
+    OUTLET_SWITCH,
+    POE_SWITCH,
+)
+from .controller import UniFiController
 from .unifi_client import UniFiClient
 
-BLOCK_SWITCH = "block"
-DPI_SWITCH = "dpi"
-POE_SWITCH = "poe"
-OUTLET_SWITCH = "outlet"
-
 CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED)
 CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED)
 
+T = TypeVar("T")
+
+
+@dataclass
+class UnifiEntityLoader(Generic[T]):
+    """Validate and load entities from different UniFi handlers."""
+
+    config_option_fn: Callable[[UniFiController], bool]
+    entity_cls: type[UnifiDPIRestrictionSwitch] | type[UnifiOutletSwitch] | type[
+        UnifiPoePortSwitch
+    ] | type[UnifiDPIRestrictionSwitch]
+    handler_fn: Callable[[UniFiController], T]
+    value_fn: Callable[[T, str], bool | None]
+
 
 async def async_setup_entry(
     hass: HomeAssistant,
@@ -52,7 +75,7 @@ async def async_setup_entry(
 
     Switches are controlling network access and switch ports with POE.
     """
-    controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
+    controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
     controller.entities[DOMAIN] = {
         BLOCK_SWITCH: set(),
         POE_SWITCH: set(),
@@ -105,42 +128,33 @@ async def async_setup_entry(
     known_poe_clients.clear()
 
     @callback
-    def async_add_outlet_switch(_: ItemEvent, obj_id: str) -> None:
-        """Add power outlet switch from UniFi controller."""
-        if not controller.api.outlets[obj_id].has_relay:
-            return
-        async_add_entities([UnifiOutletSwitch(obj_id, controller)])
-
-    controller.api.ports.subscribe(async_add_outlet_switch, ItemEvent.ADDED)
-
-    for index in controller.api.outlets:
-        async_add_outlet_switch(ItemEvent.ADDED, index)
-
-    def async_add_dpi_switch(_: ItemEvent, obj_id: str) -> None:
-        """Add DPI switch from UniFi controller."""
-        if (
-            not controller.option_dpi_restrictions
-            or not controller.api.dpi_groups[obj_id].dpiapp_ids
-        ):
-            return
-        async_add_entities([UnifiDPIRestrictionSwitch(obj_id, controller)])
-
-    controller.api.ports.subscribe(async_add_dpi_switch, ItemEvent.ADDED)
+    def async_load_entities(loader: UnifiEntityLoader) -> None:
+        """Load and subscribe to UniFi devices."""
+        entities: list[SwitchEntity] = []
+        api_handler = loader.handler_fn(controller)
+
+        @callback
+        def async_create_entity(event: ItemEvent, obj_id: str) -> None:
+            """Create UniFi entity."""
+            if not loader.config_option_fn(controller) or not loader.value_fn(
+                api_handler, obj_id
+            ):
+                return
 
-    for dpi_group_id in controller.api.dpi_groups:
-        async_add_dpi_switch(ItemEvent.ADDED, dpi_group_id)
+            entity = loader.entity_cls(obj_id, controller)
+            if event == ItemEvent.ADDED:
+                async_add_entities(entities)
+                return
+            entities.append(entity)
 
-    @callback
-    def async_add_poe_switch(_: ItemEvent, obj_id: str) -> None:
-        """Add port PoE switch from UniFi controller."""
-        if not controller.api.ports[obj_id].port_poe:
-            return
-        async_add_entities([UnifiPoePortSwitch(obj_id, controller)])
+        for obj_id in api_handler:
+            async_create_entity(ItemEvent.CHANGED, obj_id)
+        async_add_entities(entities)
 
-    controller.api.ports.subscribe(async_add_poe_switch, ItemEvent.ADDED)
+        api_handler.subscribe(async_create_entity, ItemEvent.ADDED)
 
-    for port_idx in controller.api.ports:
-        async_add_poe_switch(ItemEvent.ADDED, port_idx)
+    for unifi_loader in UNIFI_LOADERS:
+        async_load_entities(unifi_loader)
 
 
 @callback
@@ -640,3 +654,25 @@ class UnifiPoePortSwitch(SwitchEntity):
         await self.controller.api.request(
             DeviceSetPoePortModeRequest.create(device, self._index, "off")
         )
+
+
+UNIFI_LOADERS: tuple[UnifiEntityLoader, ...] = (
+    UnifiEntityLoader[DPIRestrictionGroups](
+        config_option_fn=lambda controller: controller.option_dpi_restrictions,
+        entity_cls=UnifiDPIRestrictionSwitch,
+        handler_fn=lambda controller: controller.api.dpi_groups,
+        value_fn=lambda handler, index: bool(handler[index].dpiapp_ids),
+    ),
+    UnifiEntityLoader[Outlets](
+        config_option_fn=lambda controller: True,
+        entity_cls=UnifiOutletSwitch,
+        handler_fn=lambda controller: controller.api.outlets,
+        value_fn=lambda handler, index: handler[index].has_relay,
+    ),
+    UnifiEntityLoader[Ports](
+        config_option_fn=lambda controller: True,
+        entity_cls=UnifiPoePortSwitch,
+        handler_fn=lambda controller: controller.api.ports,
+        value_fn=lambda handler, index: handler[index].port_poe,
+    ),
+)
-- 
GitLab


From d3ada34498d02652058e82f41a475d674d44bdae Mon Sep 17 00:00:00 2001
From: Rami Mosleh <engrbm87@gmail.com>
Date: Tue, 25 Oct 2022 20:44:27 +0300
Subject: [PATCH 806/985] Remove deprecate service in `speedtestdotnet`
 (#80938)

Remove deprecate service
---
 .../components/speedtestdotnet/__init__.py    | 30 +-----------------
 .../components/speedtestdotnet/const.py       |  2 --
 .../components/speedtestdotnet/services.yaml  |  3 --
 .../components/speedtestdotnet/strings.json   | 13 --------
 tests/components/speedtestdotnet/test_init.py | 31 -------------------
 5 files changed, 1 insertion(+), 78 deletions(-)
 delete mode 100644 homeassistant/components/speedtestdotnet/services.yaml

diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py
index 17238f74810..8cf32def197 100644
--- a/homeassistant/components/speedtestdotnet/__init__.py
+++ b/homeassistant/components/speedtestdotnet/__init__.py
@@ -8,9 +8,8 @@ import speedtest
 
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STARTED
-from homeassistant.core import CoreState, HomeAssistant, ServiceCall
+from homeassistant.core import CoreState, HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 
 from .const import (
@@ -20,7 +19,6 @@ from .const import (
     DEFAULT_SERVER,
     DOMAIN,
     PLATFORMS,
-    SPEED_TEST_SERVICE,
 )
 
 _LOGGER = logging.getLogger(__name__)
@@ -58,8 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
 
 async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
     """Unload SpeedTest Entry from config_entry."""
-    hass.services.async_remove(DOMAIN, SPEED_TEST_SERVICE)
-
     unload_ok = await hass.config_entries.async_unload_platforms(
         config_entry, PLATFORMS
     )
@@ -141,30 +137,6 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
         except speedtest.SpeedtestException as err:
             raise ConfigEntryNotReady from err
 
-        async def request_update(call: ServiceCall) -> None:
-            """Request update."""
-            async_create_issue(
-                self.hass,
-                DOMAIN,
-                "deprecated_service",
-                breaks_in_ha_version="2022.11.0",
-                is_fixable=True,
-                is_persistent=True,
-                severity=IssueSeverity.WARNING,
-                translation_key="deprecated_service",
-            )
-
-            _LOGGER.warning(
-                (
-                    'The "%s" service is deprecated and will be removed in "2022.11.0"; '
-                    'use the "homeassistant.update_entity" service and pass it a target Speedtest entity_id'
-                ),
-                SPEED_TEST_SERVICE,
-            )
-            await self.async_request_refresh()
-
-        self.hass.services.async_register(DOMAIN, SPEED_TEST_SERVICE, request_update)
-
         self.config_entry.async_on_unload(
             self.config_entry.add_update_listener(options_updated_listener)
         )
diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py
index e2455ad63df..e6ced462b45 100644
--- a/homeassistant/components/speedtestdotnet/const.py
+++ b/homeassistant/components/speedtestdotnet/const.py
@@ -14,8 +14,6 @@ from homeassistant.const import (
 
 DOMAIN: Final = "speedtestdotnet"
 
-SPEED_TEST_SERVICE: Final = "speedtest"
-
 
 @dataclass
 class SpeedtestSensorEntityDescription(SensorEntityDescription):
diff --git a/homeassistant/components/speedtestdotnet/services.yaml b/homeassistant/components/speedtestdotnet/services.yaml
deleted file mode 100644
index fdc6be746f8..00000000000
--- a/homeassistant/components/speedtestdotnet/services.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-speedtest:
-  name: Speedtest
-  description: Immediately execute a speed test with Speedtest.net
diff --git a/homeassistant/components/speedtestdotnet/strings.json b/homeassistant/components/speedtestdotnet/strings.json
index d4117d82cf3..c4dad30cb09 100644
--- a/homeassistant/components/speedtestdotnet/strings.json
+++ b/homeassistant/components/speedtestdotnet/strings.json
@@ -19,18 +19,5 @@
         }
       }
     }
-  },
-  "issues": {
-    "deprecated_service": {
-      "title": "The speedtest service is being removed",
-      "fix_flow": {
-        "step": {
-          "confirm": {
-            "title": "The speedtest service is being removed",
-            "description": "Update any automations or scripts that use this service to instead use the `homeassistant.update_entity` service with a target Speedtest entity_id. Then, click SUBMIT below to mark this issue as resolved."
-          }
-        }
-      }
-    }
   }
 }
diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py
index b4186ecacef..4b8071ad1ea 100644
--- a/tests/components/speedtestdotnet/test_init.py
+++ b/tests/components/speedtestdotnet/test_init.py
@@ -1,11 +1,8 @@
 """Tests for SpeedTest integration."""
 
-from collections.abc import Awaitable
 from datetime import timedelta
-from typing import Callable
 from unittest.mock import MagicMock
 
-from aiohttp import ClientWebSocketResponse
 import speedtest
 
 from homeassistant.components.speedtestdotnet.const import (
@@ -13,7 +10,6 @@ from homeassistant.components.speedtestdotnet.const import (
     CONF_SERVER_ID,
     CONF_SERVER_NAME,
     DOMAIN,
-    SPEED_TEST_SERVICE,
 )
 from homeassistant.config_entries import ConfigEntryState
 from homeassistant.const import CONF_SCAN_INTERVAL, STATE_UNAVAILABLE
@@ -21,7 +17,6 @@ from homeassistant.core import HomeAssistant
 import homeassistant.util.dt as dt_util
 
 from tests.common import MockConfigEntry, async_fire_time_changed
-from tests.components.repairs import get_repairs
 
 
 async def test_successful_config_entry(hass: HomeAssistant) -> None:
@@ -43,7 +38,6 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None:
 
     assert entry.state == ConfigEntryState.LOADED
     assert hass.data[DOMAIN]
-    assert hass.services.has_service(DOMAIN, SPEED_TEST_SERVICE)
 
 
 async def test_setup_failed(hass: HomeAssistant, mock_api: MagicMock) -> None:
@@ -125,28 +119,3 @@ async def test_get_best_server_error(hass: HomeAssistant, mock_api: MagicMock) -
     state = hass.states.get("sensor.speedtest_ping")
     assert state is not None
     assert state.state == STATE_UNAVAILABLE
-
-
-async def test_deprecated_service_alert(
-    hass: HomeAssistant,
-    hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]],
-) -> None:
-    """Test that an issue is raised if deprecated services is called."""
-    entry = MockConfigEntry(
-        domain=DOMAIN,
-    )
-    entry.add_to_hass(hass)
-
-    await hass.config_entries.async_setup(entry.entry_id)
-    await hass.async_block_till_done()
-
-    await hass.services.async_call(
-        DOMAIN,
-        "speedtest",
-        {},
-        blocking=True,
-    )
-    await hass.async_block_till_done()
-    issues = await get_repairs(hass, hass_ws_client)
-    assert len(issues) == 1
-    assert issues[0]["issue_id"] == "deprecated_service"
-- 
GitLab


From f73fc9e3558eca0a1e74a19273a67f8d2bfa8af7 Mon Sep 17 00:00:00 2001
From: Petro31 <35082313+Petro31@users.noreply.github.com>
Date: Tue, 25 Oct 2022 13:49:51 -0400
Subject: [PATCH 807/985] Adds states and state_attr as a filter, adds is_state
 and is_state_attr as a test. (#79473)

---
 homeassistant/helpers/template.py |  6 ++-
 tests/helpers/test_template.py    | 68 ++++++++++++++++++++++++++++++-
 2 files changed, 72 insertions(+), 2 deletions(-)

diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 0db755e5115..dfab80e5223 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -25,7 +25,7 @@ import weakref
 
 from awesomeversion import AwesomeVersion
 import jinja2
-from jinja2 import pass_context, pass_environment
+from jinja2 import pass_context, pass_environment, pass_eval_context
 from jinja2.sandbox import ImmutableSandboxedEnvironment
 from jinja2.utils import Namespace
 import voluptuous as vol
@@ -2153,9 +2153,13 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
         self.filters["closest"] = pass_context(hassfunction(closest_filter))
         self.globals["distance"] = hassfunction(distance)
         self.globals["is_state"] = hassfunction(is_state)
+        self.tests["is_state"] = pass_eval_context(self.globals["is_state"])
         self.globals["is_state_attr"] = hassfunction(is_state_attr)
+        self.tests["is_state_attr"] = pass_eval_context(self.globals["is_state_attr"])
         self.globals["state_attr"] = hassfunction(state_attr)
+        self.filters["state_attr"] = self.globals["state_attr"]
         self.globals["states"] = AllStates(hass)
+        self.filters["states"] = self.globals["states"]
         self.globals["utcnow"] = hassfunction(utcnow)
         self.globals["now"] = hassfunction(now)
 
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index e42f001818b..63a6154a190 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -1348,6 +1348,22 @@ def test_is_state(hass):
     )
     assert tpl.async_render() is False
 
+    tpl = template.Template(
+        """
+{% if "test.object" is is_state("available") %}yes{% else %}no{% endif %}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "yes"
+
+    tpl = template.Template(
+        """
+{{ ['test.object'] | select("is_state", "available") | first | default }}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "test.object"
+
 
 def test_is_state_attr(hass):
     """Test is_state_attr method."""
@@ -1368,10 +1384,28 @@ def test_is_state_attr(hass):
     )
     assert tpl.async_render() is False
 
+    tpl = template.Template(
+        """
+{% if "test.object" is is_state_attr("mode", "on") %}yes{% else %}no{% endif %}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "yes"
+
+    tpl = template.Template(
+        """
+{{ ['test.object'] | select("is_state_attr", "mode", "on") | first | default }}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "test.object"
+
 
 def test_state_attr(hass):
     """Test state_attr method."""
-    hass.states.async_set("test.object", "available", {"mode": "on"})
+    hass.states.async_set(
+        "test.object", "available", {"effect": "action", "mode": "on"}
+    )
     tpl = template.Template(
         """
 {% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %}
@@ -1388,6 +1422,22 @@ def test_state_attr(hass):
     )
     assert tpl.async_render() is True
 
+    tpl = template.Template(
+        """
+{% if "test.object" | state_attr("mode") == "on" %}yes{% else %}no{% endif %}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "yes"
+
+    tpl = template.Template(
+        """
+{{ ['test.object'] | map("state_attr", "effect") | first | default }}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "action"
+
 
 def test_states_function(hass):
     """Test using states as a function."""
@@ -1398,6 +1448,22 @@ def test_states_function(hass):
     tpl2 = template.Template('{{ states("test.object2") }}', hass)
     assert tpl2.async_render() == "unknown"
 
+    tpl = template.Template(
+        """
+{% if "test.object" | states == "available" %}yes{% else %}no{% endif %}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "yes"
+
+    tpl = template.Template(
+        """
+{{ ['test.object'] | map("states") | first | default }}
+        """,
+        hass,
+    )
+    assert tpl.async_render() == "available"
+
 
 @patch(
     "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
-- 
GitLab


From c197e1765a9b53772b586e6ee87ffae2180a576f Mon Sep 17 00:00:00 2001
From: Ryan Fleming <rfleming71@users.noreply.github.com>
Date: Tue, 25 Oct 2022 13:59:57 -0400
Subject: [PATCH 808/985] Add Octoprint camera entity (#79689)

---
 .../components/octoprint/__init__.py          |  2 +-
 homeassistant/components/octoprint/camera.py  | 59 ++++++++++++++++
 tests/components/octoprint/test_camera.py     | 67 +++++++++++++++++++
 3 files changed, 127 insertions(+), 1 deletion(-)
 create mode 100644 homeassistant/components/octoprint/camera.py
 create mode 100644 tests/components/octoprint/test_camera.py

diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py
index 9db4e834571..8f6de2f0a28 100644
--- a/homeassistant/components/octoprint/__init__.py
+++ b/homeassistant/components/octoprint/__init__.py
@@ -56,7 +56,7 @@ def ensure_valid_path(value):
     return value
 
 
-PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR]
+PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, Platform.SENSOR]
 DEFAULT_NAME = "OctoPrint"
 CONF_NUMBER_OF_TOOLS = "number_of_tools"
 CONF_BED = "bed"
diff --git a/homeassistant/components/octoprint/camera.py b/homeassistant/components/octoprint/camera.py
new file mode 100644
index 00000000000..ea886c97936
--- /dev/null
+++ b/homeassistant/components/octoprint/camera.py
@@ -0,0 +1,59 @@
+"""Support for OctoPrint binary camera."""
+from __future__ import annotations
+
+from pyoctoprintapi import OctoprintClient, WebcamSettings
+
+from homeassistant.components.mjpeg.camera import MjpegCamera
+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 OctoprintDataUpdateCoordinator
+from .const import DOMAIN
+
+
+async def async_setup_entry(
+    hass: HomeAssistant,
+    config_entry: ConfigEntry,
+    async_add_entities: AddEntitiesCallback,
+) -> None:
+    """Set up the available OctoPrint camera."""
+    coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][
+        config_entry.entry_id
+    ]["coordinator"]
+    client: OctoprintClient = hass.data[DOMAIN][config_entry.entry_id]["client"]
+    device_id = config_entry.unique_id
+
+    assert device_id is not None
+
+    camera_info = await client.get_webcam_info()
+
+    if not camera_info or not camera_info.enabled:
+        return
+
+    async_add_entities(
+        [
+            OctoprintCamera(
+                camera_info,
+                coordinator.device_info,
+                device_id,
+            )
+        ]
+    )
+
+
+class OctoprintCamera(MjpegCamera):
+    """Representation of an OctoPrint Camera Stream."""
+
+    def __init__(
+        self, camera_settings: WebcamSettings, device_info: DeviceInfo, device_id: str
+    ) -> None:
+        """Initialize as a subclass of MjpegCamera."""
+        super().__init__(
+            device_info=device_info,
+            mjpeg_url=camera_settings.stream_url,
+            name="OctoPrint Camera",
+            still_image_url=camera_settings.external_snapshot_url,
+            unique_id=f"camera-{device_id}",
+        )
diff --git a/tests/components/octoprint/test_camera.py b/tests/components/octoprint/test_camera.py
new file mode 100644
index 00000000000..c95cf924d15
--- /dev/null
+++ b/tests/components/octoprint/test_camera.py
@@ -0,0 +1,67 @@
+"""The tests for Octoptint camera module."""
+
+from unittest.mock import patch
+
+from pyoctoprintapi import WebcamSettings
+
+from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
+from homeassistant.helpers import entity_registry as er
+
+from . import init_integration
+
+
+async def test_camera(hass):
+    """Test the underlying camera."""
+    with patch(
+        "pyoctoprintapi.OctoprintClient.get_webcam_info",
+        return_value=WebcamSettings(
+            base_url="http://fake-octoprint/",
+            raw={
+                "streamUrl": "/webcam/?action=stream",
+                "snapshotUrl": "http://127.0.0.1:8080/?action=snapshot",
+                "webcamEnabled": True,
+            },
+        ),
+    ):
+        await init_integration(hass, CAMERA_DOMAIN)
+
+    entity_registry = er.async_get(hass)
+
+    entry = entity_registry.async_get("camera.octoprint_camera")
+    assert entry is not None
+    assert entry.unique_id == "camera-uuid"
+
+
+async def test_camera_disabled(hass):
+    """Test that the camera does not load if there is not one configured."""
+    with patch(
+        "pyoctoprintapi.OctoprintClient.get_webcam_info",
+        return_value=WebcamSettings(
+            base_url="http://fake-octoprint/",
+            raw={
+                "streamUrl": "/webcam/?action=stream",
+                "snapshotUrl": "http://127.0.0.1:8080/?action=snapshot",
+                "webcamEnabled": False,
+            },
+        ),
+    ):
+        await init_integration(hass, CAMERA_DOMAIN)
+
+    entity_registry = er.async_get(hass)
+
+    entry = entity_registry.async_get("camera.octoprint_camera")
+    assert entry is None
+
+
+async def test_no_supported_camera(hass):
+    """Test that the camera does not load if there is not one configured."""
+    with patch(
+        "pyoctoprintapi.OctoprintClient.get_webcam_info",
+        return_value=None,
+    ):
+        await init_integration(hass, CAMERA_DOMAIN)
+
+    entity_registry = er.async_get(hass)
+
+    entry = entity_registry.async_get("camera.octoprint_camera")
+    assert entry is None
-- 
GitLab


From 8175dab7ab7e1b9ecf160fe6b33b93e62e6f999c Mon Sep 17 00:00:00 2001
From: Michael <35783820+mib1185@users.noreply.github.com>
Date: Tue, 25 Oct 2022 20:07:28 +0200
Subject: [PATCH 809/985] Add week period to recorder statistics api (#80784)

* add week period to get statistics api

* add test
---
 .../components/recorder/statistics.py         |  36 ++++-
 .../components/recorder/websocket_api.py      |   4 +-
 tests/components/recorder/test_statistics.py  | 140 +++++++++++++++++-
 3 files changed, 175 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py
index 88638a6ccc7..2b249aeeb14 100644
--- a/homeassistant/components/recorder/statistics.py
+++ b/homeassistant/components/recorder/statistics.py
@@ -1014,6 +1014,35 @@ def _reduce_statistics_per_day(
     return _reduce_statistics(stats, same_day, day_start_end, timedelta(days=1))
 
 
+def same_week(time1: datetime, time2: datetime) -> bool:
+    """Return True if time1 and time2 are in the same year and week."""
+    date1 = dt_util.as_local(time1).date()
+    date2 = dt_util.as_local(time2).date()
+    return (date1.year, date1.isocalendar().week) == (
+        date2.year,
+        date2.isocalendar().week,
+    )
+
+
+def week_start_end(time: datetime) -> tuple[datetime, datetime]:
+    """Return the start and end of the period (week) time is within."""
+    time_local = dt_util.as_local(time)
+    start_local = time_local.replace(
+        hour=0, minute=0, second=0, microsecond=0
+    ) - timedelta(days=time_local.weekday())
+    start = dt_util.as_utc(start_local)
+    end = dt_util.as_utc(start_local + timedelta(days=7))
+    return (start, end)
+
+
+def _reduce_statistics_per_week(
+    stats: dict[str, list[dict[str, Any]]],
+) -> dict[str, list[dict[str, Any]]]:
+    """Reduce hourly statistics to weekly statistics."""
+
+    return _reduce_statistics(stats, same_week, week_start_end, timedelta(days=7))
+
+
 def same_month(time1: datetime, time2: datetime) -> bool:
     """Return True if time1 and time2 are in the same year and month."""
     date1 = dt_util.as_local(time1).date()
@@ -1089,7 +1118,7 @@ def statistics_during_period(
     start_time: datetime,
     end_time: datetime | None = None,
     statistic_ids: list[str] | None = None,
-    period: Literal["5minute", "day", "hour", "month"] = "hour",
+    period: Literal["5minute", "day", "hour", "week", "month"] = "hour",
     start_time_as_datetime: bool = False,
     units: dict[str, str] | None = None,
 ) -> dict[str, list[dict[str, Any]]]:
@@ -1122,7 +1151,7 @@ def statistics_during_period(
         if not stats:
             return {}
         # Return statistics combined with metadata
-        if period not in ("day", "month"):
+        if period not in ("day", "week", "month"):
             return _sorted_statistics_to_dict(
                 hass,
                 session,
@@ -1152,6 +1181,9 @@ def statistics_during_period(
         if period == "day":
             return _reduce_statistics_per_day(result)
 
+        if period == "week":
+            return _reduce_statistics_per_week(result)
+
         return _reduce_statistics_per_month(result)
 
 
diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py
index 37d117c910e..2079d9537b5 100644
--- a/homeassistant/components/recorder/websocket_api.py
+++ b/homeassistant/components/recorder/websocket_api.py
@@ -62,7 +62,7 @@ def _ws_get_statistics_during_period(
     start_time: dt,
     end_time: dt | None,
     statistic_ids: list[str] | None,
-    period: Literal["5minute", "day", "hour", "month"],
+    period: Literal["5minute", "day", "hour", "week", "month"],
     units: dict[str, str],
 ) -> str:
     """Fetch statistics and convert them to json in the executor."""
@@ -118,7 +118,7 @@ async def ws_handle_get_statistics_during_period(
         vol.Required("start_time"): str,
         vol.Optional("end_time"): str,
         vol.Optional("statistic_ids"): [str],
-        vol.Required("period"): vol.Any("5minute", "hour", "day", "month"),
+        vol.Required("period"): vol.Any("5minute", "hour", "day", "week", "month"),
         vol.Optional("units"): vol.Schema(
             {
                 vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py
index fb5cf8dba8b..aae6fcf91cb 100644
--- a/tests/components/recorder/test_statistics.py
+++ b/tests/components/recorder/test_statistics.py
@@ -885,10 +885,148 @@ def test_import_statistics_errors(hass_recorder, caplog):
     assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {}
 
 
+@pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"])
+@pytest.mark.freeze_time("2022-10-01 00:00:00+00:00")
+def test_weekly_statistics(hass_recorder, caplog, timezone):
+    """Test weekly statistics."""
+    dt_util.set_default_time_zone(dt_util.get_time_zone(timezone))
+
+    hass = hass_recorder()
+    wait_recording_done(hass)
+    assert "Compiling statistics for" not in caplog.text
+    assert "Statistics already compiled" not in caplog.text
+
+    zero = dt_util.utcnow()
+    period1 = dt_util.as_utc(dt_util.parse_datetime("2022-10-03 00:00:00"))
+    period2 = dt_util.as_utc(dt_util.parse_datetime("2022-10-09 23:00:00"))
+    period3 = dt_util.as_utc(dt_util.parse_datetime("2022-10-10 00:00:00"))
+    period4 = dt_util.as_utc(dt_util.parse_datetime("2022-10-16 23:00:00"))
+
+    external_statistics = (
+        {
+            "start": period1,
+            "last_reset": None,
+            "state": 0,
+            "sum": 2,
+        },
+        {
+            "start": period2,
+            "last_reset": None,
+            "state": 1,
+            "sum": 3,
+        },
+        {
+            "start": period3,
+            "last_reset": None,
+            "state": 2,
+            "sum": 4,
+        },
+        {
+            "start": period4,
+            "last_reset": None,
+            "state": 3,
+            "sum": 5,
+        },
+    )
+    external_metadata = {
+        "has_mean": False,
+        "has_sum": True,
+        "name": "Total imported energy",
+        "source": "test",
+        "statistic_id": "test:total_energy_import",
+        "unit_of_measurement": "kWh",
+    }
+
+    async_add_external_statistics(hass, external_metadata, external_statistics)
+    wait_recording_done(hass)
+    stats = statistics_during_period(hass, zero, period="week")
+    week1_start = dt_util.as_utc(dt_util.parse_datetime("2022-10-03 00:00:00"))
+    week1_end = dt_util.as_utc(dt_util.parse_datetime("2022-10-10 00:00:00"))
+    week2_start = dt_util.as_utc(dt_util.parse_datetime("2022-10-10 00:00:00"))
+    week2_end = dt_util.as_utc(dt_util.parse_datetime("2022-10-17 00:00:00"))
+    assert stats == {
+        "test:total_energy_import": [
+            {
+                "statistic_id": "test:total_energy_import",
+                "start": week1_start.isoformat(),
+                "end": week1_end.isoformat(),
+                "max": None,
+                "mean": None,
+                "min": None,
+                "last_reset": None,
+                "state": 1.0,
+                "sum": 3.0,
+            },
+            {
+                "statistic_id": "test:total_energy_import",
+                "start": week2_start.isoformat(),
+                "end": week2_end.isoformat(),
+                "max": None,
+                "mean": None,
+                "min": None,
+                "last_reset": None,
+                "state": 3.0,
+                "sum": 5.0,
+            },
+        ]
+    }
+
+    stats = statistics_during_period(
+        hass,
+        start_time=zero,
+        statistic_ids=["not", "the", "same", "test:total_energy_import"],
+        period="week",
+    )
+    assert stats == {
+        "test:total_energy_import": [
+            {
+                "statistic_id": "test:total_energy_import",
+                "start": week1_start.isoformat(),
+                "end": week1_end.isoformat(),
+                "max": None,
+                "mean": None,
+                "min": None,
+                "last_reset": None,
+                "state": 1.0,
+                "sum": 3.0,
+            },
+            {
+                "statistic_id": "test:total_energy_import",
+                "start": week2_start.isoformat(),
+                "end": week2_end.isoformat(),
+                "max": None,
+                "mean": None,
+                "min": None,
+                "last_reset": None,
+                "state": 3.0,
+                "sum": 5.0,
+            },
+        ]
+    }
+
+    # Use 5minute to ensure table switch works
+    stats = statistics_during_period(
+        hass,
+        start_time=zero,
+        statistic_ids=["test:total_energy_import", "with_other"],
+        period="5minute",
+    )
+    assert stats == {}
+
+    # Ensure future date has not data
+    future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00"))
+    stats = statistics_during_period(
+        hass, start_time=future, end_time=future, period="month"
+    )
+    assert stats == {}
+
+    dt_util.set_default_time_zone(dt_util.get_time_zone("UTC"))
+
+
 @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"])
 @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00")
 def test_monthly_statistics(hass_recorder, caplog, timezone):
-    """Test inserting external statistics."""
+    """Test monthly statistics."""
     dt_util.set_default_time_zone(dt_util.get_time_zone(timezone))
 
     hass = hass_recorder()
-- 
GitLab


From 403f0c16afe737e50df829d19d5e65f73557f0ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= <ludeeus@ludeeus.dev>
Date: Tue, 25 Oct 2022 20:48:18 +0200
Subject: [PATCH 810/985] Bump aiogithubapi from 22.2.4 to 22.10.1 (#80968)

---
 homeassistant/components/github/manifest.json | 2 +-
 requirements_all.txt                          | 2 +-
 requirements_test_all.txt                     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json
index 25c7cbdd512..c6f806e7a13 100644
--- a/homeassistant/components/github/manifest.json
+++ b/homeassistant/components/github/manifest.json
@@ -2,7 +2,7 @@
   "domain": "github",
   "name": "GitHub",
   "documentation": "https://www.home-assistant.io/integrations/github",
-  "requirements": ["aiogithubapi==22.2.4"],
+  "requirements": ["aiogithubapi==22.10.1"],
   "codeowners": ["@timmo001", "@ludeeus"],
   "iot_class": "cloud_polling",
   "config_flow": true,
diff --git a/requirements_all.txt b/requirements_all.txt
index dbc147173be..bfb4b4513fd 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -162,7 +162,7 @@ aioflo==2021.11.0
 aioftp==0.21.3
 
 # homeassistant.components.github
-aiogithubapi==22.2.4
+aiogithubapi==22.10.1
 
 # homeassistant.components.guardian
 aioguardian==2022.07.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 23dcd128835..f5f3fd8432c 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -146,7 +146,7 @@ aioesphomeapi==11.2.0
 aioflo==2021.11.0
 
 # homeassistant.components.github
-aiogithubapi==22.2.4
+aiogithubapi==22.10.1
 
 # homeassistant.components.guardian
 aioguardian==2022.07.0
-- 
GitLab


From a98c304db6e3733a1d6a70a7e8ce0cff1ff381d5 Mon Sep 17 00:00:00 2001
From: Thomas Dietrich <Thomas@Nurzen.de>
Date: Tue, 25 Oct 2022 20:50:52 +0200
Subject: [PATCH 811/985] Add deprecation warning for statistics integration
 default buffer_size (#69700)

* Add deprecation warning for buffer size

* Attend to comments

* Clarify deprecation info

* Move warnings to repairs issue_registry

* Apply slight wording changes, add compiled en strings file

* Format json
---
 homeassistant/components/statistics/sensor.py | 54 ++++++++++++-------
 .../components/statistics/strings.json        | 12 +++++
 .../statistics/translations/en.json           | 12 +++++
 3 files changed, 60 insertions(+), 18 deletions(-)
 create mode 100644 homeassistant/components/statistics/strings.json
 create mode 100644 homeassistant/components/statistics/translations/en.json

diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py
index 5e28777e673..cfc093c7762 100644
--- a/homeassistant/components/statistics/sensor.py
+++ b/homeassistant/components/statistics/sensor.py
@@ -34,10 +34,11 @@ from homeassistant.core import (
     Event,
     HomeAssistant,
     State,
+    async_get_hass,
     callback,
     split_entity_id,
 )
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, issue_registry
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.event import (
     async_track_point_in_utc_time,
@@ -87,15 +88,6 @@ STAT_VALUE_MAX = "value_max"
 STAT_VALUE_MIN = "value_min"
 STAT_VARIANCE = "variance"
 
-DEPRECATION_WARNING_CHARACTERISTIC = (
-    "The configuration parameter 'state_characteristic' will become "
-    "mandatory in a future release of the statistics integration. "
-    "Please add 'state_characteristic: %s' to the configuration of "
-    "sensor '%s' to keep the current behavior. Read the documentation "
-    "for further details: "
-    "https://www.home-assistant.io/integrations/statistics/"
-)
-
 # Statistics supported by a sensor source (numeric)
 STATS_NUMERIC_SUPPORT = {
     STAT_AVERAGE_LINEAR,
@@ -189,7 +181,6 @@ CONF_QUANTILE_INTERVALS = "quantile_intervals"
 CONF_QUANTILE_METHOD = "quantile_method"
 
 DEFAULT_NAME = "Stats"
-DEFAULT_BUFFER_SIZE = 20
 DEFAULT_PRECISION = 2
 DEFAULT_QUANTILE_INTERVALS = 4
 DEFAULT_QUANTILE_METHOD = "exclusive"
@@ -202,10 +193,19 @@ def valid_state_characteristic_configuration(config: dict[str, Any]) -> dict[str
 
     if config.get(CONF_STATE_CHARACTERISTIC) is None:
         config[CONF_STATE_CHARACTERISTIC] = STAT_COUNT if is_binary else STAT_MEAN
-        _LOGGER.warning(
-            DEPRECATION_WARNING_CHARACTERISTIC,
-            config[CONF_STATE_CHARACTERISTIC],
-            config[CONF_NAME],
+        issue_registry.async_create_issue(
+            hass=async_get_hass(),
+            domain=DOMAIN,
+            issue_id=f"{config[CONF_ENTITY_ID]}_default_characteristic",
+            breaks_in_ha_version="2022.12.0",
+            is_fixable=False,
+            severity=issue_registry.IssueSeverity.WARNING,
+            translation_key="deprecation_warning_characteristic",
+            translation_placeholders={
+                "entity": config[CONF_NAME],
+                "characteristic": config[CONF_STATE_CHARACTERISTIC],
+            },
+            learn_more_url="https://github.com/home-assistant/core/pull/60402",
         )
 
     characteristic = cast(str, config[CONF_STATE_CHARACTERISTIC])
@@ -220,15 +220,32 @@ def valid_state_characteristic_configuration(config: dict[str, Any]) -> dict[str
     return config
 
 
+def valid_boundary_configuration(config: dict[str, Any]) -> dict[str, Any]:
+    """Validate that sampling_size, max_age, or both are provided."""
+
+    if config.get(CONF_SAMPLES_MAX_BUFFER_SIZE) is None:
+        config[CONF_SAMPLES_MAX_BUFFER_SIZE] = 20
+        issue_registry.async_create_issue(
+            hass=async_get_hass(),
+            domain=DOMAIN,
+            issue_id=f"{config[CONF_ENTITY_ID]}_invalid_boundary_config",
+            breaks_in_ha_version="2022.12.0",
+            is_fixable=False,
+            severity=issue_registry.IssueSeverity.WARNING,
+            translation_key="deprecation_warning_size",
+            translation_placeholders={"entity": config[CONF_NAME]},
+            learn_more_url="https://github.com/home-assistant/core/pull/69700",
+        )
+    return config
+
+
 _PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend(
     {
         vol.Required(CONF_ENTITY_ID): cv.entity_id,
         vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
         vol.Optional(CONF_UNIQUE_ID): cv.string,
         vol.Optional(CONF_STATE_CHARACTERISTIC): cv.string,
-        vol.Optional(
-            CONF_SAMPLES_MAX_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE
-        ): vol.All(vol.Coerce(int), vol.Range(min=1)),
+        vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): vol.Coerce(int),
         vol.Optional(CONF_MAX_AGE): cv.time_period,
         vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int),
         vol.Optional(
@@ -242,6 +259,7 @@ _PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend(
 PLATFORM_SCHEMA = vol.All(
     _PLATFORM_SCHEMA_BASE,
     valid_state_characteristic_configuration,
+    valid_boundary_configuration,
 )
 
 
diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json
new file mode 100644
index 00000000000..0cca71f172f
--- /dev/null
+++ b/homeassistant/components/statistics/strings.json
@@ -0,0 +1,12 @@
+{
+  "issues": {
+    "deprecation_warning_characteristic": {
+      "description": "The configuration parameter `state_characteristic` of the statistics integration will become mandatory.\n\nPlease add `state_characteristic: {characteristic}` to the configuration of sensor `{entity}` to keep the current behavior.\n\nRead the documentation of the statistics integration for further details: https://www.home-assistant.io/integrations/statistics/",
+      "title": "Mandatory 'state_characteristic' assumed for a Statistics entity"
+    },
+    "deprecation_warning_size": {
+      "description": "The configuration parameter `sampling_size` of the statistics integration defaulted to the value 20 so far, which will change.\n\nPlease check the configuration for sensor `{entity}` and add suited boundaries, e.g., `sampling_size: 20` to keep the current behavior. The configuration of the statistics integration will become more flexible with version 2022.12.0 and accept either `sampling_size` or `max_age`, or both settings. The request above prepares your configuration for this otherwise breaking change.\n\nRead the documentation of the statistics integration for further details: https://www.home-assistant.io/integrations/statistics/",
+      "title": "Implicit 'sampling_size' assumed for a Statistics entity"
+    }
+  }
+}
diff --git a/homeassistant/components/statistics/translations/en.json b/homeassistant/components/statistics/translations/en.json
new file mode 100644
index 00000000000..730f5a5656d
--- /dev/null
+++ b/homeassistant/components/statistics/translations/en.json
@@ -0,0 +1,12 @@
+{
+    "issues": {
+        "deprecation_warning_characteristic": {
+            "description": "The configuration parameter `state_characteristic` of the statistics integration will become mandatory.\n\nPlease add `state_characteristic: {characteristic}` to the configuration of sensor `{entity}` to keep the current behavior.\n\nRead the documentation of the statistics integration for further details: https://www.home-assistant.io/integrations/statistics/",
+            "title": "Mandatory 'state_characteristic' assumed for a Statistics entity"
+        },
+        "deprecation_warning_size": {
+            "description": "The configuration parameter `sampling_size` of the statistics integration defaulted to the value 20 so far, which will change.\n\nPlease check the configuration for sensor `{entity}` and add suited boundaries, e.g., `sampling_size: 20` to keep the current behavior. The configuration of the statistics integration will become more flexible with version 2022.12.0 and accept either `sampling_size` or `max_age`, or both settings. The request above prepares your configuration for this otherwise breaking change.\n\nRead the documentation of the statistics integration for further details: https://www.home-assistant.io/integrations/statistics/",
+            "title": "Implicit 'sampling_size' assumed for a Statistics entity"
+        }
+    }
+}
\ No newline at end of file
-- 
GitLab


From 98591cd4b6805f52e7981172442f801938b6594c Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Tue, 25 Oct 2022 21:05:04 +0200
Subject: [PATCH 812/985] Remove hardware and fix raspberry pi brands in
 integrations.json (#80970)

---
 homeassistant/brands/raspberry.json           |  5 ----
 homeassistant/brands/raspberry_pi.json        |  5 ++++
 .../components/remote_rpi_gpio/manifest.json  |  2 +-
 homeassistant/generated/integrations.json     | 25 ++-----------------
 script/hassfest/config_flow.py                |  9 ++++---
 5 files changed, 14 insertions(+), 32 deletions(-)
 delete mode 100644 homeassistant/brands/raspberry.json
 create mode 100644 homeassistant/brands/raspberry_pi.json

diff --git a/homeassistant/brands/raspberry.json b/homeassistant/brands/raspberry.json
deleted file mode 100644
index a0ec6f12699..00000000000
--- a/homeassistant/brands/raspberry.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "domain": "raspberry_pi",
-  "name": "Raspberry Pi",
-  "integrations": ["rpi_camera", "rpi_power", "remote_rpi_gpio"]
-}
diff --git a/homeassistant/brands/raspberry_pi.json b/homeassistant/brands/raspberry_pi.json
new file mode 100644
index 00000000000..a64da918ccc
--- /dev/null
+++ b/homeassistant/brands/raspberry_pi.json
@@ -0,0 +1,5 @@
+{
+  "domain": "raspberry_pi",
+  "name": "Raspberry Pi",
+  "integrations": ["raspberry_pi", "rpi_camera", "rpi_power", "remote_rpi_gpio"]
+}
diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json
index f1613b2ba6b..64a7b0e48a8 100644
--- a/homeassistant/components/remote_rpi_gpio/manifest.json
+++ b/homeassistant/components/remote_rpi_gpio/manifest.json
@@ -1,6 +1,6 @@
 {
   "domain": "remote_rpi_gpio",
-  "name": "remote_rpi_gpio",
+  "name": "Raspberry Pi Remote GPIO",
   "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio",
   "requirements": ["gpiozero==1.6.2", "pigpio==1.78"],
   "codeowners": [],
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index a26114843b6..59caad1d554 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -2057,11 +2057,6 @@
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
-    "hardkernel": {
-      "name": "Hardkernel",
-      "integration_type": "hardware",
-      "config_flow": false
-    },
     "harman_kardon_avr": {
       "name": "Harman Kardon AVR",
       "integration_type": "hub",
@@ -2176,16 +2171,6 @@
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
-    "homeassistant_sky_connect": {
-      "name": "Home Assistant Sky Connect",
-      "integration_type": "hardware",
-      "config_flow": false
-    },
-    "homeassistant_yellow": {
-      "name": "Home Assistant Yellow",
-      "integration_type": "hardware",
-      "config_flow": false
-    },
     "homematic": {
       "name": "Homematic",
       "integrations": {
@@ -4238,7 +4223,7 @@
       "config_flow": false,
       "iot_class": "local_polling"
     },
-    "raspberry": {
+    "raspberry_pi": {
       "name": "Raspberry Pi",
       "integrations": {
         "rpi_camera": {
@@ -4254,15 +4239,10 @@
         "remote_rpi_gpio": {
           "integration_type": "hub",
           "iot_class": "local_push",
-          "name": "remote_rpi_gpio"
+          "name": "Raspberry Pi Remote GPIO"
         }
       }
     },
-    "raspberry_pi": {
-      "name": "Raspberry Pi",
-      "integration_type": "hardware",
-      "config_flow": false
-    },
     "raspyrfm": {
       "name": "RaspyRFM",
       "integration_type": "hub",
@@ -6183,7 +6163,6 @@
       "iot_class": "local_push"
     }
   },
-  "hardware": {},
   "helper": {
     "counter": {
       "name": "Counter",
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index 0e055e69768..9cebb37d371 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -104,7 +104,11 @@ def _populate_brand_integrations(
     brand_metadata.setdefault("integrations", {})
     for domain in sub_integrations:
         integration = integrations.get(domain)
-        if not integration or integration.integration_type in ("entity", "system"):
+        if not integration or integration.integration_type in (
+            "entity",
+            "hardware",
+            "system",
+        ):
             continue
         metadata = {
             "integration_type": integration.integration_type,
@@ -131,7 +135,6 @@ def _generate_integrations(
 
     result = {
         "integration": {},
-        "hardware": {},
         "helper": {},
         "translated_name": set(),
     }
@@ -176,7 +179,7 @@ def _generate_integrations(
             result["integration"][domain] = metadata
         else:  # integration
             integration = integrations[domain]
-            if integration.integration_type in ("entity", "system"):
+            if integration.integration_type in ("entity", "system", "hardware"):
                 continue
 
             if integration.translated_name:
-- 
GitLab


From d21417c8e59f6bc81ade8375938624f3ca0fb330 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Tue, 25 Oct 2022 22:06:56 +0300
Subject: [PATCH 813/985] Update devcontainer  appPort to allow connections
 from external IPs (#79730)

---
 .devcontainer/devcontainer.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index fe0d53a92ef..1711ab68fde 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,7 +5,7 @@
   "postCreateCommand": "script/setup",
   "postStartCommand": "script/bootstrap",
   "containerEnv": { "DEVCONTAINER": "1" },
-  "appPort": 8123,
+  "appPort": ["8123:8123"],
   "runArgs": ["-e", "GIT_EDITOR=code --wait"],
   "extensions": [
     "ms-python.vscode-pylance",
-- 
GitLab


From 775f4e9e0de512e9642f2e854bf382d0dc0586a5 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
Date: Tue, 25 Oct 2022 22:27:54 +0300
Subject: [PATCH 814/985] Clean-up Shelly legacy update entities (#80961)

---
 homeassistant/components/shelly/update.py | 28 ++++++++++++++--
 tests/components/shelly/test_update.py    | 40 +++++++++++++++++++++++
 2 files changed, 66 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py
index 3d562bf86e5..d801d0d03b3 100644
--- a/homeassistant/components/shelly/update.py
+++ b/homeassistant/components/shelly/update.py
@@ -19,9 +19,10 @@ from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.util import slugify
 
 from .const import CONF_SLEEP_PERIOD
-from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
+from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
 from .entity import (
     RestEntityDescription,
     RpcEntityDescription,
@@ -30,7 +31,12 @@ from .entity import (
     async_setup_entry_rest,
     async_setup_entry_rpc,
 )
-from .utils import get_device_entry_gen
+from .utils import (
+    async_remove_shelly_entity,
+    get_block_device_name,
+    get_device_entry_gen,
+    get_rpc_device_name,
+)
 
 LOGGER = logging.getLogger(__name__)
 
@@ -117,10 +123,28 @@ async def async_setup_entry(
 ) -> None:
     """Set up update entities for Shelly component."""
     if get_device_entry_gen(config_entry) == 2:
+        # Remove legacy update binary sensor & buttons, remove in 2023.2.0
+        rpc_coordinator = get_entry_data(hass)[config_entry.entry_id].rpc
+        assert rpc_coordinator
+        mac = rpc_coordinator.mac
+        async_remove_shelly_entity(hass, "binary_sensor", f"{mac}-sys-fwupdate")
+        device_name = slugify(get_rpc_device_name(rpc_coordinator.device))
+        async_remove_shelly_entity(hass, "button", f"{device_name}_ota_update")
+        async_remove_shelly_entity(hass, "button", f"{device_name}_ota_update_beta")
+
         return async_setup_entry_rpc(
             hass, config_entry, async_add_entities, RPC_UPDATES, RpcUpdateEntity
         )
 
+    # Remove legacy update binary sensor & buttons, remove in 2023.2.0
+    block_coordinator = get_entry_data(hass)[config_entry.entry_id].block
+    assert block_coordinator
+    mac = block_coordinator.mac
+    async_remove_shelly_entity(hass, "binary_sensor", f"{mac}-fwupdate")
+    device_name = slugify(get_block_device_name(block_coordinator.device))
+    async_remove_shelly_entity(hass, "button", f"{device_name}_ota_update")
+    async_remove_shelly_entity(hass, "button", f"{device_name}_ota_update_beta")
+
     if not config_entry.data[CONF_SLEEP_PERIOD]:
         async_setup_entry_rest(
             hass,
diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py
index 6a1ecc58245..4da81e076ae 100644
--- a/tests/components/shelly/test_update.py
+++ b/tests/components/shelly/test_update.py
@@ -5,6 +5,8 @@ from unittest.mock import AsyncMock
 from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
 import pytest
 
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
 from homeassistant.components.shelly.const import DOMAIN, REST_SENSORS_UPDATE_INTERVAL
 from homeassistant.components.update import (
     ATTR_IN_PROGRESS,
@@ -25,6 +27,44 @@ from . import MOCK_MAC, init_integration
 from tests.common import async_fire_time_changed
 
 
+@pytest.mark.parametrize(
+    "gen, domain, unique_id, object_id",
+    [
+        (1, BINARY_SENSOR_DOMAIN, f"{MOCK_MAC}-fwupdate", "firmware_update"),
+        (1, BUTTON_DOMAIN, "test_name_ota_update", "ota_update"),
+        (1, BUTTON_DOMAIN, "test_name_ota_update_beta", "ota_update_beta"),
+        (2, BINARY_SENSOR_DOMAIN, f"{MOCK_MAC}-sys-fwupdate", "firmware_update"),
+        (2, BUTTON_DOMAIN, "test_name_ota_update", "ota_update"),
+        (2, BUTTON_DOMAIN, "test_name_ota_update_beta", "ota_update_beta"),
+    ],
+)
+async def test_remove_legacy_entities(
+    hass: HomeAssistant,
+    gen,
+    domain,
+    unique_id,
+    object_id,
+    mock_block_device,
+    mock_rpc_device,
+):
+    """Test removes legacy update entities."""
+    entity_id = f"{domain}.test_name_{object_id}"
+    entity_registry = async_get(hass)
+    entity_registry.async_get_or_create(
+        domain,
+        DOMAIN,
+        unique_id,
+        suggested_object_id=f"test_name_{object_id}",
+        disabled_by=None,
+    )
+
+    assert entity_registry.async_get(entity_id) is not None
+
+    await init_integration(hass, gen)
+
+    assert entity_registry.async_get(entity_id) is None
+
+
 async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch):
     """Test block device update entity."""
     entity_registry = async_get(hass)
-- 
GitLab


From 2af58ad609c2009c1eba396c404d661b4bdab0a1 Mon Sep 17 00:00:00 2001
From: Aaron Bach <bachya1208@gmail.com>
Date: Tue, 25 Oct 2022 14:23:54 -0600
Subject: [PATCH 815/985] Set integration type on codeowned integrations
 @bachya (#80974)

---
 .../components/airvisual/manifest.json        |  3 ++-
 .../components/ambient_station/manifest.json  |  3 ++-
 .../components/guardian/manifest.json         |  3 ++-
 homeassistant/components/iqvia/manifest.json  |  3 ++-
 homeassistant/components/notion/manifest.json |  3 ++-
 homeassistant/components/openuv/manifest.json |  3 ++-
 .../components/rainmachine/manifest.json      |  3 ++-
 .../components/recollect_waste/manifest.json  |  3 ++-
 .../components/ridwell/manifest.json          |  3 ++-
 .../components/simplisafe/manifest.json       |  3 ++-
 homeassistant/components/slack/manifest.json  |  3 ++-
 homeassistant/components/tile/manifest.json   |  3 ++-
 .../components/watttime/manifest.json         |  3 ++-
 homeassistant/components/yi/manifest.json     |  3 ++-
 homeassistant/generated/integrations.json     | 20 +++++++++----------
 15 files changed, 38 insertions(+), 24 deletions(-)

diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json
index 9a6279f34a6..73bbf0cd589 100644
--- a/homeassistant/components/airvisual/manifest.json
+++ b/homeassistant/components/airvisual/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["pyairvisual==2022.07.0"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["pyairvisual", "pysmb"]
+  "loggers": ["pyairvisual", "pysmb"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json
index 21f7e251269..473958f680d 100644
--- a/homeassistant/components/ambient_station/manifest.json
+++ b/homeassistant/components/ambient_station/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["aioambient==2021.11.0"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_push",
-  "loggers": ["aioambient"]
+  "loggers": ["aioambient"],
+  "integration_type": "hub"
 }
diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json
index 7fab487563c..44527f95d29 100644
--- a/homeassistant/components/guardian/manifest.json
+++ b/homeassistant/components/guardian/manifest.json
@@ -21,5 +21,6 @@
       "macaddress": "30AEA4*"
     }
   ],
-  "loggers": ["aioguardian"]
+  "loggers": ["aioguardian"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json
index 9da0af32da3..2bf8eee8469 100644
--- a/homeassistant/components/iqvia/manifest.json
+++ b/homeassistant/components/iqvia/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["numpy==1.23.2", "pyiqvia==2022.04.0"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["pyiqvia"]
+  "loggers": ["pyiqvia"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json
index fa19ef81c8c..1aac9693740 100644
--- a/homeassistant/components/notion/manifest.json
+++ b/homeassistant/components/notion/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["aionotion==3.0.2"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["aionotion"]
+  "loggers": ["aionotion"],
+  "integration_type": "hub"
 }
diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json
index cd9bbfa8edf..5e89f495b03 100644
--- a/homeassistant/components/openuv/manifest.json
+++ b/homeassistant/components/openuv/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["pyopenuv==2022.04.0"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["pyopenuv"]
+  "loggers": ["pyopenuv"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json
index 08fef4973bc..a41db1d18f9 100644
--- a/homeassistant/components/rainmachine/manifest.json
+++ b/homeassistant/components/rainmachine/manifest.json
@@ -15,5 +15,6 @@
       "name": "rainmachine*"
     }
   ],
-  "loggers": ["regenmaschine"]
+  "loggers": ["regenmaschine"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json
index 68fc0e2c309..e80f8e35088 100644
--- a/homeassistant/components/recollect_waste/manifest.json
+++ b/homeassistant/components/recollect_waste/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["aiorecollect==1.0.8"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["aiorecollect"]
+  "loggers": ["aiorecollect"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json
index 0dbb33b2435..aec0faf5dd3 100644
--- a/homeassistant/components/ridwell/manifest.json
+++ b/homeassistant/components/ridwell/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["aioridwell==2022.03.0"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["aioridwell"]
+  "loggers": ["aioridwell"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index b08799e4082..dcb06aa2825 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -12,5 +12,6 @@
       "macaddress": "30AEA4*"
     }
   ],
-  "loggers": ["simplipy"]
+  "loggers": ["simplipy"],
+  "integration_type": "hub"
 }
diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json
index 57c1690e647..ab7be4ce514 100644
--- a/homeassistant/components/slack/manifest.json
+++ b/homeassistant/components/slack/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["slackclient==2.5.0"],
   "codeowners": ["@bachya", "@tkdrob"],
   "iot_class": "cloud_push",
-  "loggers": ["slack"]
+  "loggers": ["slack"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json
index dd0e78007f6..00b3313c91c 100644
--- a/homeassistant/components/tile/manifest.json
+++ b/homeassistant/components/tile/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["pytile==2022.02.0"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["pytile"]
+  "loggers": ["pytile"],
+  "integration_type": "hub"
 }
diff --git a/homeassistant/components/watttime/manifest.json b/homeassistant/components/watttime/manifest.json
index 1f233b5e105..b661b968373 100644
--- a/homeassistant/components/watttime/manifest.json
+++ b/homeassistant/components/watttime/manifest.json
@@ -6,5 +6,6 @@
   "requirements": ["aiowatttime==0.1.1"],
   "codeowners": ["@bachya"],
   "iot_class": "cloud_polling",
-  "loggers": ["aiowatttime"]
+  "loggers": ["aiowatttime"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json
index d0560ff13f5..10f2e4e3d94 100644
--- a/homeassistant/components/yi/manifest.json
+++ b/homeassistant/components/yi/manifest.json
@@ -6,5 +6,6 @@
   "dependencies": ["ffmpeg"],
   "codeowners": ["@bachya"],
   "iot_class": "local_polling",
-  "loggers": ["aioftp"]
+  "loggers": ["aioftp"],
+  "integration_type": "device"
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 59caad1d554..6b64e05a73b 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -114,7 +114,7 @@
     },
     "airvisual": {
       "name": "AirVisual",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -2047,7 +2047,7 @@
     },
     "guardian": {
       "name": "Elexa Guardian",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -2450,7 +2450,7 @@
     },
     "iqvia": {
       "name": "IQVIA",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -3750,7 +3750,7 @@
     },
     "openuv": {
       "name": "OpenUV",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4213,7 +4213,7 @@
     },
     "rainmachine": {
       "name": "RainMachine",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": true,
       "iot_class": "local_polling"
     },
@@ -4262,7 +4262,7 @@
     },
     "recollect_waste": {
       "name": "ReCollect Waste",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4339,7 +4339,7 @@
     },
     "ridwell": {
       "name": "Ridwell",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -4719,7 +4719,7 @@
     },
     "slack": {
       "name": "Slack",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_push"
     },
@@ -5833,7 +5833,7 @@
     },
     "watttime": {
       "name": "WattTime",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
@@ -6068,7 +6068,7 @@
     },
     "yi": {
       "name": "Yi Home Cameras",
-      "integration_type": "hub",
+      "integration_type": "device",
       "config_flow": false,
       "iot_class": "local_polling"
     },
-- 
GitLab


From 115a1ceea0c568ffb466e03b4ab322f253ccf151 Mon Sep 17 00:00:00 2001
From: Robert Svensson <Kane610@users.noreply.github.com>
Date: Tue, 25 Oct 2022 22:36:51 +0200
Subject: [PATCH 816/985] Rewrite UniFi block client switch (#80969)

* Refactor UniFi block client switch entities

* Use new switch loader

* Rename lambdas

* Use is_on rather than _attr_is_on when applicable
---
 homeassistant/components/unifi/switch.py | 176 +++++++++++++++--------
 tests/components/unifi/test_switch.py    |  28 ++--
 2 files changed, 124 insertions(+), 80 deletions(-)

diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py
index fbfb6b7335a..65d0041187e 100644
--- a/homeassistant/components/unifi/switch.py
+++ b/homeassistant/components/unifi/switch.py
@@ -12,17 +12,17 @@ from dataclasses import dataclass
 from typing import Any, Generic, TypeVar
 
 from aiounifi.interfaces.api_handlers import ItemEvent
+from aiounifi.interfaces.clients import Clients
 from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups
 from aiounifi.interfaces.outlets import Outlets
 from aiounifi.interfaces.ports import Ports
-from aiounifi.models.api import SOURCE_EVENT
 from aiounifi.models.client import ClientBlockRequest
 from aiounifi.models.device import (
     DeviceSetOutletRelayRequest,
     DeviceSetPoePortModeRequest,
 )
 from aiounifi.models.dpi_restriction_app import DPIRestrictionAppEnableRequest
-from aiounifi.models.event import EventKey
+from aiounifi.models.event import Event, EventKey
 
 from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity
 from homeassistant.config_entries import ConfigEntry
@@ -58,12 +58,12 @@ T = TypeVar("T")
 class UnifiEntityLoader(Generic[T]):
     """Validate and load entities from different UniFi handlers."""
 
-    config_option_fn: Callable[[UniFiController], bool]
-    entity_cls: type[UnifiDPIRestrictionSwitch] | type[UnifiOutletSwitch] | type[
-        UnifiPoePortSwitch
-    ] | type[UnifiDPIRestrictionSwitch]
+    allowed_fn: Callable[[UniFiController, str], bool]
+    entity_cls: type[UnifiBlockClientSwitch] | type[UnifiDPIRestrictionSwitch] | type[
+        UnifiOutletSwitch
+    ] | type[UnifiPoePortSwitch] | type[UnifiDPIRestrictionSwitch]
     handler_fn: Callable[[UniFiController], T]
-    value_fn: Callable[[T, str], bool | None]
+    supported_fn: Callable[[T, str], bool | None]
 
 
 async def async_setup_entry(
@@ -113,9 +113,6 @@ async def async_setup_entry(
         devices: set = controller.api.devices,
     ) -> None:
         """Update the values of the controller."""
-        if controller.option_block_clients:
-            add_block_entities(controller, async_add_entities, clients)
-
         if controller.option_poe_clients:
             add_poe_entities(controller, async_add_entities, clients, known_poe_clients)
 
@@ -136,14 +133,14 @@ async def async_setup_entry(
         @callback
         def async_create_entity(event: ItemEvent, obj_id: str) -> None:
             """Create UniFi entity."""
-            if not loader.config_option_fn(controller) or not loader.value_fn(
+            if not loader.allowed_fn(controller, obj_id) or not loader.supported_fn(
                 api_handler, obj_id
             ):
                 return
 
             entity = loader.entity_cls(obj_id, controller)
             if event == ItemEvent.ADDED:
-                async_add_entities(entities)
+                async_add_entities([entity])
                 return
             entities.append(entity)
 
@@ -157,21 +154,6 @@ async def async_setup_entry(
         async_load_entities(unifi_loader)
 
 
-@callback
-def add_block_entities(controller, async_add_entities, clients):
-    """Add new switch entities from the controller."""
-    switches = []
-
-    for mac in controller.option_block_clients:
-        if mac in controller.entities[DOMAIN][BLOCK_SWITCH] or mac not in clients:
-            continue
-
-        client = controller.api.clients[mac]
-        switches.append(UniFiBlockClientSwitch(client, controller))
-
-    async_add_entities(switches)
-
-
 @callback
 def add_poe_entities(controller, async_add_entities, clients, known_poe_clients):
     """Add new switch entities from the controller."""
@@ -319,59 +301,123 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity):
             await self.remove_item({self.client.mac})
 
 
-class UniFiBlockClientSwitch(UniFiClient, SwitchEntity):
+class UnifiBlockClientSwitch(SwitchEntity):
     """Representation of a blockable client."""
 
-    DOMAIN = DOMAIN
-    TYPE = BLOCK_SWITCH
-
+    _attr_device_class = SwitchDeviceClass.SWITCH
     _attr_entity_category = EntityCategory.CONFIG
+    _attr_has_entity_name = True
+    _attr_icon = "mdi:ethernet"
+    _attr_should_poll = False
 
-    def __init__(self, client, controller):
+    def __init__(self, obj_id: str, controller: UniFiController) -> None:
         """Set up block switch."""
-        super().__init__(client, controller)
+        controller.entities[DOMAIN][BLOCK_SWITCH].add(obj_id)
+        self._obj_id = obj_id
+        self.controller = controller
+
+        self._removed = False
+
+        client = controller.api.clients[obj_id]
+        self._attr_available = controller.available
+        self._attr_is_on = not client.blocked
+        self._attr_unique_id = f"{BLOCK_SWITCH}-{obj_id}"
+        self._attr_device_info = DeviceInfo(
+            connections={(CONNECTION_NETWORK_MAC, obj_id)},
+            default_manufacturer=client.oui,
+            default_name=client.name or client.hostname,
+        )
+
+    async def async_added_to_hass(self) -> None:
+        """Entity created."""
+        self.async_on_remove(
+            self.controller.api.clients.subscribe(self.async_signalling_callback)
+        )
+        self.async_on_remove(
+            self.controller.api.events.subscribe(
+                self.async_event_callback, CLIENT_BLOCKED + CLIENT_UNBLOCKED
+            )
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass, self.controller.signal_remove, self.remove_item
+            )
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass, self.controller.signal_options_update, self.options_updated
+            )
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                self.controller.signal_reachable,
+                self.async_signal_reachable_callback,
+            )
+        )
 
-        self._is_blocked = client.blocked
+    async def async_will_remove_from_hass(self) -> None:
+        """Disconnect object when removed."""
+        self.controller.entities[DOMAIN][BLOCK_SWITCH].remove(self._obj_id)
 
     @callback
-    def async_update_callback(self) -> None:
+    def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
         """Update the clients state."""
-        if (
-            self.client.last_updated == SOURCE_EVENT
-            and self.client.event.key in CLIENT_BLOCKED + CLIENT_UNBLOCKED
-        ):
-            self._is_blocked = self.client.event.key in CLIENT_BLOCKED
+        if event == ItemEvent.DELETED:
+            self.hass.async_create_task(self.remove_item({self._obj_id}))
+            return
 
-        super().async_update_callback()
+        client = self.controller.api.clients[self._obj_id]
+        self._attr_is_on = not client.blocked
+        self._attr_available = self.controller.available
+        self.async_write_ha_state()
 
-    @property
-    def is_on(self):
-        """Return true if client is allowed to connect."""
-        return not self._is_blocked
+    @callback
+    def async_event_callback(self, event: Event) -> None:
+        """Event subscription callback."""
+        if event.mac != self._obj_id:
+            return
+        if event.key in CLIENT_BLOCKED + CLIENT_UNBLOCKED:
+            self._attr_is_on = event.key in CLIENT_UNBLOCKED
+        self._attr_available = self.controller.available
+        self.async_write_ha_state()
+
+    @callback
+    def async_signal_reachable_callback(self) -> None:
+        """Call when controller connection state change."""
+        self.async_signalling_callback(ItemEvent.ADDED, self._obj_id)
 
     async def async_turn_on(self, **kwargs: Any) -> None:
         """Turn on connectivity for client."""
         await self.controller.api.request(
-            ClientBlockRequest.create(self.client.mac, False)
+            ClientBlockRequest.create(self._obj_id, False)
         )
 
     async def async_turn_off(self, **kwargs: Any) -> None:
         """Turn off connectivity for client."""
-        await self.controller.api.request(
-            ClientBlockRequest.create(self.client.mac, True)
-        )
+        await self.controller.api.request(ClientBlockRequest.create(self._obj_id, True))
 
     @property
     def icon(self) -> str:
         """Return the icon to use in the frontend."""
-        if self._is_blocked:
+        if not self.is_on:
             return "mdi:network-off"
         return "mdi:network"
 
     async def options_updated(self) -> None:
         """Config entry options are updated, remove entity if option is disabled."""
-        if self.client.mac not in self.controller.option_block_clients:
-            await self.remove_item({self.client.mac})
+        if self._obj_id not in self.controller.option_block_clients:
+            await self.remove_item({self._obj_id})
+
+    async def remove_item(self, keys: set) -> None:
+        """Remove entity if key is part of set."""
+        if self._obj_id not in keys or self._removed:
+            return
+        self._removed = True
+        if self.registry_entry:
+            er.async_get(self.hass).async_remove(self.entity_id)
+        else:
+            await self.async_remove(force_remove=True)
 
 
 class UnifiDPIRestrictionSwitch(SwitchEntity):
@@ -379,7 +425,7 @@ class UnifiDPIRestrictionSwitch(SwitchEntity):
 
     _attr_entity_category = EntityCategory.CONFIG
 
-    def __init__(self, obj_id: str, controller):
+    def __init__(self, obj_id: str, controller: UniFiController) -> None:
         """Set up dpi switch."""
         controller.entities[DOMAIN][DPI_SWITCH].add(obj_id)
         self._obj_id = obj_id
@@ -456,7 +502,7 @@ class UnifiDPIRestrictionSwitch(SwitchEntity):
     @property
     def icon(self):
         """Return the icon to use in the frontend."""
-        if self._attr_is_on:
+        if self.is_on:
             return "mdi:network"
         return "mdi:network-off"
 
@@ -516,7 +562,7 @@ class UnifiOutletSwitch(SwitchEntity):
     _attr_has_entity_name = True
     _attr_should_poll = False
 
-    def __init__(self, obj_id: str, controller) -> None:
+    def __init__(self, obj_id: str, controller: UniFiController) -> None:
         """Set up UniFi Network entity base."""
         self._device_mac, index = obj_id.split("_", 1)
         self._index = int(index)
@@ -591,7 +637,7 @@ class UnifiPoePortSwitch(SwitchEntity):
     _attr_icon = "mdi:ethernet"
     _attr_should_poll = False
 
-    def __init__(self, obj_id: str, controller) -> None:
+    def __init__(self, obj_id: str, controller: UniFiController) -> None:
         """Set up UniFi Network entity base."""
         self._device_mac, index = obj_id.split("_", 1)
         self._index = int(index)
@@ -657,22 +703,28 @@ class UnifiPoePortSwitch(SwitchEntity):
 
 
 UNIFI_LOADERS: tuple[UnifiEntityLoader, ...] = (
+    UnifiEntityLoader[Clients](
+        allowed_fn=lambda controller, obj_id: obj_id in controller.option_block_clients,
+        entity_cls=UnifiBlockClientSwitch,
+        handler_fn=lambda contrlr: contrlr.api.clients,
+        supported_fn=lambda handler, obj_id: True,
+    ),
     UnifiEntityLoader[DPIRestrictionGroups](
-        config_option_fn=lambda controller: controller.option_dpi_restrictions,
+        allowed_fn=lambda controller, obj_id: controller.option_dpi_restrictions,
         entity_cls=UnifiDPIRestrictionSwitch,
         handler_fn=lambda controller: controller.api.dpi_groups,
-        value_fn=lambda handler, index: bool(handler[index].dpiapp_ids),
+        supported_fn=lambda handler, obj_id: bool(handler[obj_id].dpiapp_ids),
     ),
     UnifiEntityLoader[Outlets](
-        config_option_fn=lambda controller: True,
+        allowed_fn=lambda controller, obj_id: True,
         entity_cls=UnifiOutletSwitch,
         handler_fn=lambda controller: controller.api.outlets,
-        value_fn=lambda handler, index: handler[index].has_relay,
+        supported_fn=lambda handler, obj_id: handler[obj_id].has_relay,
     ),
     UnifiEntityLoader[Ports](
-        config_option_fn=lambda controller: True,
+        allowed_fn=lambda controller, obj_id: True,
         entity_cls=UnifiPoePortSwitch,
         handler_fn=lambda controller: controller.api.ports,
-        value_fn=lambda handler, index: handler[index].port_poe,
+        supported_fn=lambda handler, obj_id: handler[obj_id].port_poe,
     ),
 )
diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py
index db0b358179c..e6357b03172 100644
--- a/tests/components/unifi/test_switch.py
+++ b/tests/components/unifi/test_switch.py
@@ -54,7 +54,7 @@ CLIENT_1 = {
     "mac": "00:00:00:00:00:01",
     "name": "POE Client 1",
     "oui": "Producer",
-    "sw_mac": "00:00:00:00:01:01",
+    "sw_mac": "10:00:00:00:01:01",
     "sw_port": 1,
     "wired-rx_bytes": 1234000000,
     "wired-tx_bytes": 5678000000,
@@ -67,7 +67,7 @@ CLIENT_2 = {
     "mac": "00:00:00:00:00:02",
     "name": "POE Client 2",
     "oui": "Producer",
-    "sw_mac": "00:00:00:00:01:01",
+    "sw_mac": "10:00:00:00:01:01",
     "sw_port": 2,
     "wired-rx_bytes": 1234000000,
     "wired-tx_bytes": 5678000000,
@@ -80,7 +80,7 @@ CLIENT_3 = {
     "mac": "00:00:00:00:00:03",
     "name": "Non-POE Client 3",
     "oui": "Producer",
-    "sw_mac": "00:00:00:00:01:01",
+    "sw_mac": "10:00:00:00:01:01",
     "sw_port": 3,
     "wired-rx_bytes": 1234000000,
     "wired-tx_bytes": 5678000000,
@@ -93,7 +93,7 @@ CLIENT_4 = {
     "mac": "00:00:00:00:00:04",
     "name": "Non-POE Client 4",
     "oui": "Producer",
-    "sw_mac": "00:00:00:00:01:01",
+    "sw_mac": "10:00:00:00:01:01",
     "sw_port": 4,
     "wired-rx_bytes": 1234000000,
     "wired-tx_bytes": 5678000000,
@@ -107,7 +107,7 @@ POE_SWITCH_CLIENTS = [
         "mac": "00:00:00:00:00:01",
         "name": "POE Client 1",
         "oui": "Producer",
-        "sw_mac": "00:00:00:00:01:01",
+        "sw_mac": "10:00:00:00:01:01",
         "sw_port": 1,
         "wired-rx_bytes": 1234000000,
         "wired-tx_bytes": 5678000000,
@@ -120,7 +120,7 @@ POE_SWITCH_CLIENTS = [
         "mac": "00:00:00:00:00:02",
         "name": "POE Client 2",
         "oui": "Producer",
-        "sw_mac": "00:00:00:00:01:01",
+        "sw_mac": "10:00:00:00:01:01",
         "sw_port": 1,
         "wired-rx_bytes": 1234000000,
         "wired-tx_bytes": 5678000000,
@@ -131,7 +131,7 @@ DEVICE_1 = {
     "board_rev": 2,
     "device_id": "mock-id",
     "ip": "10.0.1.1",
-    "mac": "00:00:00:00:01:01",
+    "mac": "10:00:00:00:01:01",
     "last_seen": 1562600145,
     "model": "US16P150",
     "name": "mock-name",
@@ -650,7 +650,7 @@ async def test_switches(hass, aioclient_mock):
     assert switch_1 is not None
     assert switch_1.state == "on"
     assert switch_1.attributes["power"] == "2.56"
-    assert switch_1.attributes[SWITCH_DOMAIN] == "00:00:00:00:01:01"
+    assert switch_1.attributes[SWITCH_DOMAIN] == "10:00:00:00:01:01"
     assert switch_1.attributes["port"] == 1
     assert switch_1.attributes["poe_mode"] == "auto"
 
@@ -1027,21 +1027,13 @@ async def test_new_client_discovered_on_block_control(
     )
 
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
-
-    blocked = hass.states.get("switch.block_client_1")
-    assert blocked is None
+    assert hass.states.get("switch.block_client_1") is None
 
     mock_unifi_websocket(message=MessageKey.CLIENT, data=BLOCKED)
     await hass.async_block_till_done()
 
-    assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
-
-    mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_BLOCKED_CLIENT_CONNECTED)
-    await hass.async_block_till_done()
-
     assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
-    blocked = hass.states.get("switch.block_client_1")
-    assert blocked is not None
+    assert hass.states.get("switch.block_client_1") is not None
 
 
 async def test_option_block_clients(hass, aioclient_mock):
-- 
GitLab


From 1b3c383558fa18f926654c5239287de1fbb4a4b7 Mon Sep 17 00:00:00 2001
From: Raman Gupta <7243222+raman325@users.noreply.github.com>
Date: Tue, 25 Oct 2022 17:31:44 -0400
Subject: [PATCH 817/985] Add integration_type to vizio, tomorrowio, zwave_js
 (#80975)

---
 homeassistant/components/tomorrowio/manifest.json | 4 +++-
 homeassistant/components/vizio/manifest.json      | 3 ++-
 homeassistant/components/zwave_js/manifest.json   | 3 ++-
 homeassistant/generated/integrations.json         | 2 +-
 4 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json
index 8c097d46eb7..7c3b688f075 100644
--- a/homeassistant/components/tomorrowio/manifest.json
+++ b/homeassistant/components/tomorrowio/manifest.json
@@ -5,5 +5,7 @@
   "documentation": "https://www.home-assistant.io/integrations/tomorrowio",
   "requirements": ["pytomorrowio==0.3.5"],
   "codeowners": ["@raman325", "@lymanepp"],
-  "iot_class": "cloud_polling"
+  "iot_class": "cloud_polling",
+  "loggers": ["pytomorrowio"],
+  "integration_type": "service"
 }
diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json
index 5b534f861cc..3fe0ac45885 100644
--- a/homeassistant/components/vizio/manifest.json
+++ b/homeassistant/components/vizio/manifest.json
@@ -8,5 +8,6 @@
   "zeroconf": ["_viziocast._tcp.local."],
   "quality_scale": "platinum",
   "iot_class": "local_polling",
-  "loggers": ["pyvizio"]
+  "loggers": ["pyvizio"],
+  "integration_type": "hub"
 }
diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json
index 5b085ab0bb3..38c1e8b181f 100644
--- a/homeassistant/components/zwave_js/manifest.json
+++ b/homeassistant/components/zwave_js/manifest.json
@@ -21,5 +21,6 @@
     }
   ],
   "zeroconf": ["_zwave-js-server._tcp.local."],
-  "loggers": ["zwave_js_server"]
+  "loggers": ["zwave_js_server"],
+  "integration_type": "hub"
 }
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index 6b64e05a73b..f3b9fea78bf 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -5400,7 +5400,7 @@
     },
     "tomorrowio": {
       "name": "Tomorrow.io",
-      "integration_type": "hub",
+      "integration_type": "service",
       "config_flow": true,
       "iot_class": "cloud_polling"
     },
-- 
GitLab


From a205ba765641b1552879712b30c42a730db25084 Mon Sep 17 00:00:00 2001
From: Rami Mosleh <engrbm87@gmail.com>
Date: Wed, 26 Oct 2022 00:53:17 +0300
Subject: [PATCH 818/985] Remove deprecated YAML in `android_ip_webcam`
 (#80875)

---
 .../components/android_ip_webcam/__init__.py  | 82 +------------------
 .../android_ip_webcam/config_flow.py          | 20 +----
 .../components/android_ip_webcam/const.py     | 32 --------
 .../android_ip_webcam/test_config_flow.py     | 30 -------
 .../components/android_ip_webcam/test_init.py | 19 -----
 5 files changed, 5 insertions(+), 178 deletions(-)

diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py
index d792b42d4bf..47307fb3690 100644
--- a/homeassistant/components/android_ip_webcam/__init__.py
+++ b/homeassistant/components/android_ip_webcam/__init__.py
@@ -2,37 +2,20 @@
 from __future__ import annotations
 
 from pydroid_ipcam import PyDroidIPCam
-import voluptuous as vol
 
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import (
     CONF_HOST,
-    CONF_NAME,
     CONF_PASSWORD,
     CONF_PORT,
-    CONF_SCAN_INTERVAL,
-    CONF_SENSORS,
-    CONF_SWITCHES,
-    CONF_TIMEOUT,
     CONF_USERNAME,
     Platform,
 )
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
-from homeassistant.helpers.typing import ConfigType
 
-from .const import (
-    CONF_MOTION_SENSOR,
-    DEFAULT_NAME,
-    DEFAULT_PORT,
-    DEFAULT_TIMEOUT,
-    DOMAIN,
-    SCAN_INTERVAL,
-    SENSORS,
-    SWITCHES,
-)
+from .const import DOMAIN
 from .coordinator import AndroidIPCamDataUpdateCoordinator
 
 PLATFORMS: list[Platform] = [
@@ -43,66 +26,7 @@ PLATFORMS: list[Platform] = [
 ]
 
 
-CONFIG_SCHEMA = vol.Schema(
-    vol.All(
-        cv.deprecated(DOMAIN),
-        {
-            DOMAIN: vol.All(
-                cv.ensure_list,
-                [
-                    vol.Schema(
-                        {
-                            vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
-                            vol.Required(CONF_HOST): cv.string,
-                            vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
-                            vol.Optional(
-                                CONF_TIMEOUT, default=DEFAULT_TIMEOUT
-                            ): cv.positive_int,
-                            vol.Optional(
-                                CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
-                            ): cv.time_period,
-                            vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
-                            vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
-                            vol.Optional(CONF_SWITCHES): vol.All(
-                                cv.ensure_list, [vol.In(SWITCHES)]
-                            ),
-                            vol.Optional(CONF_SENSORS): vol.All(
-                                cv.ensure_list, [vol.In(SENSORS)]
-                            ),
-                            vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
-                        }
-                    )
-                ],
-            )
-        },
-    ),
-    extra=vol.ALLOW_EXTRA,
-)
-
-
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
-    """Set up the IP Webcam component."""
-
-    if DOMAIN not in config:
-        return True
-
-    async_create_issue(
-        hass,
-        DOMAIN,
-        "deprecated_yaml",
-        breaks_in_ha_version="2022.11.0",
-        is_fixable=False,
-        severity=IssueSeverity.WARNING,
-        translation_key="deprecated_yaml",
-    )
-    for entry in config[DOMAIN]:
-        hass.async_create_task(
-            hass.config_entries.flow.async_init(
-                DOMAIN, context={"source": SOURCE_IMPORT}, data=entry
-            )
-        )
-
-    return True
+CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
 
 
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
diff --git a/homeassistant/components/android_ip_webcam/config_flow.py b/homeassistant/components/android_ip_webcam/config_flow.py
index c41a998ff54..2a26292fdd7 100644
--- a/homeassistant/components/android_ip_webcam/config_flow.py
+++ b/homeassistant/components/android_ip_webcam/config_flow.py
@@ -8,15 +8,7 @@ from pydroid_ipcam.exceptions import PyDroidIPCamException, Unauthorized
 import voluptuous as vol
 
 from homeassistant import config_entries
-from homeassistant.const import (
-    CONF_HOST,
-    CONF_NAME,
-    CONF_PASSWORD,
-    CONF_PORT,
-    CONF_SCAN_INTERVAL,
-    CONF_TIMEOUT,
-    CONF_USERNAME,
-)
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
 from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers import config_validation as cv
@@ -75,19 +67,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
         self._async_abort_entries_match(
             {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
         )
-        # to be removed when YAML import is removed
-        title = user_input.get(CONF_NAME) or user_input[CONF_HOST]
         if not (errors := await validate_input(self.hass, user_input)):
-            return self.async_create_entry(title=title, data=user_input)
+            return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
 
         return self.async_show_form(
             step_id="user",
             data_schema=STEP_USER_DATA_SCHEMA,
             errors=errors,
         )
-
-    async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
-        """Import a config entry from configuration.yaml."""
-        import_config.pop(CONF_SCAN_INTERVAL)
-        import_config.pop(CONF_TIMEOUT)
-        return await self.async_step_user(import_config)
diff --git a/homeassistant/components/android_ip_webcam/const.py b/homeassistant/components/android_ip_webcam/const.py
index 6672628d977..c4a6ab2fa23 100644
--- a/homeassistant/components/android_ip_webcam/const.py
+++ b/homeassistant/components/android_ip_webcam/const.py
@@ -4,38 +4,6 @@ from datetime import timedelta
 from typing import Final
 
 DOMAIN: Final = "android_ip_webcam"
-DEFAULT_NAME: Final = "IP Webcam"
 DEFAULT_PORT: Final = 8080
-DEFAULT_TIMEOUT: Final = 10
-
-CONF_MOTION_SENSOR: Final = "motion_sensor"
-
 MOTION_ACTIVE: Final = "motion_active"
 SCAN_INTERVAL: Final = timedelta(seconds=10)
-
-
-SWITCHES = [
-    "exposure_lock",
-    "ffc",
-    "focus",
-    "gps_active",
-    "motion_detect",
-    "night_vision",
-    "overlay",
-    "torch",
-    "whitebalance_lock",
-    "video_recording",
-]
-
-SENSORS = [
-    "audio_connections",
-    "battery_level",
-    "battery_temp",
-    "battery_voltage",
-    "light",
-    "motion",
-    "pressure",
-    "proximity",
-    "sound",
-    "video_connections",
-]
diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py
index d203ef15e63..881585ed5dc 100644
--- a/tests/components/android_ip_webcam/test_config_flow.py
+++ b/tests/components/android_ip_webcam/test_config_flow.py
@@ -1,5 +1,4 @@
 """Test the Android IP Webcam config flow."""
-from datetime import timedelta
 from unittest.mock import Mock, patch
 
 import aiohttp
@@ -45,35 +44,6 @@ async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None:
     assert len(mock_setup_entry.mock_calls) == 1
 
 
-async def test_import_flow_success(hass: HomeAssistant, aioclient_mock_fixture) -> None:
-    """Test a successful import of yaml."""
-    with patch(
-        "homeassistant.components.android_ip_webcam.async_setup_entry",
-        return_value=True,
-    ) as mock_setup_entry:
-        result2 = await hass.config_entries.flow.async_init(
-            DOMAIN,
-            context={"source": config_entries.SOURCE_IMPORT},
-            data={
-                "name": "IP Webcam",
-                "host": "1.1.1.1",
-                "port": 8080,
-                "timeout": 10,
-                "scan_interval": timedelta(seconds=30),
-            },
-        )
-        await hass.async_block_till_done()
-
-    assert result2["type"] == FlowResultType.CREATE_ENTRY
-    assert result2["title"] == "IP Webcam"
-    assert result2["data"] == {
-        "name": "IP Webcam",
-        "host": "1.1.1.1",
-        "port": 8080,
-    }
-    assert len(mock_setup_entry.mock_calls) == 1
-
-
 async def test_device_already_configured(
     hass: HomeAssistant, aioclient_mock_fixture
 ) -> None:
diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py
index 1fee1a5c388..fa5f551e9b1 100644
--- a/tests/components/android_ip_webcam/test_init.py
+++ b/tests/components/android_ip_webcam/test_init.py
@@ -1,8 +1,6 @@
 """Tests for the Android IP Webcam integration."""
 
 
-from collections.abc import Awaitable
-from typing import Callable
 from unittest.mock import Mock
 
 import aiohttp
@@ -10,10 +8,8 @@ import aiohttp
 from homeassistant.components.android_ip_webcam.const import DOMAIN
 from homeassistant.config_entries import ConfigEntryState
 from homeassistant.core import HomeAssistant
-from homeassistant.setup import async_setup_component
 
 from tests.common import MockConfigEntry
-from tests.components.repairs import get_repairs
 from tests.test_util.aiohttp import AiohttpClientMocker
 
 MOCK_CONFIG_DATA = {
@@ -25,21 +21,6 @@ MOCK_CONFIG_DATA = {
 }
 
 
-async def test_setup(
-    hass: HomeAssistant,
-    aioclient_mock_fixture,
-    hass_ws_client: Callable[
-        [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
-    ],
-) -> None:
-    """Test integration failed due to an error."""
-    assert await async_setup_component(hass, DOMAIN, {DOMAIN: [MOCK_CONFIG_DATA]})
-    assert hass.config_entries.async_entries(DOMAIN)
-    issues = await get_repairs(hass, hass_ws_client)
-    assert len(issues) == 1
-    assert issues[0]["issue_id"] == "deprecated_yaml"
-
-
 async def test_successful_config_entry(
     hass: HomeAssistant, aioclient_mock_fixture
 ) -> None:
-- 
GitLab


From d0ff6582e6b5906ce2cfea101f4b4437af1d9f24 Mon Sep 17 00:00:00 2001
From: GitHub Action <github-action@users.noreply.github.com>
Date: Wed, 26 Oct 2022 00:29:57 +0000
Subject: [PATCH 819/985] [ci skip] Translation update

---
 .../components/generic/translations/bg.json   |  6 +++
 .../components/generic/translations/ca.json   |  7 +++
 .../components/generic/translations/de.json   |  7 +++
 .../components/generic/translations/en.json   |  7 +++
 .../components/generic/translations/es.json   |  7 +++
 .../components/generic/translations/et.json   |  7 +++
 .../components/generic/translations/fr.json   |  3 ++
 .../components/generic/translations/it.json   |  7 +++
 .../components/generic/translations/no.json   |  7 +++
 .../components/generic/translations/pl.json   |  7 +++
 .../generic/translations/pt-BR.json           |  7 +++
 .../components/generic/translations/tr.json   |  7 +++
 .../generic/translations/zh-Hant.json         |  7 +++
 .../components/mqtt/translations/bg.json      |  6 +++
 .../components/mqtt/translations/ca.json      | 48 ++++++++++++++++--
 .../components/mqtt/translations/et.json      | 42 ++++++++++++++--
 .../components/mqtt/translations/hu.json      | 45 +++++++++++++++--
 .../components/mqtt/translations/it.json      | 36 +++++++++++++-
 .../components/mqtt/translations/no.json      | 49 +++++++++++++++++--
 .../components/mqtt/translations/pl.json      | 45 +++++++++++++++--
 .../components/mqtt/translations/ru.json      | 17 ++++++-
 .../components/mqtt/translations/zh-Hant.json |  2 +
 .../nibe_heatpump/translations/ca.json        | 13 +++--
 .../nibe_heatpump/translations/et.json        |  2 +-
 .../nibe_heatpump/translations/hu.json        | 11 ++++-
 .../nibe_heatpump/translations/no.json        | 13 +++--
 .../nibe_heatpump/translations/pl.json        | 11 ++++-
 .../nibe_heatpump/translations/tr.json        |  6 ++-
 .../components/oralb/translations/bg.json     | 21 ++++++++
 .../components/oralb/translations/ca.json     | 22 +++++++++
 .../components/oralb/translations/de.json     | 22 +++++++++
 .../components/oralb/translations/es.json     | 22 +++++++++
 .../components/oralb/translations/et.json     | 22 +++++++++
 .../components/oralb/translations/fr.json     | 22 +++++++++
 .../components/oralb/translations/hu.json     | 22 +++++++++
 .../components/oralb/translations/it.json     | 22 +++++++++
 .../components/oralb/translations/no.json     | 22 +++++++++
 .../components/oralb/translations/pl.json     | 22 +++++++++
 .../components/oralb/translations/pt-BR.json  | 22 +++++++++
 .../components/oralb/translations/tr.json     | 22 +++++++++
 .../oralb/translations/zh-Hant.json           | 22 +++++++++
 .../plugwise/translations/select.it.json      |  1 +
 .../plugwise/translations/select.zh-Hant.json |  6 +++
 .../components/pushover/translations/ca.json  |  4 ++
 .../components/pushover/translations/de.json  |  4 ++
 .../components/pushover/translations/en.json  |  8 ++-
 .../components/pushover/translations/es.json  |  4 ++
 .../components/pushover/translations/et.json  |  4 ++
 .../components/pushover/translations/it.json  |  3 ++
 .../components/pushover/translations/no.json  |  4 ++
 .../components/pushover/translations/pl.json  |  4 ++
 .../pushover/translations/pt-BR.json          |  4 ++
 .../components/pushover/translations/tr.json  |  4 ++
 .../pushover/translations/zh-Hant.json        |  3 ++
 .../components/sensibo/translations/ca.json   |  4 +-
 .../components/sensibo/translations/pl.json   |  4 +-
 .../sensibo/translations/sensor.ca.json       |  1 +
 .../sensibo/translations/sensor.hu.json       |  5 ++
 .../sensibo/translations/sensor.it.json       |  5 ++
 .../sensibo/translations/sensor.pl.json       |  5 ++
 .../sensibo/translations/sensor.ru.json       |  5 ++
 .../sensibo/translations/sensor.zh-Hant.json  |  5 ++
 .../components/sensor/translations/de.json    |  2 +
 .../components/sensor/translations/hu.json    |  2 +
 .../components/sensor/translations/no.json    |  2 +
 .../components/sensor/translations/pl.json    |  2 +
 .../components/sensor/translations/pt-BR.json |  2 +
 .../components/sensor/translations/tr.json    |  2 +
 .../sensor/translations/zh-Hant.json          |  2 +
 .../statistics/translations/ca.json           | 12 +++++
 .../statistics/translations/de.json           | 12 +++++
 .../statistics/translations/pl.json           | 12 +++++
 .../statistics/translations/pt-BR.json        | 12 +++++
 .../xiaomi_miio/translations/zh-Hant.json     |  3 +-
 74 files changed, 826 insertions(+), 43 deletions(-)
 create mode 100644 homeassistant/components/oralb/translations/bg.json
 create mode 100644 homeassistant/components/oralb/translations/ca.json
 create mode 100644 homeassistant/components/oralb/translations/de.json
 create mode 100644 homeassistant/components/oralb/translations/es.json
 create mode 100644 homeassistant/components/oralb/translations/et.json
 create mode 100644 homeassistant/components/oralb/translations/fr.json
 create mode 100644 homeassistant/components/oralb/translations/hu.json
 create mode 100644 homeassistant/components/oralb/translations/it.json
 create mode 100644 homeassistant/components/oralb/translations/no.json
 create mode 100644 homeassistant/components/oralb/translations/pl.json
 create mode 100644 homeassistant/components/oralb/translations/pt-BR.json
 create mode 100644 homeassistant/components/oralb/translations/tr.json
 create mode 100644 homeassistant/components/oralb/translations/zh-Hant.json
 create mode 100644 homeassistant/components/statistics/translations/ca.json
 create mode 100644 homeassistant/components/statistics/translations/de.json
 create mode 100644 homeassistant/components/statistics/translations/pl.json
 create mode 100644 homeassistant/components/statistics/translations/pt-BR.json

diff --git a/homeassistant/components/generic/translations/bg.json b/homeassistant/components/generic/translations/bg.json
index ed6779edf1d..8c6944af94a 100644
--- a/homeassistant/components/generic/translations/bg.json
+++ b/homeassistant/components/generic/translations/bg.json
@@ -38,6 +38,12 @@
             "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430"
         },
         "step": {
+            "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"
+            },
             "content_type": {
                 "data": {
                     "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435"
diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json
index 17e2ec60638..8a03666919e 100644
--- a/homeassistant/components/generic/translations/ca.json
+++ b/homeassistant/components/generic/translations/ca.json
@@ -75,6 +75,13 @@
             "unknown": "Error inesperat"
         },
         "step": {
+            "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"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Tipus de contingut"
diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json
index 70ea6ac052c..503f78fea32 100644
--- a/homeassistant/components/generic/translations/de.json
+++ b/homeassistant/components/generic/translations/de.json
@@ -75,6 +75,13 @@
             "unknown": "Unerwarteter Fehler"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "Dieses Bild sieht gut aus."
+                },
+                "description": "![Kamera-Standbildvorschau]({preview_url})",
+                "title": "Vorschau"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Inhaltstyp"
diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json
index 18c29225ffe..a9ea9a82d13 100644
--- a/homeassistant/components/generic/translations/en.json
+++ b/homeassistant/components/generic/translations/en.json
@@ -75,6 +75,13 @@
             "unknown": "Unexpected error"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "This image looks good."
+                },
+                "description": "![Camera Still Image Preview]({preview_url})",
+                "title": "Preview"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Content Type"
diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json
index 98c2eb4bc8f..42362863623 100644
--- a/homeassistant/components/generic/translations/es.json
+++ b/homeassistant/components/generic/translations/es.json
@@ -75,6 +75,13 @@
             "unknown": "Error inesperado"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "Esta imagen se ve bien."
+                },
+                "description": "![Vista previa de imagen fija de c\u00e1mara]({preview_url})",
+                "title": "Vista previa"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Tipos de contenido"
diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json
index 628a77ed3a0..e89e968a2f9 100644
--- a/homeassistant/components/generic/translations/et.json
+++ b/homeassistant/components/generic/translations/et.json
@@ -75,6 +75,13 @@
             "unknown": "Ootamatu t\u00f5rge"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "Pilt tundub OK"
+                },
+                "description": "![Camera Still Image Preview]({preview_url})",
+                "title": "Eelvaade"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Sisu t\u00fc\u00fcp"
diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json
index 27a0e9b8c60..d6c057d4da1 100644
--- a/homeassistant/components/generic/translations/fr.json
+++ b/homeassistant/components/generic/translations/fr.json
@@ -71,6 +71,9 @@
             "unknown": "Erreur inattendue"
         },
         "step": {
+            "confirm_still": {
+                "title": "Aper\u00e7u"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Type de contenu"
diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json
index a06dca30df8..0fd99c649a1 100644
--- a/homeassistant/components/generic/translations/it.json
+++ b/homeassistant/components/generic/translations/it.json
@@ -75,6 +75,13 @@
             "unknown": "Errore imprevisto"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "Questa immagine appare bene"
+                },
+                "description": "![Anteprima dell' immagine fissa della fotocamera]({preview_url})",
+                "title": "Anteprima"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Tipo di contenuto"
diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json
index f85e5a189ba..a4c9c27bf69 100644
--- a/homeassistant/components/generic/translations/no.json
+++ b/homeassistant/components/generic/translations/no.json
@@ -75,6 +75,13 @@
             "unknown": "Uventet feil"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "Dette bildet ser bra ut."
+                },
+                "description": "![Camera Still Image Preview]( {preview_url} )",
+                "title": "Forh\u00e5ndsvisning"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Innholdstype"
diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json
index 50de1532da6..e4ee551b524 100644
--- a/homeassistant/components/generic/translations/pl.json
+++ b/homeassistant/components/generic/translations/pl.json
@@ -75,6 +75,13 @@
             "unknown": "Nieoczekiwany b\u0142\u0105d"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "Ten obraz wygl\u0105da dobrze."
+                },
+                "description": "![Podgl\u0105d nieruchomego obrazu z kamery]({preview_url})",
+                "title": "Podgl\u0105d"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Typ zawarto\u015bci"
diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json
index 52b00d29190..de9be64fd2a 100644
--- a/homeassistant/components/generic/translations/pt-BR.json
+++ b/homeassistant/components/generic/translations/pt-BR.json
@@ -75,6 +75,13 @@
             "unknown": "Erro inesperado"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "Esta imagem parece boa."
+                },
+                "description": "![Camera Still Image Preview]({preview_url})",
+                "title": "Visualizar"
+            },
             "content_type": {
                 "data": {
                     "content_type": "Tipo de conte\u00fado"
diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json
index 3abe2af62bd..c3561d18e2a 100644
--- a/homeassistant/components/generic/translations/tr.json
+++ b/homeassistant/components/generic/translations/tr.json
@@ -75,6 +75,13 @@
             "unknown": "Beklenmeyen hata"
         },
         "step": {
+            "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": "\u00d6nizleme"
+            },
             "content_type": {
                 "data": {
                     "content_type": "\u0130\u00e7erik T\u00fcr\u00fc"
diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json
index e58b3d34ef6..09203276fda 100644
--- a/homeassistant/components/generic/translations/zh-Hant.json
+++ b/homeassistant/components/generic/translations/zh-Hant.json
@@ -75,6 +75,13 @@
             "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
         },
         "step": {
+            "confirm_still": {
+                "data": {
+                    "confirmed_ok": "\u5f71\u50cf\u633a\u6e05\u6670\u3002"
+                },
+                "description": "![\u651d\u5f71\u6a5f\u975c\u614b\u9810\u89bd]({preview_url})",
+                "title": "\u9810\u89bd"
+            },
             "content_type": {
                 "data": {
                     "content_type": "\u5167\u5bb9\u985e\u578b"
diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json
index 93b1d77dcfa..f99f120d951 100644
--- a/homeassistant/components/mqtt/translations/bg.json
+++ b/homeassistant/components/mqtt/translations/bg.json
@@ -5,15 +5,18 @@
             "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 MQTT."
         },
         "error": {
+            "bad_certificate": "CA \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d",
             "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0431\u0440\u043e\u043a\u0435\u0440\u0430."
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "\u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438",
                     "broker": "\u0411\u0440\u043e\u043a\u0435\u0440",
                     "discovery": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0442\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430",
                     "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
                     "port": "\u041f\u043e\u0440\u0442",
+                    "protocol": "MQTT \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"
                 },
                 "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f MQTT \u0431\u0440\u043e\u043a\u0435\u0440."
@@ -36,13 +39,16 @@
     },
     "options": {
         "error": {
+            "bad_certificate": "CA \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d",
             "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "\u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438",
                     "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
                     "port": "\u041f\u043e\u0440\u0442",
+                    "protocol": "MQTT \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/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json
index 819d06b5b29..13c91abe856 100644
--- a/homeassistant/components/mqtt/translations/ca.json
+++ b/homeassistant/components/mqtt/translations/ca.json
@@ -5,16 +5,33 @@
             "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
         },
         "error": {
-            "cannot_connect": "Ha fallat la connexi\u00f3"
+            "bad_birth": "T\u00f2pic del missatge de naixement ('birth') inv\u00e0lid",
+            "bad_certificate": "El certificat CA \u00e9s inv\u00e0lid",
+            "bad_client_cert": "Certificat de client inv\u00e0lid, assegura't que la codificaci\u00f3 del fitxer sigui PEM",
+            "bad_client_cert_key": "El certificat de client i la clau privada no formen una parella v\u00e0lida",
+            "bad_client_key": "Clau privada inv\u00e0lida, assegura't que la codificaci\u00f3 del fitxer sigui PEM i sense contrasenya",
+            "bad_discovery_prefix": "Prefix de descobriment inv\u00e0lid",
+            "bad_will": "T\u00f2pic del missatge d'\u00faltima voluntat ('will') inv\u00e0lid",
+            "cannot_connect": "Ha fallat la connexi\u00f3",
+            "invalid_inclusion": "El certificat de client i la clau privada s'han de configurar conjuntament"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opcions avan\u00e7ades",
                     "broker": "Broker",
+                    "certificate": "Ruta a un fitxer de certificat CA personalitzat",
+                    "client_cert": "Ruta a un fitxer de certificat de client",
+                    "client_id": "ID de client (deixa-ho buit per generar-lo aleat\u00f2riament)",
+                    "client_key": "Ruta a un fitxer de clau privada",
                     "discovery": "Habilita el descobriment autom\u00e0tic",
+                    "keepalive": "Temps entre enviaments de missatges de manteniment viu ('keep alive')",
                     "password": "Contrasenya",
                     "port": "Port",
                     "protocol": "Protocol MQTT",
+                    "set_ca_cert": "Validaci\u00f3 del certificat del 'broker'",
+                    "set_client_cert": "Utilitza un certificat de client",
+                    "tls_insecure": "Ignora la validaci\u00f3 del certificat del 'broker'",
                     "username": "Nom d'usuari"
                 },
                 "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT."
@@ -54,20 +71,40 @@
         "deprecated_yaml": {
             "description": "S'ha trobat el MQTT {platform}(s) sota el codi de la integraci\u00f3 `{platform}`.\n\nSi us plau, moveu la configuraci\u00f3 a la integraci\u00f3 `mqtt`i reinicieu el Home Assistant per solucionar aquesta incid\u00e8ncia. Vegeu la [documentaci\u00f3]({more_info_url}) per a m\u00e9s informaci\u00f3.",
             "title": "El MQTT {platform}(s) configurat manualment necessita la vostra atenci\u00f3"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "La configuraci\u00f3 seg\u00fcent que s'ha trobat a `configuration.yaml` ha estat migrada l'entrada de configuraci\u00f3 d'MQTT. Els par\u00e0metres de `configuration.yaml` ja no s'utilitzen.\n`{deprecated_settings}` \n\nElimina la configuraci\u00f3 MQTT de `configuration.yaml` i reinicia Home Assistant per solucionar aquest problema. Consulta la [documentaci\u00f3]({more_info_url}), per a m\u00e9s informaci\u00f3.",
+            "title": "S'han trobat par\u00e0metres de configuraci\u00f3 MQTT obsolets  a `configuration.yaml`"
         }
     },
     "options": {
         "error": {
-            "bad_birth": "Topic del missatge de naixement inv\u00e0lid.",
-            "bad_will": "Topic missatge d'\u00faltima voluntat inv\u00e0lid.",
-            "cannot_connect": "Ha fallat la connexi\u00f3"
+            "bad_birth": "T\u00f2pic del missatge de naixement ('birth') inv\u00e0lid",
+            "bad_certificate": "El certificat CA \u00e9s inv\u00e0lid",
+            "bad_client_cert": "Certificat de client inv\u00e0lid, assegura't que la codificaci\u00f3 del fitxer sigui PEM",
+            "bad_client_cert_key": "El certificat de client i la clau privada no formen una parella v\u00e0lida",
+            "bad_client_key": "Clau privada inv\u00e0lida, assegura't que la codificaci\u00f3 del fitxer sigui PEM i sense contrasenya",
+            "bad_discovery_prefix": "Prefix de descobriment inv\u00e0lid",
+            "bad_will": "T\u00f2pic del missatge d'\u00faltima voluntat ('will') inv\u00e0lid",
+            "cannot_connect": "Ha fallat la connexi\u00f3",
+            "invalid_inclusion": "El certificat de client i la clau privada s'han de configurar conjuntament"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opcions avan\u00e7ades",
                     "broker": "Broker",
+                    "certificate": "Puja un fitxer de certificat CA personalitzat",
+                    "client_cert": "Puja un fitxer de certificat de client",
+                    "client_id": "ID de client (deixa-ho buit per generar-lo aleat\u00f2riament)",
+                    "client_key": "Puja un fitxer de clau privada",
+                    "keepalive": "Temps entre enviaments de missatges de manteniment viu ('keep alive')",
                     "password": "Contrasenya",
                     "port": "Port",
+                    "protocol": "Protocol MQTT",
+                    "set_ca_cert": "Validaci\u00f3 del certificat del 'broker'",
+                    "set_client_cert": "Utilitza un certificat de client",
+                    "tls_insecure": "Ignora la validaci\u00f3 del certificat del 'broker'",
                     "username": "Nom d'usuari"
                 },
                 "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT.",
@@ -81,13 +118,14 @@
                     "birth_retain": "Retenci\u00f3 del missatge de naixement",
                     "birth_topic": "Topic del missatge de naixement",
                     "discovery": "Activar descobriment",
+                    "discovery_prefix": "Prefix de descobriment",
                     "will_enable": "Activa el missatge d'\u00faltima voluntat",
                     "will_payload": "Dades (payload) del missatge d'\u00faltima voluntat",
                     "will_qos": "QoS del missatge d'\u00faltima voluntat",
                     "will_retain": "Retenci\u00f3 del missatge d'\u00faltima voluntat",
                     "will_topic": "Topic del missatge d'\u00faltima voluntat"
                 },
-                "description": "Descobriment - Si est\u00e0 activat (recomanat), Home Assistant descobrir\u00e0 autom\u00e0ticament dispositius i entitats que publiquin la seva configuraci\u00f3 al broker MQTT. Si est\u00e0 desactivat, les configuracions s'han de fer manualment.\nMissatge de naixement - S'enviar\u00e0 cada vegada que Home Assistant \u00e9s connecti al broker MQTT.\nMissatge d'\u00faltima voluntat - S'enviar\u00e0 cada vegada que Home Assistant perdi la connexi\u00f3 amb el broker, tant si \u00e9s una desconnexi\u00f3 neta (per exemple si s'apaga Home Assistant) com si \u00e9s una desconnexi\u00f3 dolenta (per exemple si Home Assistant falla o perd la connexi\u00f3 a Internet).",
+                "description": "Descobriment - Si est\u00e0 activat (recomanat), Home Assistant descobrir\u00e0 autom\u00e0ticament dispositius i entitats que publiquin la seva configuraci\u00f3 al 'broker' MQTT. Si est\u00e0 desactivat, les configuracions s'han de fer manualment.\nPrefix de descobriment - Prefix del t\u00f2pics de configuraci\u00f3 a utilitzar per al descobriment autom\u00e0tic.\nMissatge de naixement - S'enviar\u00e0 cada vegada que Home Assistant es connecti al 'broker' MQTT.\nMissatge d'\u00faltima voluntat - S'enviar\u00e0 cada vegada que Home Assistant perdi la connexi\u00f3 amb el 'broker', tant si \u00e9s una desconnexi\u00f3 neta (per exemple si s'apaga Home Assistant) com si \u00e9s una desconnexi\u00f3 dolenta (per exemple si Home Assistant falla o perd la connexi\u00f3 a Internet).",
                 "title": "Opcions d'MQTT"
             }
         }
diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json
index 672137013e5..373a8a64a95 100644
--- a/homeassistant/components/mqtt/translations/et.json
+++ b/homeassistant/components/mqtt/translations/et.json
@@ -5,15 +5,33 @@
             "single_instance_allowed": "Lubatud on ainult \u00fcks MQTT konfiguratsioon."
         },
         "error": {
-            "cannot_connect": "Vahendajaga ei saa \u00fchendust luua."
+            "bad_birth": "Kehtetu loomise teavitus",
+            "bad_certificate": "CA sertifikaat on kehtetu",
+            "bad_client_cert": "Kehtetu kliendi sertifikaat, veendu, et on esitatud PEM-kodeeritud fail",
+            "bad_client_cert_key": "Kliendisertifikaat ja privaatne sertifikaat ei ole kehtiv paar",
+            "bad_client_key": "Kehtetu privaatv\u00f5ti, veendu, et PEM-kodeeritud fail tarnitakse ilma paroolita",
+            "bad_discovery_prefix": "Sobimatu tuvastuse eesliide",
+            "bad_will": "Kehtetu l\u00f5petamise teavitus",
+            "cannot_connect": "Vahendajaga ei saa \u00fchendust luua.",
+            "invalid_inclusion": "Kliendisertifikaat ja privaatne v\u00f5ti tuleb konfigureerida koos."
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "T\u00e4psemad s\u00e4tted",
                     "broker": "Vahendaja",
+                    "certificate": "Tee kohandatud CA-sertifikaadifaili juurde",
+                    "client_cert": "Kliendi serdifaili tee",
+                    "client_id": "Kliendi ID (juhuslikult genereeritud ID jaoks j\u00e4ta t\u00fchjaks)",
+                    "client_key": "Tee privaatse v\u00f5tme faili juurde",
                     "discovery": "Luba automaatne avastamine",
+                    "keepalive": "Aegumiss\u00f5numite saatmise vaheline aeg",
                     "password": "Salas\u00f5na",
                     "port": "Port",
+                    "protocol": "MQTT protokoll",
+                    "set_ca_cert": "Sertifikaadi kinnitamine",
+                    "set_client_cert": "Kasuta kliendi sertifikaati",
+                    "tls_insecure": "Eira serdi valideerimist",
                     "username": "Kasutajanimi"
                 },
                 "description": "Sisesta oma MQTT vahendaja andmed."
@@ -53,18 +71,32 @@
         "deprecated_yaml": {
             "description": "K\u00e4sitsi seadistatud MQTT {platform}  leiti platvormi v\u00f5tme ` {platform} ` alt. \n\n Selle probleemi lahendamiseks teisalda konfiguratsioon sidumisv\u00f5tmesse \"mqtt\" ja taask\u00e4ivitage Home Assistant. Lisateabe saamiseks vaata [dokumentatsiooni]( {more_info_url} ).",
             "title": "K\u00e4sitsi seadistatud MQTT {platform} vajab t\u00e4helepanu"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "J\u00e4rgmised failis \u201econfiguration.yaml\u201d leitud s\u00e4tted viidi \u00fcle MQTT konfiguratsioonikirjesse ja need alistavad n\u00fc\u00fcd faili \u201econfiguration.yaml\u201d s\u00e4tted:\n ` {deprecated_settings} ` \n\n Probleemi lahendamiseks eemalda need seaded saidilt \u201econfiguration.yaml\u201d ja taask\u00e4ivita koduabiline. Lisateabe saamiseks vaata [dokumentatsiooni]( {more_info_url} ).",
+            "title": "Configuration.yaml'is leiti vananenud MQTT seaded"
         }
     },
     "options": {
         "error": {
-            "bad_birth": "Kehtetu loomise teavitus.",
-            "bad_will": "Kehtetu l\u00f5petamise teavitus.",
-            "cannot_connect": "\u00dchendamine nurjus"
+            "bad_birth": "Kehtetu loomise teavitus",
+            "bad_certificate": "CA sertifikaat on kehtetu",
+            "bad_client_cert": "Kehtetu kliendi sertifikaat, veendu, et on esitatud PEM-kodeeritud fail",
+            "bad_client_cert_key": "Kliendisertifikaat ja privaatne sertifikaat ei ole kehtiv paar",
+            "bad_client_key": "Kehtetu privaatv\u00f5ti, veendu, et PEM-kodeeritud fail tarnitakse ilma paroolita",
+            "bad_discovery_prefix": "Sobimatu tuvastuse eesliide",
+            "bad_will": "Kehtetu l\u00f5petamise teavitus",
+            "cannot_connect": "\u00dchendamine nurjus",
+            "invalid_inclusion": "Kliendisertifikaat ja privaatne v\u00f5ti tuleb konfigureerida koos."
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "T\u00e4psemad s\u00e4tted",
                     "broker": "Vahendaja",
+                    "certificate": "Lae \u00fcles kohandatud CA-sertifikaadi fail",
+                    "client_cert": "Lae \u00fcles kliendi sertifikaadifail",
+                    "client_id": "Kliendi ID (juhuslikult genereeritud ID jaoks j\u00e4ta t\u00fchjaks)",
                     "client_key": "Privaatse v\u00f5tme faili \u00fcleslaadimine",
                     "keepalive": "Aegumiss\u00f5numite saatmise vaheline aeg",
                     "password": "Salas\u00f5na",
@@ -93,7 +125,7 @@
                     "will_retain": "L\u00f5petamisteate j\u00e4\u00e4dvustamine",
                     "will_topic": "L\u00f5petamisteade"
                 },
-                "description": "Avastamine - kui avastamine on lubatud (soovitatav) avastab Home Assistant automaatselt seadmed ja \u00fcksused, kes avaldavad oma konfiguratsiooni MQTT maakleris. Kui avastamine on keelatud, tuleb kogu seadistamine teha k\u00e4sitsi.\n S\u00fcnnis\u00f5num - s\u00fcnnis\u00f5num saadetakse iga kord kui Home Assistant (uuesti) MQTT maakleriga \u00fchendust v\u00f5tab.\n Tahte s\u00f5num - tahte s\u00f5num saadetakse iga kord kui Home Assistant kaotab \u00fchenduse maakleriga, nii korralisel (nt Home Assistant sulgub) kui ka erakorralisel (nt Home Assistant krahhi v\u00f5i v\u00f5rgu\u00fchenduse kaotamisel) \u00fchenduse kadumisel.",
+                "description": "Avastamine - Kui avastamine on lubatud (soovitatav), avastab Home Assistant automaatselt seadmed ja \u00fcksused, mis avaldavad oma konfiguratsiooni MQTT-vahendaja kaudu. Kui avastamine on v\u00e4lja l\u00fclitatud, tuleb kogu konfigureerimine teha k\u00e4sitsi.\nDiscovery prefix (Avastamise eesliide) - automaatse avastamise konfiguratsiooniteema eesliide, millega peab algama.\nBirth message (s\u00fcnniteade) - s\u00fcnniteade saadetakse iga kord, kui Home Assistant (taas)\u00fchendub MQTT-vahendajaga.\nWill message - Will message saadetakse iga kord, kui Home Assistant kaotab \u00fchenduse maakleriga, nii puhta (nt Home Assistant l\u00f5petab tegevuse) kui ka ebapuhta (nt Home Assistant kukub kokku v\u00f5i kaotab v\u00f5rgu\u00fchenduse) katkestamise korral.",
                 "title": "MQTT valikud"
             }
         }
diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json
index 805b8106fba..c643f0ef404 100644
--- a/homeassistant/components/mqtt/translations/hu.json
+++ b/homeassistant/components/mqtt/translations/hu.json
@@ -5,15 +5,33 @@
             "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
         },
         "error": {
-            "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+            "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik",
+            "bad_certificate": "A CA-tan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nytelen",
+            "bad_client_cert": "\u00c9rv\u00e9nytelen \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny. Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dolt f\u00e1jl van megadva",
+            "bad_client_cert_key": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny \u00e9s a priv\u00e1t tan\u00fas\u00edtv\u00e1ny nem \u00e9rv\u00e9nyes p\u00e1r",
+            "bad_client_key": "\u00c9rv\u00e9nytelen priv\u00e1t kulcs, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dol\u00e1s\u00fa f\u00e1jlt k\u00fcld\u00f6tt jelsz\u00f3 n\u00e9lk\u00fcl",
+            "bad_discovery_prefix": "\u00c9rv\u00e9nytelen felfedez\u00e9si el\u0151tag",
+            "bad_will": "\u00c9rv\u00e9nytelen 'will' topik",
+            "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+            "invalid_inclusion": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1nyt \u00e9s a mag\u00e1nkulcsot egy\u00fctt kell konfigur\u00e1lni"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Speci\u00e1lis be\u00e1ll\u00edt\u00e1sok",
                     "broker": "Br\u00f3ker",
+                    "certificate": "Az egy\u00e9ni CA-tan\u00fas\u00edtv\u00e1nyf\u00e1jl el\u00e9r\u00e9si \u00fatja",
+                    "client_cert": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny f\u00e1jl el\u00e9r\u00e9si \u00fatja",
+                    "client_id": "\u00dcgyf\u00e9l azonos\u00edt\u00f3 (hagyja \u00fcresen a v\u00e9letlenszer\u0171en gener\u00e1lt azonos\u00edt\u00f3hoz)",
+                    "client_key": "A priv\u00e1t kulcsf\u00e1jl el\u00e9r\u00e9si \u00fatvonala",
                     "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se",
+                    "keepalive": "A keep alive \u00fczenetek k\u00fcld\u00e9se k\u00f6z\u00f6tti id\u0151",
                     "password": "Jelsz\u00f3",
                     "port": "Port",
+                    "protocol": "MQTT protokoll",
+                    "set_ca_cert": "Br\u00f3kertan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nyes\u00edt\u00e9s",
+                    "set_client_cert": "\u00dcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+                    "tls_insecure": "A br\u00f3kertan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nyes\u00edt\u00e9s\u00e9nek figyelmen k\u00edv\u00fcl hagy\u00e1sa",
                     "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
                 },
                 "description": "K\u00e9rem, adja meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait."
@@ -53,20 +71,40 @@
         "deprecated_yaml": {
             "description": "A manu\u00e1lisan konfigur\u00e1lt MQTT {platform} a `{platform}` platformkulcs alatt tal\u00e1lhat\u00f3. \n\n A probl\u00e9ma megold\u00e1s\u00e1hoz helyezze \u00e1t a konfigur\u00e1ci\u00f3t az \"mqtt\" integr\u00e1ci\u00f3s kulcsra, \u00e9s ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse meg a [dokument\u00e1ci\u00f3t]({more_info_url}).",
             "title": "A manu\u00e1lisan konfigur\u00e1lt MQTT {platform} figyelmet ig\u00e9nyel"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "A k\u00f6vetkez\u0151 be\u00e1ll\u00edt\u00e1sok a `configuration.yaml'-ben tal\u00e1lhat\u00f3ak, \u00e1tker\u00fcltek az MQTT config bejegyz\u00e9sbe, \u00e9s mostant\u00f3l fel\u00fcl\u00edrj\u00e1k a `configuration.yaml'-ben tal\u00e1lhat\u00f3 be\u00e1ll\u00edt\u00e1sokat:\n`{deprecated_settings}`\n\nK\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket a be\u00e1ll\u00edt\u00e1sokat a `configuration.yaml` f\u00e1jlb\u00f3l \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot a probl\u00e9ma megold\u00e1s\u00e1hoz. Tov\u00e1bbi inform\u00e1ci\u00f3 a [document\u00e1ci\u00f3ban]({more_info_url}).",
+            "title": "Elavult MQTT-be\u00e1ll\u00edt\u00e1sok tal\u00e1lhat\u00f3k a \"configuration.yaml\" f\u00e1jlban"
         }
     },
     "options": {
         "error": {
             "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik.",
+            "bad_certificate": "A CA-tan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nytelen",
+            "bad_client_cert": "\u00c9rv\u00e9nytelen \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny. Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dolt f\u00e1jl van megadva",
+            "bad_client_cert_key": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny \u00e9s a priv\u00e1t tan\u00fas\u00edtv\u00e1ny nem \u00e9rv\u00e9nyes p\u00e1r",
+            "bad_client_key": "\u00c9rv\u00e9nytelen priv\u00e1t kulcs, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dol\u00e1s\u00fa f\u00e1jlt k\u00fcld\u00f6tt jelsz\u00f3 n\u00e9lk\u00fcl",
+            "bad_discovery_prefix": "\u00c9rv\u00e9nytelen felfedez\u00e9si el\u0151tag",
             "bad_will": "\u00c9rv\u00e9nytelen 'will' topik.",
-            "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+            "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+            "invalid_inclusion": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1nyt \u00e9s a priv\u00e1t kulcsot egy\u00fctt kell konfigur\u00e1lni"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Speci\u00e1lis be\u00e1ll\u00edt\u00e1sok",
                     "broker": "Br\u00f3ker",
+                    "certificate": "Egy\u00e9ni CA-tan\u00fas\u00edtv\u00e1nyf\u00e1jl felt\u00f6lt\u00e9se",
+                    "client_cert": "\u00dcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny f\u00e1jl felt\u00f6lt\u00e9se",
+                    "client_id": "\u00dcgyf\u00e9l azonos\u00edt\u00f3 (hagyja \u00fcresen a v\u00e9letlenszer\u0171en gener\u00e1lt azonos\u00edt\u00f3hoz)",
+                    "client_key": "Priv\u00e1t kulcsf\u00e1jl felt\u00f6lt\u00e9se",
+                    "keepalive": "A keep alive \u00fczenetek k\u00fcld\u00e9se k\u00f6z\u00f6tti id\u0151",
                     "password": "Jelsz\u00f3",
                     "port": "Port",
+                    "protocol": "MQTT protokoll",
+                    "set_ca_cert": "Br\u00f3kertan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nyes\u00edt\u00e9s",
+                    "set_client_cert": "\u00dcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny haszn\u00e1lata",
+                    "tls_insecure": "A br\u00f3kertan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nyes\u00edt\u00e9s\u00e9nek figyelmen k\u00edv\u00fcl hagy\u00e1sa",
                     "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
                 },
                 "description": "K\u00e9rem, adja meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.",
@@ -80,13 +118,14 @@
                     "birth_retain": "A sz\u00fclet\u00e9si \u00fczenet meg\u0151rz\u00e9se",
                     "birth_topic": "Sz\u00fclet\u00e9si \u00fczenet t\u00e9m\u00e1ja",
                     "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se",
+                    "discovery_prefix": "Felfedez\u00e9s el\u0151tag",
                     "will_enable": "Enged\u00e9lyez\u00e9si \u00fczenet",
                     "will_payload": "\u00dczenet",
                     "will_qos": "QoS \u00fczenet",
                     "will_retain": "\u00dczenet megtart\u00e1sa",
                     "will_topic": "\u00dczenet t\u00e9m\u00e1ja"
                 },
-                "description": "Discovery - Ha a felfedez\u00e9s enged\u00e9lyezve van (aj\u00e1nlott), akkor Home Assistant automatikusan felfedezi azokat az eszk\u00f6z\u00f6ket \u00e9s entit\u00e1sokat, amelyek k\u00f6zz\u00e9teszik konfigur\u00e1ci\u00f3jukat az MQTT br\u00f3keren. Ha a felfedez\u00e9s le van tiltva, minden konfigur\u00e1ci\u00f3t manu\u00e1lisan kell elv\u00e9gezni.\nBirth \u00fczenet - A sz\u00fclet\u00e9si \u00fczenet minden alkalommal el lesz k\u00fcldve, amikor Home Assistant (\u00fajra) csatlakozik az MQTT br\u00f3kerhez.\nWill \u00fczenet - Az akarat\u00fczenet minden alkalommal el lesz k\u00fcldve, amikor Home Assistant elvesz\u00edti a kapcsolatot a k\u00f6zvet\u00edt\u0151vel, mind takar\u00edt\u00e1s eset\u00e9n (pl. Home Assistant le\u00e1ll\u00edt\u00e1sa), mind rendelenes helyzetben (pl. Home Assistant \u00f6sszeomlik vagy megszakad a h\u00e1l\u00f3zati kapcsolata).",
+                "description": "Discovery - Ha a felfedez\u00e9s enged\u00e9lyezve van (aj\u00e1nlott), akkor Home Assistant automatikusan felfedezi azokat az eszk\u00f6z\u00f6ket \u00e9s entit\u00e1sokat, amelyek k\u00f6zz\u00e9teszik konfigur\u00e1ci\u00f3jukat az MQTT br\u00f3keren. Ha a felfedez\u00e9s le van tiltva, minden konfigur\u00e1ci\u00f3t manu\u00e1lisan kell elv\u00e9gezni.\nBirth \u00fczenet - A sz\u00fclet\u00e9si \u00fczenet minden alkalommal el lesz k\u00fcldve, amikor Home Assistant (\u00fajra) csatlakozik az MQTT br\u00f3kerhez.\nWill \u00fczenet - Az akarat\u00fczenet minden alkalommal el lesz k\u00fcldve, amikor Home Assistant elvesz\u00edti a kapcsolatot a k\u00f6zvet\u00edt\u0151vel, mind tiszta esetben (pl. Home Assistant le\u00e1ll\u00edt\u00e1sa), mind rendelenes helyzetben (pl. Home Assistant lefagy vagy megszakad a h\u00e1l\u00f3zati kapcsolata).",
                 "title": "MQTT opci\u00f3k"
             }
         }
diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json
index a94dd83a55d..9b94a95bdc1 100644
--- a/homeassistant/components/mqtt/translations/it.json
+++ b/homeassistant/components/mqtt/translations/it.json
@@ -5,15 +5,32 @@
             "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
         },
         "error": {
-            "cannot_connect": "Impossibile connettersi"
+            "bad_birth": "Argomento di nascita non valido",
+            "bad_certificate": "Il certificato CA non \u00e8 valido",
+            "bad_client_cert": "Certificato client non valido, assicurarsi che venga fornito un file codificato PEM",
+            "bad_client_cert_key": "Il certificato del client e il privato non sono accoppiati in modo valido",
+            "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password",
+            "bad_discovery_prefix": "Prefisso di ricerca non valido",
+            "cannot_connect": "Impossibile connettersi",
+            "invalid_inclusion": "Il certificato del client e la chiave privata devono essere configurati insieme"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opzioni avanzate",
                     "broker": "Broker",
+                    "certificate": "Percorso del file del certificato CA personalizzato",
+                    "client_cert": "Percorso per un file di certificato cliente",
+                    "client_id": "ID cliente (lasciare vuoto per generarne uno in modo casuale)",
+                    "client_key": "Percorso per un file della chiave privata",
                     "discovery": "Attiva il rilevamento",
+                    "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento attivo",
                     "password": "Password",
                     "port": "Porta",
+                    "protocol": "Protocollo MQTT",
+                    "set_ca_cert": "Convalida del certificato del broker",
+                    "set_client_cert": "Utilizzare un certificato client",
+                    "tls_insecure": "Ignorare la convalida del certificato del broker",
                     "username": "Nome utente"
                 },
                 "description": "Inserisci le informazioni di connessione del tuo broker MQTT."
@@ -58,15 +75,29 @@
     "options": {
         "error": {
             "bad_birth": "Argomento birth non valido.",
+            "bad_certificate": "Certificato CA non valido",
+            "bad_client_cert_key": "Il certificato del client e il certificato privato non sono accoppiati in modo valido",
+            "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password",
+            "bad_discovery_prefix": "Prefisso di ricerca non valido",
             "bad_will": "Argomento will non valido.",
-            "cannot_connect": "Impossibile connettersi"
+            "cannot_connect": "Impossibile connettersi",
+            "invalid_inclusion": "Il certificato e la chiave privata del client devono essere configurati insieme"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opzioni avanzate",
                     "broker": "Broker",
+                    "certificate": "Carica il file del certificato CA personalizzato",
+                    "client_cert": "Carica il file del certificato cliente",
+                    "client_id": "ID cliente (lasciare vuoto per generarne uno in modo casuale)",
+                    "client_key": "Carica il file della chiave privata",
                     "password": "Password",
                     "port": "Porta",
+                    "protocol": "Protocollo MQTT",
+                    "set_ca_cert": "Convalida del certificato del broker",
+                    "set_client_cert": "Utilizza un certificato client",
+                    "tls_insecure": "Ignora la convalida del certificato del broker",
                     "username": "Nome utente"
                 },
                 "description": "Inserisci le informazioni di connessione del tuo broker MQTT.",
@@ -80,6 +111,7 @@
                     "birth_retain": "Persistenza del messaggio birth",
                     "birth_topic": "Argomento del messaggio birth",
                     "discovery": "Attiva il rilevamento",
+                    "discovery_prefix": "Scopri il prefisso",
                     "will_enable": "Abilita il messaggio testamento",
                     "will_payload": "Payload del messaggio testamento",
                     "will_qos": "QoS del messaggio testamento",
diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json
index aa1633f6b27..910f47ac02a 100644
--- a/homeassistant/components/mqtt/translations/no.json
+++ b/homeassistant/components/mqtt/translations/no.json
@@ -5,15 +5,33 @@
             "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
         },
         "error": {
-            "cannot_connect": "Tilkobling mislyktes"
+            "bad_birth": "Ugyldig f\u00f8dselsemne",
+            "bad_certificate": "CA-sertifikatet er ugyldig",
+            "bad_client_cert": "Ugyldig klientsertifikat, s\u00f8rg for at en PEM-kodet fil leveres",
+            "bad_client_cert_key": "Klientsertifikat og privat er ikke noe gyldig par",
+            "bad_client_key": "Ugyldig privat n\u00f8kkel, s\u00f8rg for at en PEM-kodet fil leveres uten passord",
+            "bad_discovery_prefix": "Ugyldig oppdagelsesprefiks",
+            "bad_will": "Ugyldig viljeemne",
+            "cannot_connect": "Tilkobling mislyktes",
+            "invalid_inclusion": "Klientsertifikatet og den private n\u00f8kkelen m\u00e5 konfigureres sammen"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Avanserte instillinger",
                     "broker": "Megler",
+                    "certificate": "Bane til egendefinert CA-sertifikatfil",
+                    "client_cert": "Bane til en klientsertifikatfil",
+                    "client_id": "Klient-ID (la st\u00e5 tomt til tilfeldig generert)",
+                    "client_key": "Bane til en privat n\u00f8kkelfil",
                     "discovery": "Aktiver oppdagelse",
+                    "keepalive": "Tiden mellom sending hold levende meldinger",
                     "password": "Passord",
                     "port": "Port",
+                    "protocol": "MQTT-protokoll",
+                    "set_ca_cert": "Validering av meglersertifikat",
+                    "set_client_cert": "Bruk et klientsertifikat",
+                    "tls_insecure": "Ignorer validering av meglersertifikat",
                     "username": "Brukernavn"
                 },
                 "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler."
@@ -53,20 +71,40 @@
         "deprecated_yaml": {
             "description": "Manuelt konfigurert MQTT {platform} (er) funnet under plattformn\u00f8kkelen ` {platform} `. \n\n Flytt konfigurasjonen til `mqtt`-integrasjonsn\u00f8kkelen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet. Se [dokumentasjonen]( {more_info_url} ), for mer informasjon.",
             "title": "Din manuelt konfigurerte MQTT {platform} (er) trenger oppmerksomhet"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "F\u00f8lgende innstillinger funnet i 'configuration.yaml' ble migrert til MQTT-konfigurasjonsoppf\u00f8ring og vil n\u00e5 overstyre innstillingene i 'configuration.yaml':\n'{deprecated_settings}'\n\nFjern disse innstillingene fra configuration.yaml, og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet. Se [dokumentasjon]({more_info_url}), for mer informasjon.",
+            "title": "Utdaterte MQTT-innstillinger funnet i `configuration.yaml`"
         }
     },
     "options": {
         "error": {
-            "bad_birth": "Ugyldig f\u00f8dselsemne.",
-            "bad_will": "Ugyldig emne.",
-            "cannot_connect": "Tilkobling mislyktes"
+            "bad_birth": "Ugyldig f\u00f8dselsemne",
+            "bad_certificate": "CA-sertifikatet er ugyldig",
+            "bad_client_cert": "Ugyldig klientsertifikat, s\u00f8rg for at en PEM-kodet fil leveres",
+            "bad_client_cert_key": "Klientsertifikat og privat er ikke noe gyldig par",
+            "bad_client_key": "Ugyldig privat n\u00f8kkel, s\u00f8rg for at en PEM-kodet fil leveres uten passord",
+            "bad_discovery_prefix": "Ugyldig oppdagelsesprefiks",
+            "bad_will": "Ugyldig viljeemne",
+            "cannot_connect": "Tilkobling mislyktes",
+            "invalid_inclusion": "Klientsertifikatet og den private n\u00f8kkelen m\u00e5 konfigureres sammen"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Avanserte instillinger",
                     "broker": "Megler",
+                    "certificate": "Last opp egendefinert CA-sertifikatfil",
+                    "client_cert": "Last opp klientsertifikatfil",
+                    "client_id": "Klient-ID (la st\u00e5 tomt til tilfeldig generert)",
+                    "client_key": "Last opp privat n\u00f8kkelfil",
+                    "keepalive": "Tiden mellom sending hold levende meldinger",
                     "password": "Passord",
                     "port": "Port",
+                    "protocol": "MQTT-protokoll",
+                    "set_ca_cert": "Validering av meglersertifikat",
+                    "set_client_cert": "Bruk et klientsertifikat",
+                    "tls_insecure": "Ignorer validering av meglersertifikat",
                     "username": "Brukernavn"
                 },
                 "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.",
@@ -80,13 +118,14 @@
                     "birth_retain": "F\u00f8dselsmelding behold",
                     "birth_topic": "F\u00f8dselsmelding emne",
                     "discovery": "Aktiver oppdagelse",
+                    "discovery_prefix": "Oppdagelsesprefiks",
                     "will_enable": "Aktiver will melding",
                     "will_payload": "Testament melding nyttelast",
                     "will_qos": "Testament melding QoS",
                     "will_retain": "Testament melding behold",
                     "will_topic": "Testament melding emne"
                 },
-                "description": "Oppdagelse - Hvis oppdagelse er aktivert (anbefales), vil Home Assistant automatisk oppdage enheter og entiteter som publiserer konfigurasjonen til MQTT-megleren. Hvis oppdaglse er deaktivert, m\u00e5 all konfigurasjon utf\u00f8res manuelt.\nF\u00f8dselsmelding - F\u00f8dselsmeldingen vil bli sendt hver gang Home Assistant kobler til MQTT megleren.\nWill message - Will-meldingen vil bli sendt hver gang Home Assistant mister forbindelsen til megleren, b\u00e5de i tilfelle en ren (f.eks. at Home Assistant avsluttes) og i tilfelle en uren (f.eks. Home Assistant krasjer eller mister nettverkstilkoblingen) frakobling.",
+                "description": "Oppdagelse - Hvis oppdagelse er aktivert (anbefalt), vil Home Assistant automatisk oppdage enheter og enheter som publiserer konfigurasjonen deres p\u00e5 MQTT-megleren. Hvis oppdagelse er deaktivert, m\u00e5 all konfigurasjon gj\u00f8res manuelt.\n Oppdagelsesprefiks - Prefikset et konfigurasjonsemne for automatisk oppdagelse m\u00e5 starte med.\n F\u00f8dselsmelding - F\u00f8dselsmeldingen vil bli sendt hver gang Home Assistant (gjen)kobler til MQTT-megleren.\n Viljemelding - Viljemeldingen vil bli sendt hver gang Home Assistant mister forbindelsen til megleren, b\u00e5de i tilfelle en rengj\u00f8ring (f.eks. Home Assistant sl\u00e5r av) og i tilfelle en urent (f.eks. Home Assistant krasjer eller mister nettverksforbindelsen) koble fra.",
                 "title": "MQTT-alternativer"
             }
         }
diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json
index dad798296a1..0084c0a1b12 100644
--- a/homeassistant/components/mqtt/translations/pl.json
+++ b/homeassistant/components/mqtt/translations/pl.json
@@ -5,15 +5,33 @@
             "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
         },
         "error": {
-            "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+            "bad_birth": "Nieprawid\u0142owy temat \"birth\"",
+            "bad_certificate": "Certyfikat CA jest nieprawid\u0142owy",
+            "bad_client_cert": "Nieprawid\u0142owy certyfikat klienta, upewnij si\u0119, \u017ce dostarczono plik zakodowany w formacie PEM",
+            "bad_client_cert_key": "Certyfikat klienta i prywatny klucz nie s\u0105 prawid\u0142ow\u0105 par\u0105",
+            "bad_client_key": "Nieprawid\u0142owy klucz prywatny, upewnij si\u0119, \u017ce zakodowany plik PEM jest dostarczony bez has\u0142a",
+            "bad_discovery_prefix": "Nieprawid\u0142owy prefiks wykrywania",
+            "bad_will": "Nieprawid\u0142owy temat \"will\"",
+            "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+            "invalid_inclusion": "Certyfikat klienta i klucz prywatny musz\u0105 by\u0107 skonfigurowane razem"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opcje zaawansowane",
                     "broker": "Po\u015brednik",
+                    "certificate": "\u015acie\u017cka do pliku z niestandardowym certyfikatem CA",
+                    "client_cert": "\u015acie\u017cka do pliku certyfikatu klienta",
+                    "client_id": "Identyfikator klienta (pozostaw puste, aby wygenerowa\u0107 losowo)",
+                    "client_key": "\u015acie\u017cka do pliku klucza prywatnego",
                     "discovery": "W\u0142\u0105cz wykrywanie",
+                    "keepalive": "Czas pomi\u0119dzy wys\u0142aniem wiadomo\u015bci \"keep alive\"",
                     "password": "Has\u0142o",
                     "port": "Port",
+                    "protocol": "Protok\u00f3\u0142 MQTT",
+                    "set_ca_cert": "Sprawdzanie certyfikatu brokera",
+                    "set_client_cert": "U\u017cyj certyfikatu klienta",
+                    "tls_insecure": "Ignoruj sprawdzanie certyfikatu brokera",
                     "username": "Nazwa u\u017cytkownika"
                 },
                 "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT."
@@ -53,20 +71,40 @@
         "deprecated_yaml": {
             "description": "Znaleziono r\u0119cznie skonfigurowane platformy MQTT z kluczem `{platform}`.\n\nAby rozwi\u0105za\u0107 ten problem, przenie\u015b konfiguracj\u0119 do klucza integracji `mqtt` i uruchom ponownie Home Assistant. Zobacz [dokumentacj\u0119]( {more_info_url} ), aby uzyska\u0107 wi\u0119cej informacji.",
             "title": "Twoja r\u0119cznie skonfigurowana platforma MQTT {platform} wymaga uwagi"
+        },
+        "deprecated_yaml_broker_settings": {
+            "description": "Nast\u0119puj\u0105ce ustawienia znalezione w `configuration.yaml` zosta\u0142y przeniesione do wpisu konfiguracyjnego MQTT i zast\u0105pi\u0105 teraz ustawienia z `configuration.yaml`:\n`{deprecated_settings}` \n\nUsu\u0144 te ustawienia z pliku `configuration.yaml` i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem. Zobacz [dokumentacj\u0119]({more_info_url}), aby uzyska\u0107 wi\u0119cej informacji.",
+            "title": "Znaleziono przestarza\u0142e ustawienia MQTT w pliku `configuration.yaml`"
         }
     },
     "options": {
         "error": {
             "bad_birth": "Nieprawid\u0142owy temat \"birth\"",
+            "bad_certificate": "Certyfikat CA jest nieprawid\u0142owy",
+            "bad_client_cert": "Nieprawid\u0142owy certyfikat klienta, upewnij si\u0119, \u017ce dostarczono plik zakodowany w formacie PEM",
+            "bad_client_cert_key": "Certyfikat klienta i prywatny klucz nie s\u0105 prawid\u0142ow\u0105 par\u0105",
+            "bad_client_key": "Nieprawid\u0142owy klucz prywatny, upewnij si\u0119, \u017ce zakodowany plik PEM jest dostarczony bez has\u0142a",
+            "bad_discovery_prefix": "Nieprawid\u0142owy prefiks wykrywania",
             "bad_will": "Nieprawid\u0142owy temat \"will\"",
-            "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+            "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+            "invalid_inclusion": "Certyfikat klienta i klucz prywatny musz\u0105 by\u0107 skonfigurowane razem"
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "Opcje zaawansowane",
                     "broker": "Po\u015brednik",
+                    "certificate": "Prze\u015blij plik z niestandardowym certyfikatem CA",
+                    "client_cert": "Prze\u015blij plik certyfikatu klienta",
+                    "client_id": "Identyfikator klienta (pozostaw puste, aby wygenerowa\u0107 losowo)",
+                    "client_key": "Prze\u015blij plik klucza prywatnego",
+                    "keepalive": "Czas pomi\u0119dzy wys\u0142aniem wiadomo\u015bci \"keep alive\"",
                     "password": "Has\u0142o",
                     "port": "Port",
+                    "protocol": "Protok\u00f3\u0142 MQTT",
+                    "set_ca_cert": "Sprawdzanie certyfikatu brokera",
+                    "set_client_cert": "U\u017cyj certyfikatu klienta",
+                    "tls_insecure": "Ignoruj sprawdzanie certyfikatu brokera",
                     "username": "Nazwa u\u017cytkownika"
                 },
                 "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT",
@@ -80,13 +118,14 @@
                     "birth_retain": "Flaga \"retain\" wiadomo\u015bci \"birth\"",
                     "birth_topic": "Temat wiadomo\u015bci \"birth\"",
                     "discovery": "W\u0142\u0105cz wykrywanie",
+                    "discovery_prefix": "Prefiks wykrywania",
                     "will_enable": "W\u0142\u0105cz wiadomo\u015b\u0107 \"will\"",
                     "will_payload": "Warto\u015b\u0107 wiadomo\u015bci \"will\"",
                     "will_qos": "QoS wiadomo\u015bci \"will\"",
                     "will_retain": "Flaga \"retain\" wiadomo\u015bci \"will\"",
                     "will_topic": "Temat wiadomo\u015bci \"will\""
                 },
-                "description": "Wykrywanie - je\u015bli wykrywanie jest w\u0142\u0105czone (zalecane), Home Assistant automatycznie wykryje urz\u0105dzenia i encje, kt\u00f3re publikuj\u0105 swoj\u0105 konfiguracj\u0119 w brokerze MQTT. Je\u015bli wykrywanie jest wy\u0142\u0105czone, ca\u0142\u0105 konfiguracj\u0119 nale\u017cy wykona\u0107 r\u0119cznie.\nWiadomo\u015b\u0107 Birth - wiadomo\u015b\u0107 Birth zostanie wys\u0142ana za ka\u017cdym razem, gdy Home Assistant (ponownie) po\u0142\u0105czy si\u0119 z brokerem MQTT.\nWiadomo\u015b\u0107 Will - wiadomo\u015b\u0107 Will b\u0119dzie wysy\u0142ana za ka\u017cdym razem, gdy Home Assistant utraci po\u0142\u0105czenie z brokerem, zar\u00f3wno w przypadku czystego (np. wy\u0142\u0105czenie Home Assistanta), jak i w przypadku nieczystego (np. zawieszenie Home Assistanta lub utrata po\u0142\u0105czenia sieciowego) roz\u0142\u0105czenia.",
+                "description": "Wykrywanie - je\u015bli wykrywanie jest w\u0142\u0105czone (zalecane), Home Assistant automatycznie wykryje urz\u0105dzenia i encje, kt\u00f3re publikuj\u0105 swoj\u0105 konfiguracj\u0119 w brokerze MQTT. Je\u015bli wykrywanie jest wy\u0142\u0105czone, ca\u0142\u0105 konfiguracj\u0119 nale\u017cy wykona\u0107 r\u0119cznie.\nPrefiks wykrywania - prefiks od kt\u00f3rego zaczyna\u0107 ma si\u0119 temat automatycznego wykrywania.\nWiadomo\u015b\u0107 Birth - wiadomo\u015b\u0107 Birth zostanie wys\u0142ana za ka\u017cdym razem, gdy Home Assistant (ponownie) po\u0142\u0105czy si\u0119 z brokerem MQTT.\nWiadomo\u015b\u0107 Will - wiadomo\u015b\u0107 Will b\u0119dzie wysy\u0142ana za ka\u017cdym razem, gdy Home Assistant utraci po\u0142\u0105czenie z brokerem, zar\u00f3wno w przypadku czystego (np. wy\u0142\u0105czenie Home Assistanta), jak i w przypadku nieczystego roz\u0142\u0105czenia (np. zawieszenie Home Assistanta lub utrata po\u0142\u0105czenia sieciowego).",
                 "title": "Opcje MQTT"
             }
         }
diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json
index c9b15c9489c..f1f241e4c78 100644
--- a/homeassistant/components/mqtt/translations/ru.json
+++ b/homeassistant/components/mqtt/translations/ru.json
@@ -5,15 +5,30 @@
             "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
         },
         "error": {
-            "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+            "bad_birth": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.",
+            "bad_certificate": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0426\u0421 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.",
+            "bad_client_cert": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b, \u0437\u0430\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 PEM",
+            "bad_client_cert_key": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043d\u0435 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043f\u0430\u0440\u043e\u0439.",
+            "bad_client_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 PEM \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f.",
+            "bad_discovery_prefix": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0442\u043e\u043f\u0438\u043a\u0430 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f.",
+            "bad_will": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.",
+            "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+            "invalid_inclusion": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u0432\u043c\u0435\u0441\u0442\u0435."
         },
         "step": {
             "broker": {
                 "data": {
+                    "advanced_options": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
                     "broker": "\u0411\u0440\u043e\u043a\u0435\u0440",
+                    "certificate": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0426\u0421",
+                    "client_cert": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430",
+                    "client_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c)",
+                    "client_key": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430",
                     "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432",
+                    "keepalive": "\u0412\u0440\u0435\u043c\u044f \u043c\u0435\u0436\u0434\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Keep Alive",
                     "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
                     "port": "\u041f\u043e\u0440\u0442",
+                    "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b MQTT",
                     "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
                 },
                 "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT."
diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json
index ce101d389e8..c7d08ce7482 100644
--- a/homeassistant/components/mqtt/translations/zh-Hant.json
+++ b/homeassistant/components/mqtt/translations/zh-Hant.json
@@ -5,6 +5,8 @@
             "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002"
         },
         "error": {
+            "bad_birth": "Birth \u4e3b\u984c\u7121\u6548",
+            "bad_certificate": "CA \u8a8d\u8b49\u7121\u6548",
             "cannot_connect": "\u9023\u7dda\u5931\u6557"
         },
         "step": {
diff --git a/homeassistant/components/nibe_heatpump/translations/ca.json b/homeassistant/components/nibe_heatpump/translations/ca.json
index 95cc0f32841..d2924212386 100644
--- a/homeassistant/components/nibe_heatpump/translations/ca.json
+++ b/homeassistant/components/nibe_heatpump/translations/ca.json
@@ -4,7 +4,7 @@
             "already_configured": "El dispositiu ja est\u00e0 configurat"
         },
         "error": {
-            "address": "Adre\u00e7a IP remota inv\u00e0lida. L'adre\u00e7a ha de ser una adre\u00e7a IPV4.",
+            "address": "Adre\u00e7a remota inv\u00e0lida. L'adre\u00e7a ha de ser una adre\u00e7a IP o un nom d'amfitri\u00f3 resoluble.",
             "address_in_use": "El port d'escolta seleccionat ja est\u00e0 en \u00fas en aquest sistema.",
             "model": "El model seleccionat no sembla admetre modbus40",
             "read": "Error en la sol\u00b7licitud de lectura de la bomba. Verifica el port remot de lectura i/o l'adre\u00e7a IP remota.",
@@ -14,11 +14,18 @@
         "step": {
             "user": {
                 "data": {
-                    "ip_address": "Adre\u00e7a IP remota",
+                    "ip_address": "Adre\u00e7a remota",
                     "listening_port": "Port local d'escolta",
                     "remote_read_port": "Port remot de lectura",
                     "remote_write_port": "Port remot d'escriptura"
-                }
+                },
+                "data_description": {
+                    "ip_address": "Adre\u00e7a de la unitat NibeGW. El dispositiu hauria d'estar configurat amb una adre\u00e7a est\u00e0tica.",
+                    "listening_port": "Port local d'aquest sistema al qual la unitat NibeGW est\u00e0 configurada per enviar-hi dades.",
+                    "remote_read_port": "Port on la unitat NibeGW espera les sol\u00b7licituds de lectura.",
+                    "remote_write_port": "Port on la unitat NibeGW espera les sol\u00b7licituds d'escriptura."
+                },
+                "description": "Abans d'intentar configurar la integraci\u00f3, comprova que:\n - La unitat NibeGW est\u00e0 connectada a una bomba de calor.\n - S'ha activat l'accessori MODBUS40 a la configuraci\u00f3 de la bomba de calor.\n - La bomba no ha entrat en estat d'alarma per falta de l'accessori MODBUS40."
             }
         }
     }
diff --git a/homeassistant/components/nibe_heatpump/translations/et.json b/homeassistant/components/nibe_heatpump/translations/et.json
index a25e843855f..223d0f22c1a 100644
--- a/homeassistant/components/nibe_heatpump/translations/et.json
+++ b/homeassistant/components/nibe_heatpump/translations/et.json
@@ -4,7 +4,7 @@
             "already_configured": "Seade on juba h\u00e4\u00e4lestatud"
         },
         "error": {
-            "address": "M\u00e4\u00e4ratud on sobimatu kaug-IP-aadress. Aadress peab olema IPV4-aadress.",
+            "address": "M\u00e4\u00e4ratud vale kaugaadress. Aadress peab olema IP-aadress v\u00f5i lahendatav hostinimi.",
             "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\".",
diff --git a/homeassistant/components/nibe_heatpump/translations/hu.json b/homeassistant/components/nibe_heatpump/translations/hu.json
index 4fcff29a560..1dc8ea12179 100644
--- a/homeassistant/components/nibe_heatpump/translations/hu.json
+++ b/homeassistant/components/nibe_heatpump/translations/hu.json
@@ -4,7 +4,7 @@
             "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
         },
         "error": {
-            "address": "\u00c9rv\u00e9nytelen t\u00e1voli IP-c\u00edm van megadva. A c\u00edmnek IPV4-c\u00edmnek kell lennie.",
+            "address": "\u00c9rv\u00e9nytelen t\u00e1voli c\u00edm van megadva. A c\u00edmnek IP-c\u00edmnek vagy feloldhat\u00f3 g\u00e9pn\u00e9vnek kell lennie.",
             "address_in_use": "A kiv\u00e1lasztott port m\u00e1r haszn\u00e1latban van ezen a rendszeren.",
             "model": "\u00dagy t\u0171nik, hogy a kiv\u00e1lasztott modell nem t\u00e1mogatja a modbus40-et",
             "read": "Hiba a szivatty\u00fa olvas\u00e1si k\u00e9r\u00e9s\u00e9n\u00e9l. Ellen\u0151rizze a \"T\u00e1voli olvas\u00e1si portot\" vagy a \"T\u00e1voli IP-c\u00edmet\".",
@@ -18,7 +18,14 @@
                     "listening_port": "Helyi port",
                     "remote_read_port": "T\u00e1voli olvas\u00e1si port",
                     "remote_write_port": "T\u00e1voli \u00edr\u00e1si port"
-                }
+                },
+                "data_description": {
+                    "ip_address": "A NibeGW egys\u00e9g c\u00edme. A k\u00e9sz\u00fcl\u00e9ket statikus c\u00edmmel kell konfigur\u00e1lni.",
+                    "listening_port": "A rendszer azon helyi portja, amelyre a NibeGW egys\u00e9g az adatok k\u00fcld\u00e9s\u00e9re van konfigur\u00e1lva.",
+                    "remote_read_port": "A port, amelyen a NibeGW egys\u00e9g olvas\u00e1si k\u00e9r\u00e9seket fogad.",
+                    "remote_write_port": "A port, amelyen a NibeGW egys\u00e9g \u00edr\u00e1si k\u00e9r\u00e9seket fogad."
+                },
+                "description": "Miel\u0151tt megpr\u00f3b\u00e1ln\u00e1 konfigur\u00e1lni az integr\u00e1ci\u00f3t, ellen\u0151rizze, hogy:\n - A NibeGW egys\u00e9g h\u0151szivatty\u00fahoz van csatlakoztatva.\n - A MODBUS40 kieg\u00e9sz\u00edt\u0151 enged\u00e9lyezve van a h\u0151szivatty\u00fa konfigur\u00e1ci\u00f3j\u00e1ban.\n - A szivatty\u00fa nem l\u00e9pett riaszt\u00e1si \u00e1llapotba a MODBUS40 tartoz\u00e9k hi\u00e1nya miatt."
             }
         }
     }
diff --git a/homeassistant/components/nibe_heatpump/translations/no.json b/homeassistant/components/nibe_heatpump/translations/no.json
index a9c4c41993d..b0a8f601776 100644
--- a/homeassistant/components/nibe_heatpump/translations/no.json
+++ b/homeassistant/components/nibe_heatpump/translations/no.json
@@ -4,7 +4,7 @@
             "already_configured": "Enheten er allerede konfigurert"
         },
         "error": {
-            "address": "Ugyldig ekstern IP-adresse er angitt. Adressen m\u00e5 v\u00e6re en IPV4-adresse.",
+            "address": "Ugyldig ekstern adresse er angitt. Adressen m\u00e5 v\u00e6re en IP-adresse eller et vertsnavn som kan l\u00f8ses.",
             "address_in_use": "Den valgte lytteporten er allerede i bruk p\u00e5 dette systemet.",
             "model": "Den valgte modellen ser ikke ut til \u00e5 st\u00f8tte modbus40",
             "read": "Feil ved leseforesp\u00f8rsel fra pumpe. Bekreft din \"Ekstern leseport\" eller \"Ekstern IP-adresse\".",
@@ -14,11 +14,18 @@
         "step": {
             "user": {
                 "data": {
-                    "ip_address": "Ekstern IP-adresse",
+                    "ip_address": "Ekstern adresse",
                     "listening_port": "Lokal lytteport",
                     "remote_read_port": "Ekstern leseport",
                     "remote_write_port": "Ekstern skriveport"
-                }
+                },
+                "data_description": {
+                    "ip_address": "Adressen til NibeGW-enheten. Enheten skal ha blitt konfigurert med en statisk adresse.",
+                    "listening_port": "Den lokale porten p\u00e5 dette systemet, som NibeGW-enheten er konfigurert til \u00e5 sende data til.",
+                    "remote_read_port": "Porten NibeGW-enheten lytter etter leseforesp\u00f8rsler p\u00e5.",
+                    "remote_write_port": "Porten NibeGW-enheten lytter etter skriveforesp\u00f8rsler p\u00e5."
+                },
+                "description": "F\u00f8r du pr\u00f8ver \u00e5 konfigurere integrasjonen, kontroller at:\n - NibeGW-enheten er koblet til en varmepumpe.\n - MODBUS40-tilbeh\u00f8ret er aktivert i varmepumpekonfigurasjonen.\n - Pumpen har ikke g\u00e5tt i alarmtilstand om manglende MODBUS40-tilbeh\u00f8r."
             }
         }
     }
diff --git a/homeassistant/components/nibe_heatpump/translations/pl.json b/homeassistant/components/nibe_heatpump/translations/pl.json
index 7a179ad7326..8298b41c64c 100644
--- a/homeassistant/components/nibe_heatpump/translations/pl.json
+++ b/homeassistant/components/nibe_heatpump/translations/pl.json
@@ -4,7 +4,7 @@
             "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
         },
         "error": {
-            "address": "Podano nieprawid\u0142owy zdalny adres IP. Adres musi by\u0107 adresem IPV4.",
+            "address": "Podano nieprawid\u0142owy zdalny adres IP. Adres musi by\u0107 adresem IP lub rozpoznawaln\u0105 nazw\u0105 hosta.",
             "address_in_use": "Wybrany port nas\u0142uchiwania jest ju\u017c u\u017cywany w tym systemie.",
             "model": "Wybrany model nie obs\u0142uguje modbus40",
             "read": "B\u0142\u0105d przy \u017c\u0105daniu odczytu z pompy. Sprawd\u017a \u201eZdalny port odczytu\u201d lub \u201eZdalny adres IP\u201d.",
@@ -18,7 +18,14 @@
                     "listening_port": "Lokalny port nas\u0142uchiwania",
                     "remote_read_port": "Zdalny port odczytu",
                     "remote_write_port": "Zdalny port zapisu"
-                }
+                },
+                "data_description": {
+                    "ip_address": "Adres urz\u0105dzenia NibeGW. Urz\u0105dzenie powinno by\u0107 skonfigurowane z adresem statycznym.",
+                    "listening_port": "Port lokalny w tym systemie, do kt\u00f3rego urz\u0105dzenie NibeGW jest skonfigurowane do wysy\u0142ania danych.",
+                    "remote_read_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 odczytu.",
+                    "remote_write_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 zapisu."
+                },
+                "description": "Przed przyst\u0105pieniem do konfiguracji integracji sprawd\u017a, czy:\n - Urz\u0105dzenie NibeGW jest pod\u0142\u0105czona do pompy ciep\u0142a.\n - Akcesorium MODBUS40 zosta\u0142o w\u0142\u0105czone w konfiguracji pompy ciep\u0142a.\n - Pompa nie wesz\u0142a w stan alarmowy z powodu braku akcesorium MODBUS40."
             }
         }
     }
diff --git a/homeassistant/components/nibe_heatpump/translations/tr.json b/homeassistant/components/nibe_heatpump/translations/tr.json
index 6f044f9b9d4..3e7c744ca31 100644
--- a/homeassistant/components/nibe_heatpump/translations/tr.json
+++ b/homeassistant/components/nibe_heatpump/translations/tr.json
@@ -18,7 +18,11 @@
                     "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"
-                }
+                },
+                "data_description": {
+                    "remote_write_port": "NibeGW biriminin yazma isteklerini dinledi\u011fi ba\u011flant\u0131 noktas\u0131."
+                },
+                "description": "Entegrasyonu yap\u0131land\u0131rmaya \u00e7al\u0131\u015fmadan \u00f6nce \u015funlar\u0131 do\u011frulay\u0131n:\n - NibeGW \u00fcnitesi bir \u0131s\u0131 pompas\u0131na ba\u011fl\u0131d\u0131r.\n - Is\u0131 pompas\u0131 konfig\u00fcrasyonunda MODBUS40 aksesuar\u0131 etkinle\u015ftirildi.\n - Pompa, eksik MODBUS40 aksesuar\u0131 ile ilgili alarm durumuna ge\u00e7medi."
             }
         }
     }
diff --git a/homeassistant/components/oralb/translations/bg.json b/homeassistant/components/oralb/translations/bg.json
new file mode 100644
index 00000000000..af9a13197df
--- /dev/null
+++ b/homeassistant/components/oralb/translations/bg.json
@@ -0,0 +1,21 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e",
+            "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",
+            "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\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/oralb/translations/ca.json b/homeassistant/components/oralb/translations/ca.json
new file mode 100644
index 00000000000..c121ff7408c
--- /dev/null
+++ b/homeassistant/components/oralb/translations/ca.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Dispositiu no compatible"
+        },
+        "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/oralb/translations/de.json b/homeassistant/components/oralb/translations/de.json
new file mode 100644
index 00000000000..4c5720ec6fb
--- /dev/null
+++ b/homeassistant/components/oralb/translations/de.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Ger\u00e4t nicht unterst\u00fctzt"
+        },
+        "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/oralb/translations/es.json b/homeassistant/components/oralb/translations/es.json
new file mode 100644
index 00000000000..ae0ab01acdf
--- /dev/null
+++ b/homeassistant/components/oralb/translations/es.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Dispositivo no compatible"
+        },
+        "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/oralb/translations/et.json b/homeassistant/components/oralb/translations/et.json
new file mode 100644
index 00000000000..170815ec87e
--- /dev/null
+++ b/homeassistant/components/oralb/translations/et.json
@@ -0,0 +1,22 @@
+{
+    "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}",
+        "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/oralb/translations/fr.json b/homeassistant/components/oralb/translations/fr.json
new file mode 100644
index 00000000000..8ddb4af4dbc
--- /dev/null
+++ b/homeassistant/components/oralb/translations/fr.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Appareil non pris en charge"
+        },
+        "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/oralb/translations/hu.json b/homeassistant/components/oralb/translations/hu.json
new file mode 100644
index 00000000000..4668ffea416
--- /dev/null
+++ b/homeassistant/components/oralb/translations/hu.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Eszk\u00f6z nem t\u00e1mogatott"
+        },
+        "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/oralb/translations/it.json b/homeassistant/components/oralb/translations/it.json
new file mode 100644
index 00000000000..b19851b36ee
--- /dev/null
+++ b/homeassistant/components/oralb/translations/it.json
@@ -0,0 +1,22 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+            "already_in_progress": "Il processo di configurazione \u00e8 gi\u00e0 in corso",
+            "no_devices_found": "Nessun dispositivo trovato in rete",
+            "not_supported": "Dispositivo non supportato"
+        },
+        "flow_title": "{nome}",
+        "step": {
+            "bluetooth_confirm": {
+                "description": "Vuoi impostare {nome}?"
+            },
+            "user": {
+                "data": {
+                    "address": "Dispositivo"
+                },
+                "description": "Scegliere un dispositivo da configurare"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/oralb/translations/no.json b/homeassistant/components/oralb/translations/no.json
new file mode 100644
index 00000000000..0bf8b1695ec
--- /dev/null
+++ b/homeassistant/components/oralb/translations/no.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Enheten st\u00f8ttes ikke"
+        },
+        "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/oralb/translations/pl.json b/homeassistant/components/oralb/translations/pl.json
new file mode 100644
index 00000000000..4715905a2e9
--- /dev/null
+++ b/homeassistant/components/oralb/translations/pl.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane"
+        },
+        "flow_title": "{name}",
+        "step": {
+            "bluetooth_confirm": {
+                "description": "Czy chcesz skonfigurowa\u0107 {name}?"
+            },
+            "user": {
+                "data": {
+                    "address": "Urz\u0105dzenie"
+                },
+                "description": "Wybierz urz\u0105dzenie do skonfigurowania"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/oralb/translations/pt-BR.json b/homeassistant/components/oralb/translations/pt-BR.json
new file mode 100644
index 00000000000..0da7639fa2a
--- /dev/null
+++ b/homeassistant/components/oralb/translations/pt-BR.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "Dispositivo n\u00e3o suportado"
+        },
+        "flow_title": "{name}",
+        "step": {
+            "bluetooth_confirm": {
+                "description": "Deseja configurar {name}?"
+            },
+            "user": {
+                "data": {
+                    "address": "Dispositivo"
+                },
+                "description": "Escolha um dispositivo para configurar"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/oralb/translations/tr.json b/homeassistant/components/oralb/translations/tr.json
new file mode 100644
index 00000000000..bcccbd0f7ea
--- /dev/null
+++ b/homeassistant/components/oralb/translations/tr.json
@@ -0,0 +1,22 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Cihaz \u00e7oktan konfig\u00fcre edilmi\u015ftir",
+            "already_in_progress": "Konfigurasyon 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 ister misiniz?"
+            },
+            "user": {
+                "data": {
+                    "address": "Cihaz"
+                },
+                "description": "Kurulum i\u00e7in bir cihaz se\u00e7in"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/oralb/translations/zh-Hant.json b/homeassistant/components/oralb/translations/zh-Hant.json
new file mode 100644
index 00000000000..64ae1f19094
--- /dev/null
+++ b/homeassistant/components/oralb/translations/zh-Hant.json
@@ -0,0 +1,22 @@
+{
+    "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",
+            "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4"
+        },
+        "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/plugwise/translations/select.it.json b/homeassistant/components/plugwise/translations/select.it.json
index 9cbe0d8fdb9..97106b6c5b9 100644
--- a/homeassistant/components/plugwise/translations/select.it.json
+++ b/homeassistant/components/plugwise/translations/select.it.json
@@ -2,6 +2,7 @@
     "state": {
         "plugwise__dhw_mode": {
             "auto": "Automatico",
+            "boost": "Velocizza",
             "comfort": "Comfort",
             "off": "Spento"
         },
diff --git a/homeassistant/components/plugwise/translations/select.zh-Hant.json b/homeassistant/components/plugwise/translations/select.zh-Hant.json
index 6c3837959cc..fac75962cfa 100644
--- a/homeassistant/components/plugwise/translations/select.zh-Hant.json
+++ b/homeassistant/components/plugwise/translations/select.zh-Hant.json
@@ -1,5 +1,11 @@
 {
     "state": {
+        "plugwise__dhw_mode": {
+            "auto": "\u81ea\u52d5",
+            "boost": "\u5168\u901f\u6a21\u5f0f",
+            "comfort": "\u8212\u9069\u6a21\u5f0f",
+            "off": "\u95dc\u9589"
+        },
         "plugwise__regulation_mode": {
             "bleeding_cold": "\u5f37\u51b7",
             "bleeding_hot": "\u5f37\u6696",
diff --git a/homeassistant/components/pushover/translations/ca.json b/homeassistant/components/pushover/translations/ca.json
index 1a14a4ce3d5..cbfc95c86ba 100644
--- a/homeassistant/components/pushover/translations/ca.json
+++ b/homeassistant/components/pushover/translations/ca.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "La configuraci\u00f3 de Pushover mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Pushover del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.",
             "title": "La configuraci\u00f3 YAML de Pushover est\u00e0 sent eliminada"
+        },
+        "removed_yaml": {
+            "description": "La configuraci\u00f3 de Pushover 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 de Pushover del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.",
+            "title": "La configuraci\u00f3 YAML de Pushover s'ha eliminat"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/de.json b/homeassistant/components/pushover/translations/de.json
index 9a5aa0cb571..1a99ef663fa 100644
--- a/homeassistant/components/pushover/translations/de.json
+++ b/homeassistant/components/pushover/translations/de.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "Das Konfigurieren von Pushover mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Pushover-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.",
             "title": "Die Pushover-YAML-Konfiguration wird entfernt"
+        },
+        "removed_yaml": {
+            "description": "Das Konfigurieren von Pushover mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die Pushover-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.",
+            "title": "Die Pushover-YAML-Konfiguration wurde entfernt"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/en.json b/homeassistant/components/pushover/translations/en.json
index 926b58726f0..4a126782dda 100644
--- a/homeassistant/components/pushover/translations/en.json
+++ b/homeassistant/components/pushover/translations/en.json
@@ -26,9 +26,13 @@
         }
     },
     "issues": {
+        "deprecated_yaml": {
+            "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
+            "title": "The Pushover YAML configuration is being removed"
+        },
         "removed_yaml": {
-            "title": "The Pushover YAML configuration has been removed",
-            "description": "Configuring Pushover using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
+            "description": "Configuring Pushover using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
+            "title": "The Pushover YAML configuration has been removed"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/es.json b/homeassistant/components/pushover/translations/es.json
index e28f7045aa0..418fe68c28e 100644
--- a/homeassistant/components/pushover/translations/es.json
+++ b/homeassistant/components/pushover/translations/es.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "Se va a eliminar la configuraci\u00f3n de Pushover mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Pushover de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.",
             "title": "Se va a eliminar la configuraci\u00f3n YAML de Pushover"
+        },
+        "removed_yaml": {
+            "description": "Se ha eliminado la configuraci\u00f3n de Pushover mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n Pushover YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.",
+            "title": "Se ha eliminado la configuraci\u00f3n YAML de Pushover"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/et.json b/homeassistant/components/pushover/translations/et.json
index 1d309198b5c..7ae9c0abbf1 100644
--- a/homeassistant/components/pushover/translations/et.json
+++ b/homeassistant/components/pushover/translations/et.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "Pushoveri seadistamine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage failist configuration.yaml-i pushover-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.",
             "title": "Pushover YAML konfiguratsioon eemaldatakse"
+        },
+        "removed_yaml": {
+            "description": "Pushoveri konfigureerimine YAML-i abil on eemaldatud. \n\n Koduassistent ei kasuta teie olemasolevat YAML-i konfiguratsiooni. \n\n Selle probleemi lahendamiseks eemaldage Pushoveri YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.",
+            "title": "Pushoveri YAML-i konfiguratsioon on eemaldatud"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/it.json b/homeassistant/components/pushover/translations/it.json
index c72f62b0c21..8eec84415b7 100644
--- a/homeassistant/components/pushover/translations/it.json
+++ b/homeassistant/components/pushover/translations/it.json
@@ -29,6 +29,9 @@
         "deprecated_yaml": {
             "description": "La configurazione di Pushover tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Pushover dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.",
             "title": "La configurazione YAML di Pushover sar\u00e0 rimossa"
+        },
+        "removed_yaml": {
+            "title": "La configurazione Pushover YAML \u00e8 stata rimossa"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/no.json b/homeassistant/components/pushover/translations/no.json
index 8ecd1b83404..dd8713b73a4 100644
--- a/homeassistant/components/pushover/translations/no.json
+++ b/homeassistant/components/pushover/translations/no.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "Konfigurering av Pushover med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Pushover YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.",
             "title": "Pushover YAML-konfigurasjonen blir fjernet"
+        },
+        "removed_yaml": {
+            "description": "Konfigurering av Pushover med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern Pushover YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.",
+            "title": "Pushover YAML-konfigurasjonen er fjernet"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/pl.json b/homeassistant/components/pushover/translations/pl.json
index 02195af9229..46b3596dd1c 100644
--- a/homeassistant/components/pushover/translations/pl.json
+++ b/homeassistant/components/pushover/translations/pl.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "Konfiguracja Pushover 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 Pushover zostanie usuni\u0119ta"
+        },
+        "removed_yaml": {
+            "description": "Konfiguracja Pushover 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 Pushover zosta\u0142a usuni\u0119ta"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/pt-BR.json b/homeassistant/components/pushover/translations/pt-BR.json
index 60f2cb1b3b2..6e2f6412602 100644
--- a/homeassistant/components/pushover/translations/pt-BR.json
+++ b/homeassistant/components/pushover/translations/pt-BR.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "A configura\u00e7\u00e3o do Pushover 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 Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.",
             "title": "A configura\u00e7\u00e3o do Pushover YAML est\u00e1 sendo removida"
+        },
+        "removed_yaml": {
+            "description": "A configura\u00e7\u00e3o do Pushover 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 Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.",
+            "title": "A configura\u00e7\u00e3o YAML do Pushover foi removida"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/tr.json b/homeassistant/components/pushover/translations/tr.json
index d91471736b1..5ab133b70a9 100644
--- a/homeassistant/components/pushover/translations/tr.json
+++ b/homeassistant/components/pushover/translations/tr.json
@@ -29,6 +29,10 @@
         "deprecated_yaml": {
             "description": "YAML kullanarak Pushover'\u0131 yap\u0131land\u0131rma 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 Pushover 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": "Pushover YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor"
+        },
+        "removed_yaml": {
+            "description": "Pushover'i YAML kullanarak yap\u0131land\u0131rma 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": "Pushover YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/pushover/translations/zh-Hant.json b/homeassistant/components/pushover/translations/zh-Hant.json
index bf359028bde..82f4f8f2d94 100644
--- a/homeassistant/components/pushover/translations/zh-Hant.json
+++ b/homeassistant/components/pushover/translations/zh-Hant.json
@@ -29,6 +29,9 @@
         "deprecated_yaml": {
             "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Pushover \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 Pushover YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002",
             "title": "Pushover YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664"
+        },
+        "removed_yaml": {
+            "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Pushover \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushover YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json
index 9429650dbee..44257e5e3f7 100644
--- a/homeassistant/components/sensibo/translations/ca.json
+++ b/homeassistant/components/sensibo/translations/ca.json
@@ -17,7 +17,7 @@
                     "api_key": "Clau API"
                 },
                 "data_description": {
-                    "api_key": "Consulta la documentaci\u00f3 per obtenir una nova clau API."
+                    "api_key": "Consulta la documentaci\u00f3 per obtenir la teva clau API"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "Clau API"
                 },
                 "data_description": {
-                    "api_key": "Consulta la documentaci\u00f3 per obtenir la teva clau API."
+                    "api_key": "Consulta la documentaci\u00f3 per obtenir la teva clau API"
                 }
             }
         }
diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json
index dc2fdda0065..bd42df1e2fe 100644
--- a/homeassistant/components/sensibo/translations/pl.json
+++ b/homeassistant/components/sensibo/translations/pl.json
@@ -17,7 +17,7 @@
                     "api_key": "Klucz API"
                 },
                 "data_description": {
-                    "api_key": "Post\u0119puj zgodnie z dokumentacj\u0105, aby uzyska\u0107 nowy klucz API."
+                    "api_key": "Post\u0119puj zgodnie z dokumentacj\u0105, aby uzyska\u0107 klucz API"
                 }
             },
             "user": {
@@ -25,7 +25,7 @@
                     "api_key": "Klucz API"
                 },
                 "data_description": {
-                    "api_key": "Post\u0119puj zgodnie z dokumentacj\u0105, aby uzyska\u0107 klucz API."
+                    "api_key": "Post\u0119puj zgodnie z dokumentacj\u0105, aby uzyska\u0107 klucz API"
                 }
             }
         }
diff --git a/homeassistant/components/sensibo/translations/sensor.ca.json b/homeassistant/components/sensibo/translations/sensor.ca.json
index c9872da68db..ea5340076eb 100644
--- a/homeassistant/components/sensibo/translations/sensor.ca.json
+++ b/homeassistant/components/sensibo/translations/sensor.ca.json
@@ -5,6 +5,7 @@
             "s": "Sensible"
         },
         "sensibo__smart_type": {
+            "feelslike": "Sensaci\u00f3 de",
             "humidity": "Humitat",
             "temperature": "Temperatura"
         }
diff --git a/homeassistant/components/sensibo/translations/sensor.hu.json b/homeassistant/components/sensibo/translations/sensor.hu.json
index 38a372f0f78..093ce31a3e1 100644
--- a/homeassistant/components/sensibo/translations/sensor.hu.json
+++ b/homeassistant/components/sensibo/translations/sensor.hu.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Norm\u00e1l",
             "s": "\u00c9rz\u00e9keny"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "\u00c9rz\u00e9sre",
+            "humidity": "P\u00e1ratartalom",
+            "temperature": "H\u0151m\u00e9rs\u00e9klet"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.it.json b/homeassistant/components/sensibo/translations/sensor.it.json
index 85550808ee9..cb611c4cf42 100644
--- a/homeassistant/components/sensibo/translations/sensor.it.json
+++ b/homeassistant/components/sensibo/translations/sensor.it.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "Normale",
             "s": "Sensibile"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "Sembra che",
+            "humidity": "Umidit\u00e0",
+            "temperature": "Temperatura"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.pl.json b/homeassistant/components/sensibo/translations/sensor.pl.json
index 1b8ed8a8ed7..bf55a86f303 100644
--- a/homeassistant/components/sensibo/translations/sensor.pl.json
+++ b/homeassistant/components/sensibo/translations/sensor.pl.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "normalna",
             "s": "wysoka"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "odczuwalna",
+            "humidity": "wilgotno\u015b\u0107",
+            "temperature": "temperatura"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.ru.json b/homeassistant/components/sensibo/translations/sensor.ru.json
index c916a2b22f4..74722e0358a 100644
--- a/homeassistant/components/sensibo/translations/sensor.ru.json
+++ b/homeassistant/components/sensibo/translations/sensor.ru.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439",
             "s": "\u0427\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "\u041e\u0449\u0443\u0449\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a",
+            "humidity": "\u0412\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u044c",
+            "temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensibo/translations/sensor.zh-Hant.json b/homeassistant/components/sensibo/translations/sensor.zh-Hant.json
index 5144fdcc699..a98e9d8000a 100644
--- a/homeassistant/components/sensibo/translations/sensor.zh-Hant.json
+++ b/homeassistant/components/sensibo/translations/sensor.zh-Hant.json
@@ -3,6 +3,11 @@
         "sensibo__sensitivity": {
             "n": "\u6b63\u5e38",
             "s": "\u654f\u611f"
+        },
+        "sensibo__smart_type": {
+            "feelslike": "\u9ad4\u611f",
+            "humidity": "\u6fd5\u5ea6",
+            "temperature": "\u6eab\u5ea6"
         }
     }
 }
\ No newline at end of file
diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json
index 378489203d9..3920f829bf4 100644
--- a/homeassistant/components/sensor/translations/de.json
+++ b/homeassistant/components/sensor/translations/de.json
@@ -32,6 +32,7 @@
             "is_volatile_organic_compounds": "Aktuelle Konzentration fl\u00fcchtiger organischer Verbindungen in {entity_name}",
             "is_voltage": "Aktuelle Spannung von {entity_name}",
             "is_volume": "Aktuelle Lautst\u00e4rke von {entity_name}",
+            "is_water": "Aktuelles {entity_name} Wasser",
             "is_weight": "Aktuelles Gewicht von {entity_name}"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "{entity_name} Konzentrations\u00e4nderungen fl\u00fcchtiger organischer Verbindungen",
             "voltage": "{entity_name} Spannungs\u00e4nderungen",
             "volume": "Lautst\u00e4rke von {entity_name} \u00e4ndert sich",
+            "water": "{entity_name} Wasser\u00e4nderung",
             "weight": "Das Gewicht von {entity_name} \u00e4ndert sich"
         }
     },
diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json
index 7e164d9b980..4a124c4402b 100644
--- a/homeassistant/components/sensor/translations/hu.json
+++ b/homeassistant/components/sensor/translations/hu.json
@@ -32,6 +32,7 @@
             "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_volume": "{entity_name} aktu\u00e1lis hangereje",
+            "is_water": "Aktu\u00e1lis {entity_name} v\u00edz",
             "is_weight": "{entity_name} aktu\u00e1lis s\u00falya"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "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",
             "volume": "{entity_name} hanger\u0151 v\u00e1ltoz\u00e1s",
+            "water": "{entity_name} v\u00edz v\u00e1ltoz\u00e1sok",
             "weight": "{entity_name} s\u00falyv\u00e1ltoz\u00e1s"
         }
     },
diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json
index f75991aa881..53ef0c00942 100644
--- a/homeassistant/components/sensor/translations/no.json
+++ b/homeassistant/components/sensor/translations/no.json
@@ -32,6 +32,7 @@
             "is_volatile_organic_compounds": "Gjeldende {entity_name} flyktige organiske forbindelser",
             "is_voltage": "Gjeldende {entity_name} spenning",
             "is_volume": "Gjeldende {entity_name} -volum",
+            "is_water": "N\u00e5v\u00e6rende {entity_name} vann",
             "is_weight": "N\u00e5v\u00e6rende vekt p\u00e5 {entity_name}"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "{entity_name} konsentrasjon av flyktige organiske forbindelser",
             "voltage": "{entity_name} spenningsendringer",
             "volume": "{entity_name} volumendringer",
+            "water": "{entity_name} vannforandringer",
             "weight": "Vektendringer {entity_name}"
         }
     },
diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json
index 3ac3f849a6a..2accf6eaf52 100644
--- a/homeassistant/components/sensor/translations/pl.json
+++ b/homeassistant/components/sensor/translations/pl.json
@@ -32,6 +32,7 @@
             "is_volatile_organic_compounds": "obecny poziom st\u0119\u017cenia lotnych zwi\u0105zk\u00f3w organicznych {entity_name}",
             "is_voltage": "obecne napi\u0119cie {entity_name}",
             "is_volume": "obecna obj\u0119to\u015b\u0107 {entity_name}",
+            "is_water": "obecny poziom wody {entity_name}",
             "is_weight": "obecna waga {entity_name}"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "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}",
             "volume": "zmieni si\u0119 obj\u0119to\u015b\u0107 {entity_name}",
+            "water": "zmieni si\u0119 poziom wody {entity_name}",
             "weight": "zmieni si\u0119 waga {entity_name}"
         }
     },
diff --git a/homeassistant/components/sensor/translations/pt-BR.json b/homeassistant/components/sensor/translations/pt-BR.json
index 69bdd4971c3..6284a2dd170 100644
--- a/homeassistant/components/sensor/translations/pt-BR.json
+++ b/homeassistant/components/sensor/translations/pt-BR.json
@@ -32,6 +32,7 @@
             "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_volume": "Volume atual de {entity_name}",
+            "is_water": "\u00c1gua atual {entity_name}",
             "is_weight": "Peso atual {entity_name}"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "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}",
             "volume": "Altera\u00e7\u00f5es de volume de {entity_name}",
+            "water": "mudan\u00e7as de \u00e1gua {entity_name}",
             "weight": "Altera\u00e7\u00f5es de peso {entity_name}"
         }
     },
diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json
index ad2e5110f0c..33b43342c81 100644
--- a/homeassistant/components/sensor/translations/tr.json
+++ b/homeassistant/components/sensor/translations/tr.json
@@ -32,6 +32,7 @@
             "is_volatile_organic_compounds": "Mevcut {entity_name} u\u00e7ucu organik bile\u015fik konsantrasyon seviyesi",
             "is_voltage": "Mevcut {entity_name} voltaj\u0131",
             "is_volume": "Mevcut {entity_name} birimi",
+            "is_water": "Mevcut {entity_name} su",
             "is_weight": "Mevcut {entity_name} a\u011f\u0131rl\u0131\u011f\u0131"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "{entity_name} u\u00e7ucu organik bile\u015fik konsantrasyonu de\u011fi\u015fiklikleri",
             "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri",
             "volume": "{entity_name} birim de\u011fi\u015fiklikleri",
+            "water": "{entity_name} su de\u011fi\u015fimi",
             "weight": "{entity_name} a\u011f\u0131rl\u0131k de\u011fi\u015fiklikleri"
         }
     },
diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json
index 2f513ac59e9..1adb71c652b 100644
--- a/homeassistant/components/sensor/translations/zh-Hant.json
+++ b/homeassistant/components/sensor/translations/zh-Hant.json
@@ -32,6 +32,7 @@
             "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_volume": "\u76ee\u524d{entity_name}\u9ad4\u7a4d",
+            "is_water": "\u76ee\u524d{entity_name}\u6c34\u4f4d",
             "is_weight": "\u76ee\u524d{entity_name}\u91cd\u91cf"
         },
         "trigger_type": {
@@ -66,6 +67,7 @@
             "volatile_organic_compounds": "{entity_name} \u63ee\u767c\u6027\u6709\u6a5f\u7269\u6fc3\u5ea6\u8b8a\u5316",
             "voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3\u8b8a\u66f4",
             "volume": "{entity_name}\u9ad4\u7a4d\u8b8a\u66f4",
+            "water": "{entity_name}\u6c34\u4f4d\u8b8a\u66f4",
             "weight": "{entity_name}\u91cd\u91cf\u8b8a\u66f4"
         }
     },
diff --git a/homeassistant/components/statistics/translations/ca.json b/homeassistant/components/statistics/translations/ca.json
new file mode 100644
index 00000000000..55f2b128d8b
--- /dev/null
+++ b/homeassistant/components/statistics/translations/ca.json
@@ -0,0 +1,12 @@
+{
+    "issues": {
+        "deprecation_warning_characteristic": {
+            "description": "El par\u00e0metre de configuraci\u00f3 `state_characteristic` de la integraci\u00f3 d'estad\u00edstiques ser\u00e0 obligatori. \n\nAfegeix `state_characteristic: {characteristic}` a la configuraci\u00f3 del sensor `{entity}` per mantenir el comportament actual. \n\nLlegeix la documentaci\u00f3 de la integraci\u00f3 d'estad\u00edstiques per a m\u00e9s informaci\u00f3: https://www.home-assistant.io/integrations/statistics/",
+            "title": "S'ha assumit 'state_characteristic', variable obligat\u00f2ria per a una entitat d'estad\u00edstica"
+        },
+        "deprecation_warning_size": {
+            "description": "El par\u00e0metre de configuraci\u00f3 `sampling_size` de la integraci\u00f3 d'estad\u00edstiques tenia un valor predeterminat fix de 20, ara aix\u00f2 canviar\u00e0. \n\nSi us plau, comprova la configuraci\u00f3 del sensor `{entity}` i afegeix els valors l\u00edmits adequats, per exemple `sampling_size: 20` per mantenir el comportament actual. La configuraci\u00f3 de la integraci\u00f3 d'estad\u00edstiques ser\u00e0 m\u00e9s flexible a partir de la versi\u00f3 2022.12.0 i acceptar\u00e0 `sampling_size` i/o `max_age`. Aquesta nota et prepara per a aquest canvi de la configuraci\u00f3 que podria portar problemes en les teves entitats estad\u00edstiques. \n\nLlegeix la documentaci\u00f3 de la integraci\u00f3 d'estad\u00edstiques per a m\u00e9s informaci\u00f3: https://www.home-assistant.io/integrations/statistics/",
+            "title": "S'ha assumit 'sampling_size', per a una entitat d'estad\u00edstica"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/statistics/translations/de.json b/homeassistant/components/statistics/translations/de.json
new file mode 100644
index 00000000000..f29b161ec39
--- /dev/null
+++ b/homeassistant/components/statistics/translations/de.json
@@ -0,0 +1,12 @@
+{
+    "issues": {
+        "deprecation_warning_characteristic": {
+            "description": "Der Konfigurationsparameter `state_characteristic` der Statistik-Integration wird zur Pflicht.\n\nBitte f\u00fcge `state_characteristic: {characteristic}` in die Konfiguration des Sensors `{entity}` ein, um das aktuelle Verhalten beizubehalten.\n\nLies die Dokumentation der Statistik-Integration f\u00fcr weitere Details: https://www.home-assistant.io/integrations/statistics/",
+            "title": "Obligatorisches 'state_characteristic' wird f\u00fcr eine Statistikentit\u00e4t angenommen"
+        },
+        "deprecation_warning_size": {
+            "description": "Der Konfigurationsparameter `sampling_size` der Statistikintegration war bisher standardm\u00e4\u00dfig auf den Wert 20 eingestellt, was sich \u00e4ndern wird.\n\nBitte \u00fcberpr\u00fcfe die Konfiguration f\u00fcr Sensor `{entity}` und f\u00fcge geeignete Grenzen hinzu, zB `sampling_size: 20`, um das aktuelle Verhalten beizubehalten. Die Konfiguration der Statistikintegration wird mit Version 2022.12.0 flexibler und akzeptiert entweder \u201esampling_size\u201c oder \u201emax_age\u201c oder beide Einstellungen. Die obige Anfrage bereitet deine Konfiguration auf diese ansonsten bahnbrechende \u00c4nderung vor. \n\nLies die Dokumentation der Statistikintegration f\u00fcr weitere Details: https://www.home-assistant.io/integrations/statistics/",
+            "title": "Implizite 'sampling_size' angenommen f\u00fcr eine Statistikentit\u00e4t"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/statistics/translations/pl.json b/homeassistant/components/statistics/translations/pl.json
new file mode 100644
index 00000000000..f10ea9d2bbe
--- /dev/null
+++ b/homeassistant/components/statistics/translations/pl.json
@@ -0,0 +1,12 @@
+{
+    "issues": {
+        "deprecation_warning_characteristic": {
+            "description": "Parametr konfiguracyjny `state_characteristic` integracji Statystyk stanie si\u0119 obowi\u0105zkowy. \n\nProsz\u0119 doda\u0107 `state_characteristic: \"{characteristic}\" do konfiguracji sensora \"{entity}\", aby zachowa\u0107 bie\u017c\u0105ce zachowanie. \n\nPrzeczytaj dokumentacj\u0119 integracji Statystyk, aby uzyska\u0107 wi\u0119cej informacji: https://www.home-assistant.io/integrations/statistics/",
+            "title": "Obowi\u0105zkowe \"state_characteristic\" przyj\u0119ty dla encji Statystyk"
+        },
+        "deprecation_warning_size": {
+            "description": "Parametr konfiguracyjny `sampling_size` integracji Statystyk do tej pory mia\u0142 domy\u015bln\u0105 warto\u015b\u0107 20, kt\u00f3ra teraz si\u0119 zmieni. \n\nSprawd\u017a konfiguracj\u0119 sensora \"{entity}\" i dodaj odpowiednie granice, np. \"sampling_size: 20\", aby zachowa\u0107 bie\u017c\u0105ce zachowanie. Konfiguracja integracji Statystyk stanie si\u0119 bardziej elastyczna w wersji 2022.12.0 i zaakceptuje albo \"sampling_size\" lub \"max_age\" lub oba te ustawienia. Powy\u017csze ostrze\u017cenie przygotowuje twoj\u0105 konfiguracj\u0119 na t\u0119, w przeciwnym razie, prze\u0142omow\u0105 zmian\u0119. \n\nPrzeczytaj dokumentacj\u0119 integracji Statystyk, aby uzyska\u0107 wi\u0119cej informacji: https://www.home-assistant.io/integrations/statistics/",
+            "title": "Niejawny 'sampling_size' przyj\u0119ty dla encji Statystyk"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/statistics/translations/pt-BR.json b/homeassistant/components/statistics/translations/pt-BR.json
new file mode 100644
index 00000000000..1c95540dadb
--- /dev/null
+++ b/homeassistant/components/statistics/translations/pt-BR.json
@@ -0,0 +1,12 @@
+{
+    "issues": {
+        "deprecation_warning_characteristic": {
+            "description": "O par\u00e2metro de configura\u00e7\u00e3o `state_characteristic` da integra\u00e7\u00e3o de estat\u00edsticas se tornar\u00e1 obrigat\u00f3rio. \n\n Por favor, adicione `state_characteristic: {characteristic} ` \u00e0 configura\u00e7\u00e3o do sensor ` {entity} ` para manter o comportamento atual. \n\n Leia a documenta\u00e7\u00e3o da integra\u00e7\u00e3o de estat\u00edsticas para mais detalhes: https://www.home-assistant.io/integrations/statistics/",
+            "title": "Obrigat\u00f3rio 'state_characteristic' assumido para uma entidade de Estat\u00edstica"
+        },
+        "deprecation_warning_size": {
+            "description": "O par\u00e2metro de configura\u00e7\u00e3o `sampling_size` da integra\u00e7\u00e3o de estat\u00edsticas padronizou para o valor 20 at\u00e9 agora, que ser\u00e1 alterado. \n\n Verifique a configura\u00e7\u00e3o do sensor `{entity}` e adicione limites adequados, por exemplo, `sampling_size: 20` para manter o comportamento atual. A configura\u00e7\u00e3o da integra\u00e7\u00e3o de estat\u00edsticas se tornar\u00e1 mais flex\u00edvel com a vers\u00e3o 2022.12.0 e aceitar\u00e1 `sampling_size` ou `max_age`, ou ambas as configura\u00e7\u00f5es. A solicita\u00e7\u00e3o acima prepara sua configura\u00e7\u00e3o para essa altera\u00e7\u00e3o que, de outra forma, seria importante. \n\n Leia a documenta\u00e7\u00e3o da integra\u00e7\u00e3o de estat\u00edsticas para mais detalhes: https://www.home-assistant.io/integrations/statistics/",
+            "title": "'sampling_size' impl\u00edcito assumido para uma entidade Estat\u00edstica"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
index e38567ea37c..9c3158ac2e9 100644
--- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
+++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json
@@ -5,7 +5,8 @@
             "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
             "incomplete_info": "\u6240\u63d0\u4f9b\u4e4b\u88dd\u7f6e\u8cc7\u8a0a\u4e0d\u5b8c\u6574\u3001\u7121\u4e3b\u6a5f\u7aef\u6216\u6b0a\u6756\uff0c\u7121\u6cd5\u8a2d\u5b9a\u88dd\u7f6e\u3002",
             "not_xiaomi_miio": "\u5c0f\u7c73 Miio \uff08\u5c1a\uff09\u4e0d\u652f\u63f4\u8a72\u88dd\u7f6e\u3002",
-            "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
+            "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f",
+            "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
         },
         "error": {
             "cannot_connect": "\u9023\u7dda\u5931\u6557",
-- 
GitLab


From 8b119ab5fc9e8ac9c444cc8f69136e670ce7470c Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
Date: Wed, 26 Oct 2022 00:45:33 -0500
Subject: [PATCH 820/985] Address review feedback for jellyfin (#80987)

address review feedback for jellyfin
---
 homeassistant/components/jellyfin/media_player.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py
index aea9fd4a594..36fb65916d2 100644
--- a/homeassistant/components/jellyfin/media_player.py
+++ b/homeassistant/components/jellyfin/media_player.py
@@ -19,7 +19,7 @@ from homeassistant.util.dt import parse_datetime
 
 from .browse_media import build_item_response, build_root_response
 from .client_wrapper import get_artwork_url
-from .const import CONTENT_TYPE_MAP, DOMAIN
+from .const import CONTENT_TYPE_MAP, DOMAIN, USER_APP_NAME
 from .coordinator import JellyfinDataUpdateCoordinator
 from .entity import JellyfinEntity
 from .models import JellyfinData
@@ -39,7 +39,7 @@ async def async_setup_entry(
             JellyfinMediaPlayer(coordinator, session_id, session_data)
             for session_id, session_data in coordinator.data.items()
             if session_data["DeviceId"] != jellyfin_data.client_device_id
-            and session_data["Client"] != "Home Assistant"
+            and session_data["Client"] != USER_APP_NAME
         ),
     )
 
-- 
GitLab


From 8e196fbe0619f854ba916599cc18992ba9d9cdf4 Mon Sep 17 00:00:00 2001
From: niobos <niobos@dest-unreach.be>
Date: Wed, 26 Oct 2022 08:03:53 +0200
Subject: [PATCH 821/985] Add Velbus cover opening/closing (#79851)

* Velbus cover/blind: indicate opening/closing

* Add docstrings because flake8 requirement

Co-authored-by: Niels Laukens <niels@dest-unreach.be>
---
 homeassistant/components/velbus/cover.py | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py
index 782d8d3c81b..009c4fadfb9 100644
--- a/homeassistant/components/velbus/cover.py
+++ b/homeassistant/components/velbus/cover.py
@@ -38,7 +38,7 @@ class VelbusCover(VelbusEntity, CoverEntity):
     _channel: VelbusBlind
 
     def __init__(self, channel: VelbusBlind) -> None:
-        """Initialize the dimmer."""
+        """Initialize the cover."""
         super().__init__(channel)
         if self._channel.support_position():
             self._attr_supported_features = (
@@ -59,6 +59,16 @@ class VelbusCover(VelbusEntity, CoverEntity):
         """Return if the cover is closed."""
         return self._channel.is_closed()
 
+    @property
+    def is_opening(self) -> bool:
+        """Return if the cover is opening."""
+        return self._channel.is_opening()
+
+    @property
+    def is_closing(self) -> bool:
+        """Return if the cover is closing."""
+        return self._channel.is_closing()
+
     @property
     def current_cover_position(self) -> int | None:
         """Return current position of cover.
-- 
GitLab


From 7dd1f58d04da70ce6db8b08fd163f7e5211cde15 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
Date: Wed, 26 Oct 2022 18:52:52 +1100
Subject: [PATCH 822/985] Bump aiolifx_effects dependency to 0.3.0 (#80994)

---
 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 730eceb2afa..da07a2ffc8b 100644
--- a/homeassistant/components/lifx/manifest.json
+++ b/homeassistant/components/lifx/manifest.json
@@ -5,7 +5,7 @@
   "documentation": "https://www.home-assistant.io/integrations/lifx",
   "requirements": [
     "aiolifx==0.8.6",
-    "aiolifx_effects==0.2.2",
+    "aiolifx_effects==0.3.0",
     "aiolifx_themes==0.1.1"
   ],
   "quality_scale": "platinum",
diff --git a/requirements_all.txt b/requirements_all.txt
index bfb4b4513fd..5efb7623eb2 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -193,7 +193,7 @@ aiokef==0.2.16
 aiolifx==0.8.6
 
 # homeassistant.components.lifx
-aiolifx_effects==0.2.2
+aiolifx_effects==0.3.0
 
 # homeassistant.components.lifx
 aiolifx_themes==0.1.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index f5f3fd8432c..b74b354936b 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -171,7 +171,7 @@ aiokafka==0.7.2
 aiolifx==0.8.6
 
 # homeassistant.components.lifx
-aiolifx_effects==0.2.2
+aiolifx_effects==0.3.0
 
 # homeassistant.components.lifx
 aiolifx_themes==0.1.1
-- 
GitLab


From a90ef3a5752d39b13ec57c7acee83cc36c0cd3bd Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Wed, 26 Oct 2022 03:05:33 -0500
Subject: [PATCH 823/985] Add additional data to HomeKit diagnostics (#80980)

---
 .../components/homekit/diagnostics.py         |  33 ++++
 tests/components/homekit/test_diagnostics.py  | 151 +++++++++++++++++-
 2 files changed, 182 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py
index aadac9b4acc..dbd40c1d6f5 100644
--- a/homeassistant/components/homekit/diagnostics.py
+++ b/homeassistant/components/homekit/diagnostics.py
@@ -6,12 +6,16 @@ from typing import Any
 from pyhap.accessory_driver import AccessoryDriver
 from pyhap.state import State
 
+from homeassistant.components.diagnostics import async_redact_data
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.core import HomeAssistant
 
 from . import HomeKit
+from .accessories import HomeAccessory, HomeBridge
 from .const import DOMAIN, HOMEKIT
 
+TO_REDACT = {"access_token", "entity_picture"}
+
 
 async def async_get_config_entry_diagnostics(
     hass: HomeAssistant, entry: ConfigEntry
@@ -30,6 +34,11 @@ async def async_get_config_entry_diagnostics(
     if not homekit.driver:  # not started yet or startup failed
         return data
     driver: AccessoryDriver = homekit.driver
+    if driver.accessory:
+        if isinstance(driver.accessory, HomeBridge):
+            data["bridge"] = _get_bridge_diagnostics(hass, driver.accessory)
+        else:
+            data["accessory"] = _get_accessory_diagnostics(hass, driver.accessory)
     data.update(driver.get_accessories())
     state: State = driver.state
     data.update(
@@ -42,3 +51,27 @@ async def async_get_config_entry_diagnostics(
         }
     )
     return data
+
+
+def _get_bridge_diagnostics(hass: HomeAssistant, bridge: HomeBridge) -> dict[int, Any]:
+    """Return diagnostics for a bridge."""
+    return {
+        aid: _get_accessory_diagnostics(hass, accessory)
+        for aid, accessory in bridge.accessories.items()
+    }
+
+
+def _get_accessory_diagnostics(
+    hass: HomeAssistant, accessory: HomeAccessory
+) -> dict[str, Any]:
+    """Return diagnostics for an accessory."""
+    return {
+        "aid": accessory.aid,
+        "config": accessory.config,
+        "category": accessory.category,
+        "name": accessory.display_name,
+        "entity_id": accessory.entity_id,
+        "entity_state": async_redact_data(
+            hass.states.get(accessory.entity_id), TO_REDACT
+        ),
+    }
diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py
index e3a85b85972..15d4a6f6e2e 100644
--- a/tests/components/homekit/test_diagnostics.py
+++ b/tests/components/homekit/test_diagnostics.py
@@ -1,7 +1,11 @@
 """Test homekit diagnostics."""
 from unittest.mock import ANY, patch
 
-from homeassistant.components.homekit.const import DOMAIN
+from homeassistant.components.homekit.const import (
+    CONF_HOMEKIT_MODE,
+    DOMAIN,
+    HOMEKIT_MODE_ACCESSORY,
+)
 from homeassistant.const import CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STARTED
 
 from .util import async_init_integration
@@ -28,7 +32,7 @@ async def test_config_entry_not_running(
 
 
 async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zeroconf):
-    """Test generating diagnostics for a config entry."""
+    """Test generating diagnostics for a bridge config entry."""
     entry = MockConfigEntry(
         domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
     )
@@ -38,6 +42,7 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer
     await hass.async_block_till_done()
     diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
     assert diag == {
+        "bridge": {},
         "accessories": [
             {
                 "aid": 1,
@@ -117,3 +122,145 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer
     ), patch("homeassistant.components.homekit.async_port_is_available"):
         assert await hass.config_entries.async_unload(entry.entry_id)
         await hass.async_block_till_done()
+
+
+async def test_config_entry_accessory(
+    hass, hass_client, hk_driver, mock_async_zeroconf
+):
+    """Test generating diagnostics for an accessory config entry."""
+    hass.states.async_set("light.demo", "on")
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_NAME: "mock_name",
+            CONF_PORT: 12345,
+            CONF_HOMEKIT_MODE: HOMEKIT_MODE_ACCESSORY,
+            "filter": {
+                "exclude_domains": [],
+                "exclude_entities": [],
+                "include_domains": [],
+                "include_entities": ["light.demo"],
+            },
+        },
+    )
+    entry.add_to_hass(hass)
+    assert await hass.config_entries.async_setup(entry.entry_id)
+    hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
+    await hass.async_block_till_done()
+    diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
+    assert diag == {
+        "accessories": [
+            {
+                "aid": 1,
+                "services": [
+                    {
+                        "characteristics": [
+                            {"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"},
+                            {
+                                "format": "string",
+                                "iid": 3,
+                                "perms": ["pr"],
+                                "type": "20",
+                                "value": "Home Assistant " "Light",
+                            },
+                            {
+                                "format": "string",
+                                "iid": 4,
+                                "perms": ["pr"],
+                                "type": "21",
+                                "value": "Light",
+                            },
+                            {
+                                "format": "string",
+                                "iid": 5,
+                                "perms": ["pr"],
+                                "type": "23",
+                                "value": "demo",
+                            },
+                            {
+                                "format": "string",
+                                "iid": 6,
+                                "perms": ["pr"],
+                                "type": "30",
+                                "value": "light.demo",
+                            },
+                            {
+                                "format": "string",
+                                "iid": 7,
+                                "perms": ["pr"],
+                                "type": "52",
+                                "value": "2022.11.0",
+                            },
+                        ],
+                        "iid": 1,
+                        "type": "3E",
+                    },
+                    {
+                        "characteristics": [
+                            {
+                                "format": "string",
+                                "iid": 9,
+                                "perms": ["pr", "ev"],
+                                "type": "37",
+                                "value": "01.01.00",
+                            }
+                        ],
+                        "iid": 8,
+                        "type": "A2",
+                    },
+                    {
+                        "characteristics": [
+                            {
+                                "format": "bool",
+                                "iid": 11,
+                                "perms": ["pr", "pw", "ev"],
+                                "type": "25",
+                                "value": True,
+                            }
+                        ],
+                        "iid": 10,
+                        "type": "43",
+                    },
+                ],
+            }
+        ],
+        "accessory": {
+            "aid": 1,
+            "category": 5,
+            "config": {},
+            "entity_id": "light.demo",
+            "entity_state": {
+                "attributes": {},
+                "context": {"id": ANY, "parent_id": None, "user_id": None},
+                "entity_id": "light.demo",
+                "last_changed": ANY,
+                "last_updated": ANY,
+                "state": "on",
+            },
+            "name": "demo",
+        },
+        "client_properties": {},
+        "config-entry": {
+            "data": {"name": "mock_name", "port": 12345},
+            "options": {
+                "filter": {
+                    "exclude_domains": [],
+                    "exclude_entities": [],
+                    "include_domains": [],
+                    "include_entities": ["light.demo"],
+                },
+                "mode": "accessory",
+            },
+            "title": "Mock Title",
+            "version": 1,
+        },
+        "config_version": 2,
+        "pairing_id": ANY,
+        "status": 1,
+    }
+    with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
+        "homeassistant.components.homekit.HomeKit.async_stop"
+    ), patch("homeassistant.components.homekit.async_port_is_available"):
+        assert await hass.config_entries.async_unload(entry.entry_id)
+        await hass.async_block_till_done()
-- 
GitLab


From e15f2e050e7afadbb19d32973104e4e2f5a172ae Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Wed, 26 Oct 2022 03:21:30 -0500
Subject: [PATCH 824/985] Update ibeacon-ble to 1.0.1 (#80785)

---
 homeassistant/components/ibeacon/__init__.py  |  2 +-
 homeassistant/components/ibeacon/const.py     | 13 ++++
 .../components/ibeacon/coordinator.py         | 63 ++++++++++++------
 .../components/ibeacon/manifest.json          |  2 +-
 homeassistant/components/ibeacon/sensor.py    | 10 ++-
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 tests/components/ibeacon/__init__.py          | 16 ++++-
 tests/components/ibeacon/test_coordinator.py  | 65 +++++++++++++++++--
 9 files changed, 144 insertions(+), 31 deletions(-)

diff --git a/homeassistant/components/ibeacon/__init__.py b/homeassistant/components/ibeacon/__init__.py
index 2c191211910..02a19ef6332 100644
--- a/homeassistant/components/ibeacon/__init__.py
+++ b/homeassistant/components/ibeacon/__init__.py
@@ -13,7 +13,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Set up Bluetooth LE Tracker from a config entry."""
     coordinator = hass.data[DOMAIN] = IBeaconCoordinator(hass, entry, async_get(hass))
     await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
-    coordinator.async_start()
+    await coordinator.async_start()
     return True
 
 
diff --git a/homeassistant/components/ibeacon/const.py b/homeassistant/components/ibeacon/const.py
index 7d1ab15da0a..19b3a6f6599 100644
--- a/homeassistant/components/ibeacon/const.py
+++ b/homeassistant/components/ibeacon/const.py
@@ -2,6 +2,9 @@
 
 from datetime import timedelta
 
+from homeassistant.components.bluetooth import (
+    FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
+)
 from homeassistant.const import Platform
 
 DOMAIN = "ibeacon"
@@ -31,5 +34,15 @@ MAX_IDS = 10
 # we will add it to the ignore list since its garbage data.
 MAX_IDS_PER_UUID = 50
 
+# Number of times a beacon must be seen before it is added to the system
+# This is to prevent devices that are just passing by from being added
+# to the system.
+MIN_SEEN_TRANSIENT_NEW = (
+    round(
+        FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS / UPDATE_INTERVAL.total_seconds()
+    )
+    + 1
+)
+
 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 9979cdf4fa8..33b33c56ed0 100644
--- a/homeassistant/components/ibeacon/coordinator.py
+++ b/homeassistant/components/ibeacon/coordinator.py
@@ -9,8 +9,7 @@ from ibeacon_ble import (
     IBEACON_FIRST_BYTE,
     IBEACON_SECOND_BYTE,
     iBeaconAdvertisement,
-    is_ibeacon_service_info,
-    parse,
+    iBeaconParser,
 )
 
 from homeassistant.components import bluetooth
@@ -27,6 +26,7 @@ from .const import (
     DOMAIN,
     MAX_IDS,
     MAX_IDS_PER_UUID,
+    MIN_SEEN_TRANSIENT_NEW,
     SIGNAL_IBEACON_DEVICE_NEW,
     SIGNAL_IBEACON_DEVICE_SEEN,
     SIGNAL_IBEACON_DEVICE_UNAVAILABLE,
@@ -111,6 +111,7 @@ class IBeaconCoordinator:
         self.hass = hass
         self._entry = entry
         self._dev_reg = registry
+        self._ibeacon_parser = iBeaconParser()
 
         # iBeacon devices that do not follow the spec
         # and broadcast custom data in the major and minor fields
@@ -125,6 +126,7 @@ class IBeaconCoordinator:
         self._last_ibeacon_advertisement_by_unique_id: dict[
             str, iBeaconAdvertisement
         ] = {}
+        self._transient_seen_count: dict[str, int] = {}
         self._group_ids_by_address: dict[str, set[str]] = {}
         self._unique_ids_by_address: dict[str, set[str]] = {}
         self._unique_ids_by_group_id: dict[str, set[str]] = {}
@@ -161,6 +163,7 @@ class IBeaconCoordinator:
     def _async_cancel_unavailable_tracker(self, address: str) -> None:
         """Cancel unavailable tracking for an address."""
         self._unavailable_trackers.pop(address)()
+        self._transient_seen_count.pop(address, None)
 
     @callback
     def _async_ignore_uuid(self, uuid: str) -> None:
@@ -236,7 +239,7 @@ class IBeaconCoordinator:
         """Update from a bluetooth callback."""
         if service_info.address in self._ignore_addresses:
             return
-        if not (ibeacon_advertisement := parse(service_info)):
+        if not (ibeacon_advertisement := self._ibeacon_parser.parse(service_info)):
             return
 
         uuid_str = str(ibeacon_advertisement.uuid)
@@ -297,12 +300,21 @@ class IBeaconCoordinator:
             or service_info.device.name.replace("-", ":") == service_info.device.address
         ):
             return
+        previously_tracked = address in self._unique_ids_by_address
         self._last_ibeacon_advertisement_by_unique_id[unique_id] = ibeacon_advertisement
         self._async_track_ibeacon_with_unique_address(address, group_id, unique_id)
         if address not in self._unavailable_trackers:
             self._unavailable_trackers[address] = bluetooth.async_track_unavailable(
                 self.hass, self._async_handle_unavailable, address
             )
+
+        if not previously_tracked and new and ibeacon_advertisement.transient:
+            # Do not create a new tracker right away for transient devices
+            # If they keep advertising, we will create entities for them
+            # once _async_update_rssi_and_transients has seen them enough times
+            self._transient_seen_count[address] = 1
+            return
+
         # Some manufacturers violate the spec and flood us with random
         # data (sometimes its temperature data).
         #
@@ -349,23 +361,42 @@ class IBeaconCoordinator:
             async_dispatcher_send(self.hass, signal_unavailable(group_id))
 
     @callback
-    def _async_update_rssi(self) -> None:
+    def _async_update_rssi_and_transients(self) -> None:
         """Check to see if the rssi has changed and update any devices.
 
         We don't callback on RSSI changes so we need to check them
         here and send them over the dispatcher periodically to
         ensure the distance calculation is update.
+
+        If the transient flag is set we also need to check to see
+        if the device is still transmitting and increment the counter
         """
         for (
             unique_id,
             ibeacon_advertisement,
         ) in self._last_ibeacon_advertisement_by_unique_id.items():
             address = unique_id.split("_")[-1]
-            if (
-                service_info := bluetooth.async_last_service_info(
-                    self.hass, address, connectable=False
-                )
-            ) and service_info.rssi != ibeacon_advertisement.rssi:
+            service_info = bluetooth.async_last_service_info(
+                self.hass, address, connectable=False
+            )
+            if not service_info:
+                continue
+
+            if address in self._transient_seen_count:
+                self._transient_seen_count[address] += 1
+                if self._transient_seen_count[address] == MIN_SEEN_TRANSIENT_NEW:
+                    self._transient_seen_count.pop(address)
+                    _async_dispatch_update(
+                        self.hass,
+                        unique_id,
+                        service_info,
+                        ibeacon_advertisement,
+                        True,
+                        True,
+                    )
+                    continue
+
+            if service_info.rssi != ibeacon_advertisement.rssi:
                 ibeacon_advertisement.update_rssi(service_info.rssi)
                 async_dispatcher_send(
                     self.hass,
@@ -377,7 +408,7 @@ class IBeaconCoordinator:
     def _async_update(self, _now: datetime) -> None:
         """Update the Coordinator."""
         self._async_check_unavailable_groups_with_random_macs()
-        self._async_update_rssi()
+        self._async_update_rssi_and_transients()
 
     @callback
     def _async_restore_from_registry(self) -> None:
@@ -403,9 +434,9 @@ class IBeaconCoordinator:
                 group_id = f"{uuid}_{major}_{minor}"
                 self._group_ids_random_macs.add(group_id)
 
-    @callback
-    def async_start(self) -> None:
+    async def async_start(self) -> None:
         """Start the Coordinator."""
+        await self._ibeacon_parser.async_setup()
         self._async_restore_from_registry()
         entry = self._entry
         entry.async_on_unload(
@@ -421,14 +452,6 @@ class IBeaconCoordinator:
             )
         )
         entry.async_on_unload(self._async_stop)
-        # Replay any that are already there.
-        for service_info in bluetooth.async_discovered_service_info(
-            self.hass, connectable=False
-        ):
-            if is_ibeacon_service_info(service_info):
-                self._async_update_ibeacon(
-                    service_info, bluetooth.BluetoothChange.ADVERTISEMENT
-                )
         entry.async_on_unload(
             async_track_time_interval(self.hass, self._async_update, UPDATE_INTERVAL)
         )
diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json
index 3df2b9e000d..ade53491a4c 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.4"],
+  "requirements": ["ibeacon_ble==1.0.1"],
   "codeowners": ["@bdraco"],
   "iot_class": "local_push",
   "loggers": ["bleak"],
diff --git a/homeassistant/components/ibeacon/sensor.py b/homeassistant/components/ibeacon/sensor.py
index 4684efdf142..32c17957b60 100644
--- a/homeassistant/components/ibeacon/sensor.py
+++ b/homeassistant/components/ibeacon/sensor.py
@@ -27,7 +27,7 @@ from .entity import IBeaconEntity
 class IBeaconRequiredKeysMixin:
     """Mixin for required keys."""
 
-    value_fn: Callable[[iBeaconAdvertisement], int | None]
+    value_fn: Callable[[iBeaconAdvertisement], str | int | None]
 
 
 @dataclass
@@ -63,6 +63,12 @@ SENSOR_DESCRIPTIONS = (
         state_class=SensorStateClass.MEASUREMENT,
         device_class=SensorDeviceClass.DISTANCE,
     ),
+    IBeaconSensorEntityDescription(
+        key="vendor",
+        name="Vendor",
+        entity_registry_enabled_default=False,
+        value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.vendor,
+    ),
 )
 
 
@@ -132,6 +138,6 @@ class IBeaconSensorEntity(IBeaconEntity, SensorEntity):
         self.async_write_ha_state()
 
     @property
-    def native_value(self) -> int | None:
+    def native_value(self) -> str | int | None:
         """Return the state of the sensor."""
         return self.entity_description.value_fn(self._ibeacon_advertisement)
diff --git a/requirements_all.txt b/requirements_all.txt
index 5efb7623eb2..78da40a9eea 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -901,7 +901,7 @@ iammeter==0.1.7
 iaqualink==0.5.0
 
 # homeassistant.components.ibeacon
-ibeacon_ble==0.7.4
+ibeacon_ble==1.0.1
 
 # homeassistant.components.watson_tts
 ibm-watson==5.2.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index b74b354936b..bfd614cec5a 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -672,7 +672,7 @@ hyperion-py==0.7.5
 iaqualink==0.5.0
 
 # homeassistant.components.ibeacon
-ibeacon_ble==0.7.4
+ibeacon_ble==1.0.1
 
 # homeassistant.components.ping
 icmplib==3.0
diff --git a/tests/components/ibeacon/__init__.py b/tests/components/ibeacon/__init__.py
index f10bc65ed33..56d5eb78467 100644
--- a/tests/components/ibeacon/__init__.py
+++ b/tests/components/ibeacon/__init__.py
@@ -58,7 +58,21 @@ BEACON_RANDOM_ADDRESS_SERVICE_INFO = BluetoothServiceInfo(
     service_uuids=[],
     source="local",
 )
-
+TESLA_TRANSIENT = BluetoothServiceInfo(
+    address="CC:CC:CC:CC:CC:CC",
+    rssi=-60,
+    name="S6da7c9389bd5452cC",
+    manufacturer_data={
+        76: b"\x02\x15t'\x8b\xda\xb6DE \x8f\x0cr\x0e\xaf\x05\x995\x00\x00[$\xc5"
+    },
+    service_data={},
+    service_uuids=[],
+    source="hci0",
+)
+TESLA_TRANSIENT_BLE_DEVICE = BLEDevice(
+    address="CC:CC:CC:CC:CC:CC",
+    name="S6da7c9389bd5452cC",
+)
 
 FEASY_BEACON_BLE_DEVICE = BLEDevice(
     address="AA:BB:CC:DD:EE:FF",
diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py
index 5ea19914ee4..25ce7154a37 100644
--- a/tests/components/ibeacon/test_coordinator.py
+++ b/tests/components/ibeacon/test_coordinator.py
@@ -2,16 +2,27 @@
 
 
 from dataclasses import replace
+from datetime import timedelta
 
 import pytest
 
-from homeassistant.components.ibeacon.const import DOMAIN
+from homeassistant.components.ibeacon.const import DOMAIN, UPDATE_INTERVAL
+from homeassistant.const import STATE_HOME
 from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo
+from homeassistant.util import dt as dt_util
 
-from . import BLUECHARM_BEACON_SERVICE_INFO, BLUECHARM_BEACON_SERVICE_INFO_DBUS
+from . import (
+    BLUECHARM_BEACON_SERVICE_INFO,
+    BLUECHARM_BEACON_SERVICE_INFO_DBUS,
+    TESLA_TRANSIENT,
+    TESLA_TRANSIENT_BLE_DEVICE,
+)
 
-from tests.common import MockConfigEntry
-from tests.components.bluetooth import inject_bluetooth_service_info
+from tests.common import MockConfigEntry, async_fire_time_changed
+from tests.components.bluetooth import (
+    inject_bluetooth_service_info,
+    patch_all_discovered_devices,
+)
 
 
 @pytest.fixture(autouse=True)
@@ -195,3 +206,49 @@ async def test_rotating_major_minor_and_mac_no_name(hass):
     await hass.async_block_till_done()
 
     assert len(hass.states.async_entity_ids("device_tracker")) == before_entity_count
+
+
+async def test_ignore_transient_devices_unless_we_see_them_a_few_times(hass):
+    """Test we ignore transient devices unless we see them a few times."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+    )
+    entry.add_to_hass(hass)
+
+    assert await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    before_entity_count = len(hass.states.async_entity_ids())
+    inject_bluetooth_service_info(
+        hass,
+        TESLA_TRANSIENT,
+    )
+    await hass.async_block_till_done()
+    assert len(hass.states.async_entity_ids()) == before_entity_count
+
+    with patch_all_discovered_devices([TESLA_TRANSIENT_BLE_DEVICE]):
+        async_fire_time_changed(
+            hass,
+            dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL.total_seconds() * 2),
+        )
+        await hass.async_block_till_done()
+
+    assert len(hass.states.async_entity_ids()) == before_entity_count
+
+    for i in range(3, 17):
+        with patch_all_discovered_devices([TESLA_TRANSIENT_BLE_DEVICE]):
+            async_fire_time_changed(
+                hass,
+                dt_util.utcnow()
+                + timedelta(seconds=UPDATE_INTERVAL.total_seconds() * 2 * i),
+            )
+            await hass.async_block_till_done()
+
+    assert len(hass.states.async_entity_ids()) > before_entity_count
+
+    assert hass.states.get("device_tracker.s6da7c9389bd5452cc_cccc").state == STATE_HOME
+
+    await hass.config_entries.async_reload(entry.entry_id)
+
+    await hass.async_block_till_done()
+    assert hass.states.get("device_tracker.s6da7c9389bd5452cc_cccc").state == STATE_HOME
-- 
GitLab


From 8714fc5c2c51d1ffca70f0c39a0efc1b0e8301d4 Mon Sep 17 00:00:00 2001
From: Sean Vig <sean.v.775@gmail.com>
Date: Wed, 26 Oct 2022 04:24:47 -0400
Subject: [PATCH 825/985] Use `._attr_*` properties for monoprice integration
 (#80505)

---
 .../components/monoprice/media_player.py      | 90 ++++++-------------
 1 file changed, 25 insertions(+), 65 deletions(-)

diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py
index 2f4a4a33f49..f6150ab1d22 100644
--- a/homeassistant/components/monoprice/media_player.py
+++ b/homeassistant/components/monoprice/media_player.py
@@ -5,6 +5,7 @@ from serial import SerialException
 
 from homeassistant import core
 from homeassistant.components.media_player import (
+    MediaPlayerDeviceClass,
     MediaPlayerEntity,
     MediaPlayerEntityFeature,
     MediaPlayerState,
@@ -27,6 +28,7 @@ from .const import (
 
 _LOGGER = logging.getLogger(__name__)
 
+MAX_VOLUME = 38
 PARALLEL_UPDATES = 1
 
 
@@ -114,6 +116,7 @@ async def async_setup_entry(
 class MonopriceZone(MediaPlayerEntity):
     """Representation of a Monoprice amplifier zone."""
 
+    _attr_device_class = MediaPlayerDeviceClass.RECEIVER
     _attr_supported_features = (
         MediaPlayerEntityFeature.VOLUME_MUTE
         | MediaPlayerEntityFeature.VOLUME_SET
@@ -131,16 +134,18 @@ class MonopriceZone(MediaPlayerEntity):
         # dict source name -> source_id
         self._source_name_id = sources[1]
         # ordered list of all source names
-        self._source_names = sources[2]
+        self._attr_source_list = sources[2]
         self._zone_id = zone_id
-        self._unique_id = f"{namespace}_{self._zone_id}"
-        self._name = f"Zone {self._zone_id}"
+        self._attr_unique_id = f"{namespace}_{self._zone_id}"
+        self._attr_name = f"Zone {self._zone_id}"
+        self._attr_device_info = DeviceInfo(
+            identifiers={(DOMAIN, self._attr_unique_id)},
+            manufacturer="Monoprice",
+            model="6-Zone Amplifier",
+            name=self.name,
+        )
 
         self._snapshot = None
-        self._state = None
-        self._volume = None
-        self._source = None
-        self._mute = None
         self._update_success = True
 
     def update(self) -> None:
@@ -156,71 +161,24 @@ class MonopriceZone(MediaPlayerEntity):
             self._update_success = False
             return
 
-        self._state = MediaPlayerState.ON if state.power else MediaPlayerState.OFF
-        self._volume = state.volume
-        self._mute = state.mute
+        self._attr_state = MediaPlayerState.ON if state.power else MediaPlayerState.OFF
+        self._attr_volume_level = state.volume / MAX_VOLUME
+        self._attr_is_volume_muted = state.mute
         idx = state.source
         if idx in self._source_id_name:
-            self._source = self._source_id_name[idx]
+            self._attr_source = self._source_id_name[idx]
         else:
-            self._source = None
+            self._attr_source = None
 
     @property
     def entity_registry_enabled_default(self) -> bool:
         """Return if the entity should be enabled when first added to the entity registry."""
         return self._zone_id < 20 or self._update_success
 
-    @property
-    def device_info(self) -> DeviceInfo:
-        """Return device info for this device."""
-        return DeviceInfo(
-            identifiers={(DOMAIN, self.unique_id)},
-            manufacturer="Monoprice",
-            model="6-Zone Amplifier",
-            name=self.name,
-        )
-
-    @property
-    def unique_id(self):
-        """Return unique ID for this device."""
-        return self._unique_id
-
-    @property
-    def name(self):
-        """Return the name of the zone."""
-        return self._name
-
-    @property
-    def state(self):
-        """Return the state of the zone."""
-        return self._state
-
-    @property
-    def volume_level(self):
-        """Volume level of the media player (0..1)."""
-        if self._volume is None:
-            return None
-        return self._volume / 38.0
-
-    @property
-    def is_volume_muted(self):
-        """Boolean if volume is currently muted."""
-        return self._mute
-
     @property
     def media_title(self):
         """Return the current source as medial title."""
-        return self._source
-
-    @property
-    def source(self):
-        """Return the current input source of the device."""
-        return self._source
-
-    @property
-    def source_list(self):
-        """List of available input sources."""
-        return self._source_names
+        return self.source
 
     def snapshot(self):
         """Save zone's current state."""
@@ -253,16 +211,18 @@ class MonopriceZone(MediaPlayerEntity):
 
     def set_volume_level(self, volume: float) -> None:
         """Set volume level, range 0..1."""
-        self._monoprice.set_volume(self._zone_id, int(volume * 38))
+        self._monoprice.set_volume(self._zone_id, round(volume * MAX_VOLUME))
 
     def volume_up(self) -> None:
         """Volume up the media player."""
-        if self._volume is None:
+        if self.volume_level is None:
             return
-        self._monoprice.set_volume(self._zone_id, min(self._volume + 1, 38))
+        volume = round(self.volume_level * MAX_VOLUME)
+        self._monoprice.set_volume(self._zone_id, min(volume + 1, MAX_VOLUME))
 
     def volume_down(self) -> None:
         """Volume down media player."""
-        if self._volume is None:
+        if self.volume_level is None:
             return
-        self._monoprice.set_volume(self._zone_id, max(self._volume - 1, 0))
+        volume = round(self.volume_level * MAX_VOLUME)
+        self._monoprice.set_volume(self._zone_id, max(volume - 1, 0))
-- 
GitLab


From 352976fd1d36b70d04f713aa831320f8de7a4a85 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 26 Oct 2022 10:29:33 +0200
Subject: [PATCH 826/985] Add rules for converting volumes (#80951)

* Add rules for converting volumes

* Use SensorDeviceClass in new tests

* Tweak tests

* Update flo tests

* Update sensor tests
---
 homeassistant/util/unit_system.py    | 22 +++++++++++++++
 tests/components/flo/test_sensor.py  |  6 ++--
 tests/components/sensor/test_init.py |  6 ++--
 tests/util/test_unit_system.py       | 41 ++++++++++++++++++++++++++++
 4 files changed, 70 insertions(+), 5 deletions(-)

diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 1ab1004e676..42cf429650a 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -36,8 +36,12 @@ from homeassistant.const import (
     TEMPERATURE,
     UNIT_NOT_RECOGNIZED_TEMPLATE,
     VOLUME,
+    VOLUME_CUBIC_FEET,
+    VOLUME_CUBIC_METERS,
+    VOLUME_FLUID_OUNCE,
     VOLUME_GALLONS,
     VOLUME_LITERS,
+    VOLUME_MILLILITERS,
     WIND_SPEED,
 )
 from homeassistant.helpers.frame import report
@@ -270,9 +274,18 @@ METRIC_SYSTEM = UnitSystem(
         ("distance", LENGTH_INCHES): LENGTH_MILLIMETERS,
         ("distance", LENGTH_MILES): LENGTH_KILOMETERS,
         ("distance", LENGTH_YARD): LENGTH_METERS,
+        # Convert non-metric volumes of gas meters
+        ("gas", VOLUME_CUBIC_FEET): VOLUME_CUBIC_METERS,
         # Convert non-metric speeds except knots to km/h
         ("speed", SPEED_FEET_PER_SECOND): SPEED_KILOMETERS_PER_HOUR,
         ("speed", SPEED_MILES_PER_HOUR): SPEED_KILOMETERS_PER_HOUR,
+        # Convert non-metric volumes
+        ("volume", VOLUME_CUBIC_FEET): VOLUME_CUBIC_METERS,
+        ("volume", VOLUME_FLUID_OUNCE): VOLUME_MILLILITERS,
+        ("volume", VOLUME_GALLONS): VOLUME_LITERS,
+        # Convert non-metric volumes of water meters
+        ("water", VOLUME_CUBIC_FEET): VOLUME_CUBIC_METERS,
+        ("water", VOLUME_GALLONS): VOLUME_LITERS,
     },
     length=LENGTH_KILOMETERS,
     mass=MASS_GRAMS,
@@ -291,9 +304,18 @@ US_CUSTOMARY_SYSTEM = UnitSystem(
         ("distance", LENGTH_KILOMETERS): LENGTH_MILES,
         ("distance", LENGTH_METERS): LENGTH_FEET,
         ("distance", LENGTH_MILLIMETERS): LENGTH_INCHES,
+        # Convert non-USCS volumes of gas meters
+        ("gas", VOLUME_CUBIC_METERS): VOLUME_CUBIC_FEET,
         # Convert non-USCS speeds except knots to mph
         ("speed", SPEED_METERS_PER_SECOND): SPEED_MILES_PER_HOUR,
         ("speed", SPEED_KILOMETERS_PER_HOUR): SPEED_MILES_PER_HOUR,
+        # Convert non-USCS volumes
+        ("volume", VOLUME_CUBIC_METERS): VOLUME_CUBIC_FEET,
+        ("volume", VOLUME_LITERS): VOLUME_GALLONS,
+        ("volume", VOLUME_MILLILITERS): VOLUME_FLUID_OUNCE,
+        # Convert non-USCS volumes of water meters
+        ("water", VOLUME_CUBIC_METERS): VOLUME_CUBIC_FEET,
+        ("water", VOLUME_LITERS): VOLUME_GALLONS,
     },
     length=LENGTH_MILES,
     mass=MASS_POUNDS,
diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py
index c3266a84bd8..63cd2b97e70 100644
--- a/tests/components/flo/test_sensor.py
+++ b/tests/components/flo/test_sensor.py
@@ -3,12 +3,14 @@ from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN
 from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass
 from homeassistant.const import ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME
 from homeassistant.setup import async_setup_component
+from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
 
 from .common import TEST_PASSWORD, TEST_USER_ID
 
 
 async def test_sensors(hass, config_entry, aioclient_mock_fixture):
     """Test Flo by Moen sensors."""
+    hass.config.units = US_CUSTOMARY_SYSTEM
     config_entry.add_to_hass(hass)
     assert await async_setup_component(
         hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}
@@ -49,7 +51,7 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture):
         == SensorStateClass.MEASUREMENT
     )
 
-    assert hass.states.get("sensor.smart_water_shutoff_water_temperature").state == "21"
+    assert hass.states.get("sensor.smart_water_shutoff_water_temperature").state == "70"
     assert (
         hass.states.get("sensor.smart_water_shutoff_water_temperature").attributes[
             ATTR_STATE_CLASS
@@ -58,7 +60,7 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture):
     )
 
     # and 3 entities for the detector
-    assert hass.states.get("sensor.kitchen_sink_temperature").state == "16"
+    assert hass.states.get("sensor.kitchen_sink_temperature").state == "61"
     assert (
         hass.states.get("sensor.kitchen_sink_temperature").attributes[ATTR_STATE_CLASS]
         == SensorStateClass.MEASUREMENT
diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py
index 567d00d653d..9bfa6fc46ba 100644
--- a/tests/components/sensor/test_init.py
+++ b/tests/components/sensor/test_init.py
@@ -567,11 +567,11 @@ async def test_custom_unit(
             SensorDeviceClass.VOLUME,
         ),
         (
-            VOLUME_FLUID_OUNCE,
-            VOLUME_LITERS,
             VOLUME_LITERS,
-            78,
+            VOLUME_FLUID_OUNCE,
+            VOLUME_FLUID_OUNCE,
             2.3,
+            77.8,
             SensorDeviceClass.VOLUME,
         ),
         (
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 2331ff08412..db396738503 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -27,7 +27,12 @@ from homeassistant.const import (
     TEMP_CELSIUS,
     TEMPERATURE,
     VOLUME,
+    VOLUME_CUBIC_FEET,
+    VOLUME_CUBIC_METERS,
+    VOLUME_FLUID_OUNCE,
+    VOLUME_GALLONS,
     VOLUME_LITERS,
+    VOLUME_MILLILITERS,
     WIND_SPEED,
 )
 from homeassistant.exceptions import HomeAssistantError
@@ -398,6 +403,10 @@ def test_get_unit_system_invalid(key: str) -> None:
         (SensorDeviceClass.DISTANCE, LENGTH_YARD, LENGTH_METERS),
         (SensorDeviceClass.DISTANCE, LENGTH_KILOMETERS, None),
         (SensorDeviceClass.DISTANCE, "very_long", None),
+        # Test gas meter conversion
+        (SensorDeviceClass.GAS, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
+        (SensorDeviceClass.GAS, VOLUME_CUBIC_METERS, None),
+        (SensorDeviceClass.GAS, "very_much", None),
         # Test speed conversion
         (SensorDeviceClass.SPEED, SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR),
         (SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR),
@@ -405,6 +414,20 @@ def test_get_unit_system_invalid(key: str) -> None:
         (SensorDeviceClass.SPEED, SPEED_KNOTS, None),
         (SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, None),
         (SensorDeviceClass.SPEED, "very_fast", None),
+        # Test volume conversion
+        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
+        (SensorDeviceClass.VOLUME, VOLUME_FLUID_OUNCE, VOLUME_MILLILITERS),
+        (SensorDeviceClass.VOLUME, VOLUME_GALLONS, VOLUME_LITERS),
+        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_METERS, None),
+        (SensorDeviceClass.VOLUME, VOLUME_LITERS, None),
+        (SensorDeviceClass.VOLUME, VOLUME_MILLILITERS, None),
+        (SensorDeviceClass.VOLUME, "very_much", None),
+        # Test water meter conversion
+        (SensorDeviceClass.WATER, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
+        (SensorDeviceClass.WATER, VOLUME_GALLONS, VOLUME_LITERS),
+        (SensorDeviceClass.WATER, VOLUME_CUBIC_METERS, None),
+        (SensorDeviceClass.WATER, VOLUME_LITERS, None),
+        (SensorDeviceClass.WATER, "very_much", None),
     ),
 )
 def test_get_metric_converted_unit_(
@@ -427,6 +450,10 @@ def test_get_metric_converted_unit_(
         (SensorDeviceClass.DISTANCE, LENGTH_MILLIMETERS, LENGTH_INCHES),
         (SensorDeviceClass.DISTANCE, LENGTH_MILES, None),
         (SensorDeviceClass.DISTANCE, "very_long", None),
+        # Test gas meter conversion
+        (SensorDeviceClass.GAS, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
+        (SensorDeviceClass.GAS, VOLUME_CUBIC_FEET, None),
+        (SensorDeviceClass.GAS, "very_much", None),
         # Test speed conversion
         (SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR),
         (SensorDeviceClass.SPEED, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR),
@@ -434,6 +461,20 @@ def test_get_metric_converted_unit_(
         (SensorDeviceClass.SPEED, SPEED_KNOTS, None),
         (SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, None),
         (SensorDeviceClass.SPEED, "very_fast", None),
+        # Test volume conversion
+        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
+        (SensorDeviceClass.VOLUME, VOLUME_LITERS, VOLUME_GALLONS),
+        (SensorDeviceClass.VOLUME, VOLUME_MILLILITERS, VOLUME_FLUID_OUNCE),
+        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_FEET, None),
+        (SensorDeviceClass.VOLUME, VOLUME_FLUID_OUNCE, None),
+        (SensorDeviceClass.VOLUME, VOLUME_GALLONS, None),
+        (SensorDeviceClass.VOLUME, "very_much", None),
+        # Test water meter conversion
+        (SensorDeviceClass.WATER, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
+        (SensorDeviceClass.WATER, VOLUME_LITERS, VOLUME_GALLONS),
+        (SensorDeviceClass.WATER, VOLUME_CUBIC_FEET, None),
+        (SensorDeviceClass.WATER, VOLUME_GALLONS, None),
+        (SensorDeviceClass.WATER, "very_much", None),
     ),
 )
 def test_get_us_converted_unit(
-- 
GitLab


From be7e61b88b72ac3fdee61510e4cfc43595dffddb Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
Date: Wed, 26 Oct 2022 11:27:44 +0200
Subject: [PATCH 827/985] Add unique id for min_max (#81007)

---
 homeassistant/components/min_max/sensor.py | 5 ++++-
 tests/components/min_max/test_sensor.py    | 6 ++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py
index 0f53875861d..87edcd38766 100644
--- a/homeassistant/components/min_max/sensor.py
+++ b/homeassistant/components/min_max/sensor.py
@@ -16,6 +16,7 @@ from homeassistant.const import (
     ATTR_UNIT_OF_MEASUREMENT,
     CONF_NAME,
     CONF_TYPE,
+    CONF_UNIQUE_ID,
     STATE_UNAVAILABLE,
     STATE_UNKNOWN,
 )
@@ -61,6 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
         vol.Optional(CONF_NAME): cv.string,
         vol.Required(CONF_ENTITY_IDS): cv.entity_ids,
         vol.Optional(CONF_ROUND_DIGITS, default=2): vol.Coerce(int),
+        vol.Optional(CONF_UNIQUE_ID): str,
     }
 )
 
@@ -102,11 +104,12 @@ async def async_setup_platform(
     name = config.get(CONF_NAME)
     sensor_type = config.get(CONF_TYPE)
     round_digits = config.get(CONF_ROUND_DIGITS)
+    unique_id = config.get(CONF_UNIQUE_ID)
 
     await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
 
     async_add_entities(
-        [MinMaxSensor(entity_ids, name, sensor_type, round_digits, None)]
+        [MinMaxSensor(entity_ids, name, sensor_type, round_digits, unique_id)]
     )
 
 
diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py
index 47435dfbaf3..4819cc31a9b 100644
--- a/tests/components/min_max/test_sensor.py
+++ b/tests/components/min_max/test_sensor.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
 )
+import homeassistant.helpers.entity_registry as er
 from homeassistant.setup import async_setup_component
 
 from tests.common import get_fixture_path
@@ -63,6 +64,7 @@ async def test_min_sensor(hass):
             "name": "test_min",
             "type": "min",
             "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
+            "unique_id": "very_unique_id",
         }
     }
 
@@ -81,6 +83,10 @@ async def test_min_sensor(hass):
     assert entity_ids[2] == state.attributes.get("min_entity_id")
     assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
 
+    entity_reg = er.async_get(hass)
+    entity = entity_reg.async_get("sensor.test_min")
+    assert entity.unique_id == "very_unique_id"
+
 
 async def test_max_sensor(hass):
     """Test the max sensor."""
-- 
GitLab


From c59944bc8451fa3e191a41cc93a0553371cb4fe9 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 11:54:44 +0200
Subject: [PATCH 828/985] Fix precipitation units in darksky (#80611)

---
 homeassistant/components/darksky/sensor.py | 23 +++++++++++-----------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py
index 663b92a7ccf..504029d339b 100644
--- a/homeassistant/components/darksky/sensor.py
+++ b/homeassistant/components/darksky/sensor.py
@@ -30,8 +30,6 @@ from homeassistant.const import (
     LENGTH_KILOMETERS,
     LENGTH_MILES,
     PERCENTAGE,
-    PRECIPITATION_INCHES,
-    PRECIPITATION_MILLIMETERS_PER_HOUR,
     PRESSURE_MBAR,
     SPEED_KILOMETERS_PER_HOUR,
     SPEED_METERS_PER_SECOND,
@@ -39,6 +37,7 @@ from homeassistant.const import (
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
     UV_INDEX,
+    UnitOfVolumetricFlux,
 )
 from homeassistant.core import HomeAssistant
 import homeassistant.helpers.config_validation as cv
@@ -146,11 +145,11 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = {
     "precip_intensity": DarkskySensorEntityDescription(
         key="precip_intensity",
         name="Precip Intensity",
-        si_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
-        us_unit=PRECIPITATION_INCHES,
-        ca_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
-        uk_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
-        uk2_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        si_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        us_unit=UnitOfVolumetricFlux.INCHES_PER_HOUR,
+        ca_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        uk_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        uk2_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         icon="mdi:weather-rainy",
         forecast_mode=["currently", "minutely", "hourly", "daily"],
     ),
@@ -392,11 +391,11 @@ SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = {
     "precip_intensity_max": DarkskySensorEntityDescription(
         key="precip_intensity_max",
         name="Daily Max Precip Intensity",
-        si_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
-        us_unit=PRECIPITATION_INCHES,
-        ca_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
-        uk_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
-        uk2_unit=PRECIPITATION_MILLIMETERS_PER_HOUR,
+        si_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        us_unit=UnitOfVolumetricFlux.INCHES_PER_HOUR,
+        ca_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        uk_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        uk2_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         icon="mdi:thermometer",
         forecast_mode=["daily"],
     ),
-- 
GitLab


From 9ed31bb8dfd4694d35b2f46ce6c2c7734eddbb5f Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
Date: Wed, 26 Oct 2022 12:39:23 +0200
Subject: [PATCH 829/985] fix integration descriptions (#81008)

---
 homeassistant/loader.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index a2fc2594b61..d43eecda778 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -264,7 +264,6 @@ async def async_get_integration_descriptions(
     custom_integrations = await async_get_custom_components(hass)
     custom_flows: dict[str, Any] = {
         "integration": {},
-        "hardware": {},
         "helper": {},
     }
 
@@ -273,14 +272,14 @@ async def async_get_integration_descriptions(
         if integration.integration_type in ("entity", "system"):
             continue
 
-        for integration_type in ("integration", "hardware", "helper"):
+        for integration_type in ("integration", "helper"):
             if integration.domain not in core_flows[integration_type]:
                 continue
             del core_flows[integration_type][integration.domain]
         if integration.domain in core_flows["translated_name"]:
             core_flows["translated_name"].remove(integration.domain)
 
-        if integration.integration_type in ("hardware", "helper"):
+        if integration.integration_type == "helper":
             integration_key: str = integration.integration_type
         else:
             integration_key = "integration"
-- 
GitLab


From d65e639f0087389ea5fb5da3f07c79cbb6d78d1b Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Wed, 26 Oct 2022 12:40:15 +0200
Subject: [PATCH 830/985] Update orjson to 3.8.1 (#81000)

---
 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 80402eef843..b66832b4737 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -26,7 +26,7 @@ httpx==0.23.0
 ifaddr==0.1.7
 jinja2==3.1.2
 lru-dict==1.1.8
-orjson==3.7.11
+orjson==3.8.1
 paho-mqtt==1.6.1
 pillow==9.2.0
 pip>=21.0,<22.4
diff --git a/pyproject.toml b/pyproject.toml
index c09d7f6854e..3ca463d6fc1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,7 +43,7 @@ dependencies    = [
     "PyJWT==2.5.0",
     # PyJWT has loose dependency. We want the latest one.
     "cryptography==38.0.1",
-    "orjson==3.7.11",
+    "orjson==3.8.1",
     "pip>=21.0,<22.4",
     "python-slugify==4.0.1",
     "pyyaml==6.0",
diff --git a/requirements.txt b/requirements.txt
index 785d66a08e3..962a9d59dc6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,7 +17,7 @@ jinja2==3.1.2
 lru-dict==1.1.8
 PyJWT==2.5.0
 cryptography==38.0.1
-orjson==3.7.11
+orjson==3.8.1
 pip>=21.0,<22.4
 python-slugify==4.0.1
 pyyaml==6.0
-- 
GitLab


From e3233f72ce69103eae88646fcc95d89f858e46cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= <github@dahoiv.net>
Date: Wed, 26 Oct 2022 12:41:23 +0200
Subject: [PATCH 831/985] Tibber strict typing (#79407)

Co-authored-by: Franck Nijhof <git@frenck.dev>
---
 .strict-typing                                |  1 +
 homeassistant/components/tibber/__init__.py   |  4 +-
 .../components/tibber/config_flow.py          |  8 ++-
 homeassistant/components/tibber/manifest.json |  2 +-
 homeassistant/components/tibber/notify.py     | 16 ++++-
 homeassistant/components/tibber/sensor.py     | 69 +++++++++++--------
 mypy.ini                                      | 10 +++
 requirements_all.txt                          |  2 +-
 requirements_test_all.txt                     |  2 +-
 9 files changed, 77 insertions(+), 37 deletions(-)

diff --git a/.strict-typing b/.strict-typing
index 3e14772318c..e79fb6b1a26 100644
--- a/.strict-typing
+++ b/.strict-typing
@@ -260,6 +260,7 @@ homeassistant.components.tag.*
 homeassistant.components.tailscale.*
 homeassistant.components.tautulli.*
 homeassistant.components.tcp.*
+homeassistant.components.tibber.*
 homeassistant.components.tile.*
 homeassistant.components.tilt_ble.*
 homeassistant.components.tolo.*
diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py
index 34f5843412e..35507986f90 100644
--- a/homeassistant/components/tibber/__init__.py
+++ b/homeassistant/components/tibber/__init__.py
@@ -12,7 +12,7 @@ from homeassistant.const import (
     EVENT_HOMEASSISTANT_STOP,
     Platform,
 )
-from homeassistant.core import HomeAssistant
+from homeassistant.core import Event, HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
 from homeassistant.helpers import discovery
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     )
     hass.data[DOMAIN] = tibber_connection
 
-    async def _close(event):
+    async def _close(event: Event) -> None:
         await tibber_connection.rt_disconnect()
 
     entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close))
diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py
index 4e804225c56..d0adc0391ab 100644
--- a/homeassistant/components/tibber/config_flow.py
+++ b/homeassistant/components/tibber/config_flow.py
@@ -1,5 +1,8 @@
 """Adds config flow for Tibber integration."""
+from __future__ import annotations
+
 import asyncio
+from typing import Any
 
 import aiohttp
 import tibber
@@ -7,6 +10,7 @@ import voluptuous as vol
 
 from homeassistant import config_entries
 from homeassistant.const import CONF_ACCESS_TOKEN
+from homeassistant.data_entry_flow import FlowResult
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
 
 from .const import DOMAIN
@@ -19,7 +23,9 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
 
     VERSION = 1
 
-    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 initial step."""
 
         self._async_abort_entries_match()
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index dcb15d55002..5341febc62a 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -3,7 +3,7 @@
   "domain": "tibber",
   "name": "Tibber",
   "documentation": "https://www.home-assistant.io/integrations/tibber",
-  "requirements": ["pyTibber==0.25.4"],
+  "requirements": ["pyTibber==0.25.6"],
   "codeowners": ["@danielhiversen"],
   "quality_scale": "silver",
   "config_flow": true,
diff --git a/homeassistant/components/tibber/notify.py b/homeassistant/components/tibber/notify.py
index ab912eb33da..270528fc4e9 100644
--- a/homeassistant/components/tibber/notify.py
+++ b/homeassistant/components/tibber/notify.py
@@ -1,19 +1,29 @@
 """Support for Tibber notifications."""
+from __future__ import annotations
+
 import asyncio
+from collections.abc import Callable
 import logging
+from typing import Any
 
 from homeassistant.components.notify import (
     ATTR_TITLE,
     ATTR_TITLE_DEFAULT,
     BaseNotificationService,
 )
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 from . import DOMAIN as TIBBER_DOMAIN
 
 _LOGGER = logging.getLogger(__name__)
 
 
-async def async_get_service(hass, config, discovery_info=None):
+async def async_get_service(
+    hass: HomeAssistant,
+    config: ConfigType,
+    discovery_info: DiscoveryInfoType | None = None,
+) -> TibberNotificationService:
     """Get the Tibber notification service."""
     tibber_connection = hass.data[TIBBER_DOMAIN]
     return TibberNotificationService(tibber_connection.send_notification)
@@ -22,11 +32,11 @@ async def async_get_service(hass, config, discovery_info=None):
 class TibberNotificationService(BaseNotificationService):
     """Implement the notification service for Tibber."""
 
-    def __init__(self, notify):
+    def __init__(self, notify: Callable) -> None:
         """Initialize the service."""
         self._notify = notify
 
-    async def async_send_message(self, message=None, **kwargs):
+    async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
         """Send a message to Tibber devices."""
         title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
         try:
diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py
index ca0c253590f..4dcc4a8a777 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -2,11 +2,14 @@
 from __future__ import annotations
 
 import asyncio
+import datetime
 from datetime import timedelta
 import logging
 from random import randrange
+from typing import Any
 
 import aiohttp
+import tibber
 
 from homeassistant.components.recorder import get_instance
 from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
@@ -31,7 +34,7 @@ from homeassistant.const import (
     POWER_WATT,
     SIGNAL_STRENGTH_DECIBELS,
 )
-from homeassistant.core import HomeAssistant, callback
+from homeassistant.core import Event, HomeAssistant, callback
 from homeassistant.exceptions import PlatformNotReady
 from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
 from homeassistant.helpers.entity import DeviceInfo, EntityCategory
@@ -296,7 +299,9 @@ async def async_setup_entry(
 class TibberSensor(SensorEntity):
     """Representation of a generic Tibber sensor."""
 
-    def __init__(self, *args, tibber_home, **kwargs):
+    def __init__(
+        self, *args: Any, tibber_home: tibber.TibberHome, **kwargs: Any
+    ) -> None:
         """Initialize the sensor."""
         super().__init__(*args, **kwargs)
         self._tibber_home = tibber_home
@@ -305,11 +310,11 @@ class TibberSensor(SensorEntity):
             self._home_name = tibber_home.info["viewer"]["home"]["address"].get(
                 "address1", ""
             )
-        self._device_name = None
-        self._model = None
+        self._device_name: None | str = None
+        self._model: None | str = None
 
     @property
-    def device_info(self):
+    def device_info(self) -> DeviceInfo:
         """Return the device_info of the device."""
         device_info = DeviceInfo(
             identifiers={(TIBBER_DOMAIN, self._tibber_home.home_id)},
@@ -324,10 +329,10 @@ class TibberSensor(SensorEntity):
 class TibberSensorElPrice(TibberSensor):
     """Representation of a Tibber sensor for el price."""
 
-    def __init__(self, tibber_home):
+    def __init__(self, tibber_home: tibber.TibberHome) -> None:
         """Initialize the sensor."""
         super().__init__(tibber_home=tibber_home)
-        self._last_updated = None
+        self._last_updated: datetime.datetime | None = None
         self._spread_load_constant = randrange(5000)
 
         self._attr_available = False
@@ -380,7 +385,7 @@ class TibberSensorElPrice(TibberSensor):
         self._attr_native_unit_of_measurement = self._tibber_home.price_unit
 
     @Throttle(MIN_TIME_BETWEEN_UPDATES)
-    async def _fetch_data(self):
+    async def _fetch_data(self) -> None:
         _LOGGER.debug("Fetching data")
         try:
             await self._tibber_home.update_info_and_price_info()
@@ -401,10 +406,10 @@ class TibberDataSensor(TibberSensor, CoordinatorEntity["TibberDataCoordinator"])
 
     def __init__(
         self,
-        tibber_home,
+        tibber_home: tibber.TibberHome,
         coordinator: TibberDataCoordinator,
         entity_description: SensorEntityDescription,
-    ):
+    ) -> None:
         """Initialize the sensor."""
         super().__init__(coordinator=coordinator, tibber_home=tibber_home)
         self.entity_description = entity_description
@@ -419,7 +424,7 @@ class TibberDataSensor(TibberSensor, CoordinatorEntity["TibberDataCoordinator"])
         self._device_name = self._home_name
 
     @property
-    def native_value(self):
+    def native_value(self) -> Any:
         """Return the value of the sensor."""
         return getattr(self._tibber_home, self.entity_description.key)
 
@@ -429,11 +434,11 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"])
 
     def __init__(
         self,
-        tibber_home,
+        tibber_home: tibber.TibberHome,
         description: SensorEntityDescription,
-        initial_state,
+        initial_state: float,
         coordinator: TibberRtDataCoordinator,
-    ):
+    ) -> None:
         """Initialize the sensor."""
         super().__init__(coordinator=coordinator, tibber_home=tibber_home)
         self.entity_description = description
@@ -486,12 +491,17 @@ class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"])
 class TibberRtDataCoordinator(DataUpdateCoordinator):
     """Handle Tibber realtime data."""
 
-    def __init__(self, async_add_entities, tibber_home, hass):
+    def __init__(
+        self,
+        async_add_entities: AddEntitiesCallback,
+        tibber_home: tibber.TibberHome,
+        hass: HomeAssistant,
+    ) -> None:
         """Initialize the data handler."""
         self._async_add_entities = async_add_entities
         self._tibber_home = tibber_home
         self.hass = hass
-        self._added_sensors = set()
+        self._added_sensors: set[str] = set()
         super().__init__(
             hass,
             _LOGGER,
@@ -506,12 +516,12 @@ class TibberRtDataCoordinator(DataUpdateCoordinator):
         hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
 
     @callback
-    def _handle_ha_stop(self, _event) -> None:
+    def _handle_ha_stop(self, _event: Event) -> None:
         """Handle Home Assistant stopping."""
         self._async_remove_device_updates_handler()
 
     @callback
-    def _add_sensors(self):
+    def _add_sensors(self) -> None:
         """Add sensor."""
         if not (live_measurement := self.get_live_measurement()):
             return
@@ -534,7 +544,7 @@ class TibberRtDataCoordinator(DataUpdateCoordinator):
         if new_entities:
             self._async_add_entities(new_entities)
 
-    def get_live_measurement(self):
+    def get_live_measurement(self) -> Any:
         """Get live measurement data."""
         if errors := self.data.get("errors"):
             _LOGGER.error(errors[0])
@@ -545,7 +555,7 @@ class TibberRtDataCoordinator(DataUpdateCoordinator):
 class TibberDataCoordinator(DataUpdateCoordinator):
     """Handle Tibber data and insert statistics."""
 
-    def __init__(self, hass, tibber_connection):
+    def __init__(self, hass: HomeAssistant, tibber_connection: tibber.Tibber) -> None:
         """Initialize the data handler."""
         super().__init__(
             hass,
@@ -555,13 +565,13 @@ class TibberDataCoordinator(DataUpdateCoordinator):
         )
         self._tibber_connection = tibber_connection
 
-    async def _async_update_data(self):
+    async def _async_update_data(self) -> None:
         """Update data via API."""
         await self._tibber_connection.fetch_consumption_data_active_homes()
         await self._tibber_connection.fetch_production_data_active_homes()
         await self._insert_statistics()
 
-    async def _insert_statistics(self):
+    async def _insert_statistics(self) -> None:
         """Insert Tibber statistics."""
         for home in self._tibber_connection.get_homes():
             sensors = []
@@ -602,9 +612,10 @@ class TibberDataCoordinator(DataUpdateCoordinator):
                         else home.hourly_consumption_data
                     )
 
-                    start = dt_util.parse_datetime(hourly_data[0]["from"]) - timedelta(
-                        hours=1
-                    )
+                    from_time = dt_util.parse_datetime(hourly_data[0]["from"])
+                    if from_time is None:
+                        continue
+                    start = from_time - timedelta(hours=1)
                     stat = await get_instance(self.hass).async_add_executor_job(
                         statistics_during_period,
                         self.hass,
@@ -623,15 +634,17 @@ class TibberDataCoordinator(DataUpdateCoordinator):
                     if data.get(sensor_type) is None:
                         continue
 
-                    start = dt_util.parse_datetime(data["from"])
-                    if last_stats_time is not None and start <= last_stats_time:
+                    from_time = dt_util.parse_datetime(data["from"])
+                    if from_time is None or (
+                        last_stats_time is not None and from_time <= last_stats_time
+                    ):
                         continue
 
                     _sum += data[sensor_type]
 
                     statistics.append(
                         StatisticData(
-                            start=start,
+                            start=from_time,
                             state=data[sensor_type],
                             sum=_sum,
                         )
diff --git a/mypy.ini b/mypy.ini
index 846f91303e6..cd6bc14169d 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -2353,6 +2353,16 @@ disallow_untyped_defs = true
 warn_return_any = true
 warn_unreachable = true
 
+[mypy-homeassistant.components.tibber.*]
+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.tile.*]
 check_untyped_defs = true
 disallow_incomplete_defs = true
diff --git a/requirements_all.txt b/requirements_all.txt
index 78da40a9eea..9b5b1455efa 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1409,7 +1409,7 @@ pyRFXtrx==0.30.0
 pySwitchmate==0.5.1
 
 # homeassistant.components.tibber
-pyTibber==0.25.4
+pyTibber==0.25.6
 
 # homeassistant.components.dlink
 pyW215==0.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index bfd614cec5a..2b5b8c92d7b 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1009,7 +1009,7 @@ pyMetno==0.9.0
 pyRFXtrx==0.30.0
 
 # homeassistant.components.tibber
-pyTibber==0.25.4
+pyTibber==0.25.6
 
 # homeassistant.components.nextbus
 py_nextbusnext==0.1.5
-- 
GitLab


From 9715b6c86287916601be8af3bafa8756b08b42cc Mon Sep 17 00:00:00 2001
From: G Johansson <goran.johansson@shiftit.se>
Date: Wed, 26 Oct 2022 12:42:21 +0200
Subject: [PATCH 832/985] Remove yaml import anthemav (#79931)

---
 .../components/anthemav/config_flow.py        |  6 --
 .../components/anthemav/media_player.py       | 57 ++-----------------
 .../components/anthemav/strings.json          |  6 --
 .../components/anthemav/translations/en.json  |  6 --
 tests/components/anthemav/test_config_flow.py | 30 +---------
 5 files changed, 6 insertions(+), 99 deletions(-)

diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py
index 0e878bcc913..23694654eb3 100644
--- a/homeassistant/components/anthemav/config_flow.py
+++ b/homeassistant/components/anthemav/config_flow.py
@@ -92,9 +92,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
         return self.async_show_form(
             step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
         )
-
-    async def async_step_import(
-        self, user_input: dict[str, Any] | None = None
-    ) -> FlowResult:
-        """Import a config entry from configuration.yaml."""
-        return await self.async_step_user(user_input)
diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py
index 0c5837e154e..2ab23ff2d37 100644
--- a/homeassistant/components/anthemav/media_player.py
+++ b/homeassistant/components/anthemav/media_player.py
@@ -5,72 +5,23 @@ import logging
 
 from anthemav.connection import Connection
 from anthemav.protocol import AVR
-import voluptuous as vol
 
 from homeassistant.components.media_player import (
-    PLATFORM_SCHEMA,
     MediaPlayerDeviceClass,
     MediaPlayerEntity,
     MediaPlayerEntityFeature,
     MediaPlayerState,
 )
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_MAC, CONF_NAME
 from homeassistant.core import HomeAssistant, callback
-import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.dispatcher import async_dispatcher_connect
 from homeassistant.helpers.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 .const import (
-    ANTHEMAV_UDATE_SIGNAL,
-    CONF_MODEL,
-    DEFAULT_NAME,
-    DEFAULT_PORT,
-    DOMAIN,
-    MANUFACTURER,
-)
-
-_LOGGER = logging.getLogger(__name__)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
-    {
-        vol.Required(CONF_HOST): cv.string,
-        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
-        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
-    }
-)
 
+from .const import ANTHEMAV_UDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER
 
-async def async_setup_platform(
-    hass: HomeAssistant,
-    config: ConfigType,
-    async_add_entities: AddEntitiesCallback,
-    discovery_info: DiscoveryInfoType | None = None,
-) -> None:
-    """Set up our socket to the AVR."""
-    async_create_issue(
-        hass,
-        DOMAIN,
-        "deprecated_yaml",
-        breaks_in_ha_version="2022.10.0",
-        is_fixable=False,
-        severity=IssueSeverity.WARNING,
-        translation_key="deprecated_yaml",
-    )
-    _LOGGER.warning(
-        "Configuration of the Anthem A/V Receivers integration in YAML is "
-        "deprecated and will be removed in Home Assistant 2022.10; Your "
-        "existing configuration has been imported into the UI automatically "
-        "and can be safely removed from your configuration.yaml file"
-    )
-    await hass.config_entries.flow.async_init(
-        DOMAIN,
-        context={"source": SOURCE_IMPORT},
-        data=config,
-    )
+_LOGGER = logging.getLogger(__name__)
 
 
 async def async_setup_entry(
diff --git a/homeassistant/components/anthemav/strings.json b/homeassistant/components/anthemav/strings.json
index b4e777c4de1..1f1dd0ec75b 100644
--- a/homeassistant/components/anthemav/strings.json
+++ b/homeassistant/components/anthemav/strings.json
@@ -15,11 +15,5 @@
     "abort": {
       "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
     }
-  },
-  "issues": {
-    "deprecated_yaml": {
-      "title": "The Anthem A/V Receivers YAML configuration is being removed",
-      "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
-    }
   }
 }
diff --git a/homeassistant/components/anthemav/translations/en.json b/homeassistant/components/anthemav/translations/en.json
index af4c83eb2a8..9177d5a6e70 100644
--- a/homeassistant/components/anthemav/translations/en.json
+++ b/homeassistant/components/anthemav/translations/en.json
@@ -15,11 +15,5 @@
                 }
             }
         }
-    },
-    "issues": {
-        "deprecated_yaml": {
-            "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
-            "title": "The Anthem A/V Receivers YAML configuration is being removed"
-        }
     }
 }
\ No newline at end of file
diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py
index f8bec435dc6..e62fb4ba52c 100644
--- a/tests/components/anthemav/test_config_flow.py
+++ b/tests/components/anthemav/test_config_flow.py
@@ -2,10 +2,9 @@
 from unittest.mock import AsyncMock, patch
 
 from anthemav.device_error import DeviceError
-import pytest
 
 from homeassistant.components.anthemav.const import DOMAIN
-from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+from homeassistant.config_entries import SOURCE_USER
 from homeassistant.core import HomeAssistant
 from homeassistant.data_entry_flow import FlowResultType
 
@@ -95,36 +94,11 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
     assert result2["errors"] == {"base": "cannot_connect"}
 
 
-async def test_import_configuration(
-    hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock
-) -> None:
-    """Test we import existing configuration."""
-    config = {
-        "host": "1.1.1.1",
-        "port": 14999,
-        "name": "Anthem Av Import",
-    }
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": SOURCE_IMPORT}, data=config
-    )
-
-    assert result["type"] == FlowResultType.CREATE_ENTRY
-    assert result["data"] == {
-        "host": "1.1.1.1",
-        "port": 14999,
-        "name": "Anthem Av Import",
-        "mac": "00:00:00:00:00:01",
-        "model": "MRX 520",
-    }
-
-
-@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT])
 async def test_device_already_configured(
     hass: HomeAssistant,
     mock_connection_create: AsyncMock,
     mock_anthemav: AsyncMock,
     mock_config_entry: MockConfigEntry,
-    source: str,
 ) -> None:
     """Test we import existing configuration."""
     config = {
@@ -134,7 +108,7 @@ async def test_device_already_configured(
 
     mock_config_entry.add_to_hass(hass)
     result = await hass.config_entries.flow.async_init(
-        DOMAIN, context={"source": source}, data=config
+        DOMAIN, context={"source": SOURCE_USER}, data=config
     )
 
     assert result.get("type") == FlowResultType.ABORT
-- 
GitLab


From d5a2484076bd45eac9f91ac677c11b3bac6da270 Mon Sep 17 00:00:00 2001
From: Nolan Gilley <nkgilley@gmail.com>
Date: Wed, 26 Oct 2022 06:43:06 -0400
Subject: [PATCH 833/985] Don't set force bool during set_humidity (#80269)

---
 homeassistant/components/generic_hygrostat/humidifier.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py
index 0f111ca3d87..720c76e766d 100644
--- a/homeassistant/components/generic_hygrostat/humidifier.py
+++ b/homeassistant/components/generic_hygrostat/humidifier.py
@@ -282,7 +282,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity):
             return
 
         self._target_humidity = humidity
-        await self._async_operate(force=True)
+        await self._async_operate()
         await self.async_update_ha_state()
 
     @property
-- 
GitLab


From 5757197fb6a4d38e4fd2db3b62361c80100923bd Mon Sep 17 00:00:00 2001
From: Klaas Neirinck <kneirinck@users.noreply.github.com>
Date: Wed, 26 Oct 2022 12:44:23 +0200
Subject: [PATCH 834/985] Add auto preset to Comfoconnect fan (#80697)

---
 homeassistant/components/comfoconnect/fan.py | 50 ++++++++++++++++++--
 1 file changed, 47 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py
index dd2c9632d7c..7577dd77aeb 100644
--- a/homeassistant/components/comfoconnect/fan.py
+++ b/homeassistant/components/comfoconnect/fan.py
@@ -10,7 +10,10 @@ from pycomfoconnect import (
     CMD_FAN_MODE_HIGH,
     CMD_FAN_MODE_LOW,
     CMD_FAN_MODE_MEDIUM,
+    CMD_MODE_AUTO,
+    CMD_MODE_MANUAL,
     SENSOR_FAN_SPEED_MODE,
+    SENSOR_OPERATING_MODE_BIS,
 )
 
 from homeassistant.components.fan import FanEntity, FanEntityFeature
@@ -37,6 +40,9 @@ CMD_MAPPING = {
 
 SPEED_RANGE = (1, 3)  # away is not included in speeds and instead mapped to off
 
+PRESET_MODE_AUTO = "auto"
+PRESET_MODES = [PRESET_MODE_AUTO]
+
 
 def setup_platform(
     hass: HomeAssistant,
@@ -55,7 +61,8 @@ class ComfoConnectFan(FanEntity):
 
     _attr_icon = "mdi:air-conditioner"
     _attr_should_poll = False
-    _attr_supported_features = FanEntityFeature.SET_SPEED
+    _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE
+    _attr_preset_modes = PRESET_MODES
     current_speed = None
 
     def __init__(self, ccb: ComfoConnectBridge) -> None:
@@ -63,6 +70,7 @@ class ComfoConnectFan(FanEntity):
         self._ccb = ccb
         self._attr_name = ccb.name
         self._attr_unique_id = ccb.unique_id
+        self._attr_preset_mode = None
 
     async def async_added_to_hass(self) -> None:
         """Register for sensor updates."""
@@ -71,14 +79,24 @@ class ComfoConnectFan(FanEntity):
             async_dispatcher_connect(
                 self.hass,
                 SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(SENSOR_FAN_SPEED_MODE),
-                self._handle_update,
+                self._handle_speed_update,
+            )
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass,
+                SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(SENSOR_OPERATING_MODE_BIS),
+                self._handle_mode_update,
             )
         )
         await self.hass.async_add_executor_job(
             self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE
         )
+        await self.hass.async_add_executor_job(
+            self._ccb.comfoconnect.register_sensor, SENSOR_OPERATING_MODE_BIS
+        )
 
-    def _handle_update(self, value: float) -> None:
+    def _handle_speed_update(self, value: float) -> None:
         """Handle update callbacks."""
         _LOGGER.debug(
             "Handle update for fan speed (%d): %s", SENSOR_FAN_SPEED_MODE, value
@@ -86,6 +104,16 @@ class ComfoConnectFan(FanEntity):
         self.current_speed = value
         self.schedule_update_ha_state()
 
+    def _handle_mode_update(self, value: int) -> None:
+        """Handle update callbacks."""
+        _LOGGER.debug(
+            "Handle update for operating mode (%d): %s",
+            SENSOR_OPERATING_MODE_BIS,
+            value,
+        )
+        self._attr_preset_mode = PRESET_MODE_AUTO if value == -1 else None
+        self.schedule_update_ha_state()
+
     @property
     def percentage(self) -> int | None:
         """Return the current speed percentage."""
@@ -105,6 +133,10 @@ class ComfoConnectFan(FanEntity):
         **kwargs: Any,
     ) -> None:
         """Turn on the fan."""
+        if preset_mode:
+            self.set_preset_mode(preset_mode)
+            return
+
         if percentage is None:
             self.set_percentage(1)  # Set fan speed to low
         else:
@@ -125,3 +157,15 @@ class ComfoConnectFan(FanEntity):
             cmd = CMD_MAPPING[speed]
 
         self._ccb.comfoconnect.cmd_rmi_request(cmd)
+
+    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:
+            raise ValueError(f"Invalid preset mode: {preset_mode}")
-- 
GitLab


From 7a04ba96f3575989ea1a1e43e9dc4313867c0f8d Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Wed, 26 Oct 2022 13:27:13 +0200
Subject: [PATCH 835/985] Adjust unique ID of Octoprint camera entity (#80996)

---
 homeassistant/components/octoprint/camera.py | 2 +-
 tests/components/octoprint/test_camera.py    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/octoprint/camera.py b/homeassistant/components/octoprint/camera.py
index ea886c97936..653c15f1843 100644
--- a/homeassistant/components/octoprint/camera.py
+++ b/homeassistant/components/octoprint/camera.py
@@ -55,5 +55,5 @@ class OctoprintCamera(MjpegCamera):
             mjpeg_url=camera_settings.stream_url,
             name="OctoPrint Camera",
             still_image_url=camera_settings.external_snapshot_url,
-            unique_id=f"camera-{device_id}",
+            unique_id=device_id,
         )
diff --git a/tests/components/octoprint/test_camera.py b/tests/components/octoprint/test_camera.py
index c95cf924d15..2badf1285ce 100644
--- a/tests/components/octoprint/test_camera.py
+++ b/tests/components/octoprint/test_camera.py
@@ -29,7 +29,7 @@ async def test_camera(hass):
 
     entry = entity_registry.async_get("camera.octoprint_camera")
     assert entry is not None
-    assert entry.unique_id == "camera-uuid"
+    assert entry.unique_id == "uuid"
 
 
 async def test_camera_disabled(hass):
-- 
GitLab


From 7796f361fc131c55ffb76133f2219db61561708e Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:32:06 +0200
Subject: [PATCH 836/985] Use precipitation_intensity class in integrations
 (#80615)

---
 homeassistant/components/aemet/const.py            | 3 +++
 homeassistant/components/ambient_station/sensor.py | 2 +-
 homeassistant/components/buienradar/sensor.py      | 4 ++--
 homeassistant/components/ecowitt/sensor.py         | 2 ++
 homeassistant/components/rfxtrx/sensor.py          | 1 +
 homeassistant/components/tellduslive/sensor.py     | 2 +-
 6 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py
index 46742a85beb..7058257d808 100644
--- a/homeassistant/components/aemet/const.py
+++ b/homeassistant/components/aemet/const.py
@@ -209,6 +209,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key=ATTR_API_FORECAST_PRECIPITATION,
         name="Precipitation",
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     SensorEntityDescription(
         key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY,
@@ -266,6 +267,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key=ATTR_API_RAIN,
         name="Rain",
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     SensorEntityDescription(
         key=ATTR_API_RAIN_PROB,
@@ -277,6 +279,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key=ATTR_API_SNOW,
         name="Snow",
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     SensorEntityDescription(
         key=ATTR_API_SNOW_PROB,
diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py
index 48ee70f9c84..b1e98261c6e 100644
--- a/homeassistant/components/ambient_station/sensor.py
+++ b/homeassistant/components/ambient_station/sensor.py
@@ -194,9 +194,9 @@ SENSOR_DESCRIPTIONS = (
     SensorEntityDescription(
         key=TYPE_HOURLYRAININ,
         name="Hourly rain rate",
-        icon="mdi:water",
         native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     SensorEntityDescription(
         key=TYPE_HUMIDITY10,
diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py
index 52e1129dfa8..bf44c884147 100644
--- a/homeassistant/components/buienradar/sensor.py
+++ b/homeassistant/components/buienradar/sensor.py
@@ -184,8 +184,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key="precipitation",
         name="Precipitation",
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
-        icon="mdi:weather-pouring",
         state_class=SensorStateClass.MEASUREMENT,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     SensorEntityDescription(
         key="irradiance",
@@ -198,7 +198,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
         key="precipitation_forecast_average",
         name="Precipitation forecast average",
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
-        icon="mdi:weather-pouring",
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     SensorEntityDescription(
         key="precipitation_forecast_total",
diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py
index 35e929ae2c2..200ae1d73fc 100644
--- a/homeassistant/components/ecowitt/sensor.py
+++ b/homeassistant/components/ecowitt/sensor.py
@@ -158,11 +158,13 @@ ECOWITT_SENSORS_MAPPING: Final = {
         key="RAIN_RATE_MM",
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     EcoWittSensorTypes.RAIN_RATE_INCHES: SensorEntityDescription(
         key="RAIN_RATE_INCHES",
         native_unit_of_measurement=UnitOfVolumetricFlux.INCHES_PER_HOUR,
         state_class=SensorStateClass.MEASUREMENT,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     EcoWittSensorTypes.LIGHTNING_DISTANCE_KM: SensorEntityDescription(
         key="LIGHTNING_DISTANCE_KM",
diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py
index 6be2d0cf725..5f85f344e9b 100644
--- a/homeassistant/components/rfxtrx/sensor.py
+++ b/homeassistant/components/rfxtrx/sensor.py
@@ -173,6 +173,7 @@ SENSOR_TYPES = (
         name="Rain rate",
         state_class=SensorStateClass.MEASUREMENT,
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     RfxtrxSensorEntityDescription(
         key="Sound",
diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py
index ecccba8b734..ed3efeb4344 100644
--- a/homeassistant/components/tellduslive/sensor.py
+++ b/homeassistant/components/tellduslive/sensor.py
@@ -58,8 +58,8 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
         key=SENSOR_TYPE_RAINRATE,
         name="Rain rate",
         native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
-        icon="mdi:water",
         state_class=SensorStateClass.MEASUREMENT,
+        device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
     ),
     SENSOR_TYPE_RAINTOTAL: SensorEntityDescription(
         key=SENSOR_TYPE_RAINTOTAL,
-- 
GitLab


From 5669ca74d358a1b3b7fa79087b302f00c7ccecdc Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
Date: Wed, 26 Oct 2022 22:46:34 +1100
Subject: [PATCH 837/985] Disable Powerview signal sensor by default (#81014)

---
 homeassistant/components/hunterdouglas_powerview/sensor.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py
index a4a3da57121..e4f6d52287b 100644
--- a/homeassistant/components/hunterdouglas_powerview/sensor.py
+++ b/homeassistant/components/hunterdouglas_powerview/sensor.py
@@ -73,6 +73,7 @@ SENSORS: Final = [
         ),
         create_sensor_fn=lambda shade: bool(ATTR_SIGNAL_STRENGTH in shade.raw_data),
         update_fn=lambda shade: shade.refresh(),
+        entity_registry_enabled_default=False,
     ),
 ]
 
-- 
GitLab


From 052c673c9e3d4bc46a04bfc6f0ac94e171771aa4 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:47:29 +0200
Subject: [PATCH 838/985] Use `wind_speed` device class instead of `speed` in
 Accuweather (#81016)

---
 homeassistant/components/accuweather/sensor.py | 12 ++++++------
 tests/components/accuweather/test_sensor.py    | 12 ++++++------
 2 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py
index 0238026c1a0..78041c5309c 100644
--- a/homeassistant/components/accuweather/sensor.py
+++ b/homeassistant/components/accuweather/sensor.py
@@ -190,7 +190,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
     ),
     AccuWeatherSensorDescription(
         key="WindGustDay",
-        device_class=SensorDeviceClass.SPEED,
+        device_class=SensorDeviceClass.WIND_SPEED,
         icon="mdi:weather-windy",
         name="Wind gust day",
         entity_registry_enabled_default=False,
@@ -202,7 +202,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
     ),
     AccuWeatherSensorDescription(
         key="WindGustNight",
-        device_class=SensorDeviceClass.SPEED,
+        device_class=SensorDeviceClass.WIND_SPEED,
         icon="mdi:weather-windy",
         name="Wind gust night",
         entity_registry_enabled_default=False,
@@ -214,7 +214,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
     ),
     AccuWeatherSensorDescription(
         key="WindDay",
-        device_class=SensorDeviceClass.SPEED,
+        device_class=SensorDeviceClass.WIND_SPEED,
         icon="mdi:weather-windy",
         name="Wind day",
         unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR
@@ -225,7 +225,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
     ),
     AccuWeatherSensorDescription(
         key="WindNight",
-        device_class=SensorDeviceClass.SPEED,
+        device_class=SensorDeviceClass.WIND_SPEED,
         icon="mdi:weather-windy",
         name="Wind night",
         unit_fn=lambda metric: SPEED_KILOMETERS_PER_HOUR
@@ -335,7 +335,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
     ),
     AccuWeatherSensorDescription(
         key="Wind",
-        device_class=SensorDeviceClass.SPEED,
+        device_class=SensorDeviceClass.WIND_SPEED,
         icon="mdi:weather-windy",
         name="Wind",
         state_class=SensorStateClass.MEASUREMENT,
@@ -346,7 +346,7 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
     ),
     AccuWeatherSensorDescription(
         key="WindGust",
-        device_class=SensorDeviceClass.SPEED,
+        device_class=SensorDeviceClass.WIND_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 aba5c4437ed..55052b5ca3e 100644
--- a/tests/components/accuweather/test_sensor.py
+++ b/tests/components/accuweather/test_sensor.py
@@ -436,7 +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
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
 
     entry = registry.async_get("sensor.home_wind_gust")
     assert entry
@@ -449,7 +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
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
 
     entry = registry.async_get("sensor.home_wind")
     assert entry
@@ -582,7 +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
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
 
     entry = registry.async_get("sensor.home_wind_day_0d")
     assert entry
@@ -596,7 +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
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
 
     entry = registry.async_get("sensor.home_wind_night_0d")
     assert entry
@@ -610,7 +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
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
 
     entry = registry.async_get("sensor.home_wind_gust_day_0d")
     assert entry
@@ -624,7 +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
+    assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED
 
     entry = registry.async_get("sensor.home_wind_gust_night_0d")
     assert entry
-- 
GitLab


From 2d9f39d40631d95824b43c63e4ccad4130cc296b Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:52:34 +0200
Subject: [PATCH 839/985] Strict typing for shared MQTT modules (#80913)

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
---
 homeassistant/components/mqtt/__init__.py     | 10 ++--
 homeassistant/components/mqtt/client.py       | 38 +++++++++++----
 homeassistant/components/mqtt/config_flow.py  | 13 +++--
 homeassistant/components/mqtt/mixins.py       | 10 ++--
 homeassistant/components/mqtt/subscription.py |  4 +-
 homeassistant/components/mqtt/util.py         | 48 +++++++++----------
 6 files changed, 75 insertions(+), 48 deletions(-)

diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 0b46ef0a32b..06921105aae 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -182,7 +182,7 @@ MQTT_PUBLISH_SCHEMA = vol.All(
 
 
 async def _async_setup_discovery(
-    hass: HomeAssistant, conf: ConfigType, config_entry
+    hass: HomeAssistant, conf: ConfigType, config_entry: ConfigEntry
 ) -> None:
     """Try to start the discovery of MQTT devices.
 
@@ -377,7 +377,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         retain: bool = call.data[ATTR_RETAIN]
         if msg_topic_template is not None:
             try:
-                rendered_topic = template.Template(
+                rendered_topic: Any = template.Template(
                     msg_topic_template, hass
                 ).async_render(parse_result=False)
                 msg_topic = valid_publish_topic(rendered_topic)
@@ -620,12 +620,12 @@ def async_subscribe_connection_status(
     """Subscribe to MQTT connection changes."""
     connection_status_callback_job = HassJob(connection_status_callback)
 
-    async def connected():
+    async def connected() -> None:
         task = hass.async_run_hass_job(connection_status_callback_job, True)
         if task:
             await task
 
-    async def disconnected():
+    async def disconnected() -> None:
         task = hass.async_run_hass_job(connection_status_callback_job, False)
         if task:
             await task
@@ -636,7 +636,7 @@ def async_subscribe_connection_status(
     }
 
     @callback
-    def unsubscribe():
+    def unsubscribe() -> None:
         subscriptions["connect"]()
         subscriptions["disconnect"]()
 
diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py
index 041e3cfa374..e909a378581 100644
--- a/homeassistant/components/mqtt/client.py
+++ b/homeassistant/components/mqtt/client.py
@@ -245,7 +245,7 @@ def subscribe(
         async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop
     ).result()
 
-    def remove():
+    def remove() -> None:
         """Remove listener convert."""
         run_callback_threadsafe(hass.loop, async_remove).result()
 
@@ -341,7 +341,7 @@ class MQTT:
         self._ha_started = asyncio.Event()
         self._last_subscribe = time.time()
         self._mqttc: mqtt.Client = None
-        self._cleanup_on_unload: list[Callable] = []
+        self._cleanup_on_unload: list[Callable[[], None]] = []
 
         self._paho_lock = asyncio.Lock()  # Prevents parallel calls to the MQTT client
         self._pending_operations: dict[int, asyncio.Event] = {}
@@ -352,14 +352,14 @@ class MQTT:
         else:
 
             @callback
-            def ha_started(_):
+            def ha_started(_: Event) -> None:
                 self._ha_started.set()
 
             self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started)
 
         self.init_client()
 
-        async def async_stop_mqtt(_event: Event):
+        async def async_stop_mqtt(_event: Event) -> None:
             """Stop MQTT component."""
             await self.async_disconnect()
 
@@ -506,9 +506,11 @@ class MQTT:
 
         def _client_unsubscribe(topic: str) -> int:
             result: int | None = None
+            mid: int | None = None
             result, mid = self._mqttc.unsubscribe(topic)
             _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid)
             _raise_on_error(result)
+            assert mid
             return mid
 
         if any(other.topic == topic for other in self.subscriptions):
@@ -553,7 +555,13 @@ class MQTT:
         if errors:
             _raise_on_errors(errors)
 
-    def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None:
+    def _mqtt_on_connect(
+        self,
+        _mqttc: mqtt.Client,
+        _userdata: None,
+        _flags: dict[str, Any],
+        result_code: int,
+    ) -> None:
         """On connect callback.
 
         Resubscribe to all topics we were subscribed to and publish birth
@@ -596,7 +604,7 @@ class MQTT:
             and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE]
         ):
 
-            async def publish_birth_message(birth_message):
+            async def publish_birth_message(birth_message: PublishMessage) -> None:
                 await self._ha_started.wait()  # Wait for Home Assistant to start
                 await self._discovery_cooldown()  # Wait for MQTT discovery to cool down
                 await self.async_publish(
@@ -611,7 +619,9 @@ class MQTT:
                 publish_birth_message(birth_message), self.hass.loop
             )
 
-    def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None:
+    def _mqtt_on_message(
+        self, _mqttc: mqtt.Client, _userdata: None, msg: MQTTMessage
+    ) -> None:
         """Message received callback."""
         self.hass.add_job(self._mqtt_handle_message, msg)
 
@@ -663,7 +673,13 @@ class MQTT:
             )
         self._mqtt_data.state_write_requests.process_write_state_requests()
 
-    def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None:
+    def _mqtt_on_callback(
+        self,
+        _mqttc: mqtt.Client,
+        _userdata: None,
+        mid: int,
+        _granted_qos: tuple[Any, ...] | None = None,
+    ) -> None:
         """Publish / Subscribe / Unsubscribe callback."""
         self.hass.add_job(self._mqtt_handle_mid, mid)
 
@@ -679,7 +695,9 @@ class MQTT:
             if mid not in self._pending_operations:
                 self._pending_operations[mid] = asyncio.Event()
 
-    def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None:
+    def _mqtt_on_disconnect(
+        self, _mqttc: mqtt.Client, _userdata: None, result_code: int
+    ) -> None:
         """Disconnected callback."""
         self.connected = False
         dispatcher_send(self.hass, MQTT_DISCONNECTED)
@@ -707,7 +725,7 @@ class MQTT:
                 del self._pending_operations[mid]
                 self._pending_operations_condition.notify_all()
 
-    async def _discovery_cooldown(self):
+    async def _discovery_cooldown(self) -> None:
         now = time.time()
         # Reset discovery and subscribe cooldowns
         self._mqtt_data.last_discovery = now
diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py
index d94a2648918..ec818348701 100644
--- a/homeassistant/components/mqtt/config_flow.py
+++ b/homeassistant/components/mqtt/config_flow.py
@@ -287,7 +287,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
         options_config: dict[str, Any] = {}
         bad_input: bool = False
 
-        def _birth_will(birt_or_will: str) -> dict:
+        def _birth_will(birt_or_will: str) -> dict[str, Any]:
             """Return the user input for birth or will."""
             assert user_input
             return {
@@ -298,8 +298,11 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
             }
 
         def _validate(
-            field: str, values: dict[str, Any], error_code: str, schema: Callable
-        ):
+            field: str,
+            values: dict[str, Any],
+            error_code: str,
+            schema: Callable[[Any], Any],
+        ) -> None:
             """Validate the user input."""
             nonlocal bad_input
             try:
@@ -679,7 +682,9 @@ def try_connection(
 
     result: queue.Queue[bool] = queue.Queue(maxsize=1)
 
-    def on_connect(client_, userdata, flags, result_code):
+    def on_connect(
+        client_: mqtt.Client, userdata: None, flags: dict[str, Any], result_code: int
+    ) -> None:
         """Handle connection result."""
         result.put(result_code == mqtt.CONNACK_ACCEPTED)
 
diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py
index 7866e3cf6d6..a91a7fc7a88 100644
--- a/homeassistant/components/mqtt/mixins.py
+++ b/homeassistant/components/mqtt/mixins.py
@@ -287,6 +287,7 @@ async def async_get_platform_config_from_yaml(
     config_yaml: ConfigType | None = None,
 ) -> list[ConfigType]:
     """Return a list of validated configurations for the domain."""
+    platform_configs: Any | None
     mqtt_data = get_mqtt_data(hass)
     if config_yaml is None:
         config_yaml = mqtt_data.config
@@ -294,6 +295,7 @@ async def async_get_platform_config_from_yaml(
         return []
     if not (platform_configs := config_yaml.get(platform_domain)):
         return []
+    assert isinstance(platform_configs, list)
     return platform_configs
 
 
@@ -662,7 +664,9 @@ def stop_discovery_updates(
     clear_discovery_hash(hass, discovery_hash)
 
 
-async def async_remove_discovery_payload(hass: HomeAssistant, discovery_data: dict):
+async def async_remove_discovery_payload(
+    hass: HomeAssistant, discovery_data: DiscoveryInfoType
+) -> None:
     """Clear retained discovery topic in broker to avoid rediscovery after a restart of HA."""
     discovery_topic = discovery_data[ATTR_DISCOVERY_TOPIC]
     await async_publish(hass, discovery_topic, "", retain=True)
@@ -820,7 +824,7 @@ class MqttDiscoveryUpdate(Entity):
         """Initialize the discovery update mixin."""
         self._discovery_data = discovery_data
         self._discovery_update = discovery_update
-        self._remove_discovery_updated: Callable | None = None
+        self._remove_discovery_updated: Callable[[], None] | None = None
         self._removed_from_hass = False
         if discovery_data is None:
             return
@@ -1169,7 +1173,7 @@ def update_device(
     device_info = device_info_from_specifications(config[CONF_DEVICE])
 
     if config_entry_id is not None and device_info is not None:
-        update_device_info = cast(dict, device_info)
+        update_device_info = cast(dict[str, Any], device_info)
         update_device_info["config_entry_id"] = config_entry_id
         device = device_registry.async_get_or_create(**update_device_info)
 
diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py
index 05f7f3934ee..87f5d3882bb 100644
--- a/homeassistant/components/mqtt/subscription.py
+++ b/homeassistant/components/mqtt/subscription.py
@@ -21,7 +21,7 @@ class EntitySubscription:
     hass: HomeAssistant = attr.ib()
     topic: str = attr.ib()
     message_callback: MessageCallbackType = attr.ib()
-    subscribe_task: Coroutine | None = attr.ib()
+    subscribe_task: Coroutine[Any, Any, Callable[[], None]] | None = attr.ib()
     unsubscribe_callback: Callable[[], None] | None = attr.ib()
     qos: int = attr.ib(default=0)
     encoding: str = attr.ib(default="utf-8")
@@ -53,7 +53,7 @@ class EntitySubscription:
             hass, self.topic, self.message_callback, self.qos, self.encoding
         )
 
-    async def subscribe(self):
+    async def subscribe(self) -> None:
         """Subscribe to a topic."""
         if not self.subscribe_task:
             return
diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py
index 7e23b6c01f1..0b2d10977aa 100644
--- a/homeassistant/components/mqtt/util.py
+++ b/homeassistant/components/mqtt/util.py
@@ -40,58 +40,58 @@ def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None:
     return not bool(hass.config_entries.async_entries(DOMAIN)[0].disabled_by)
 
 
-def valid_topic(value: Any) -> str:
+def valid_topic(topic: Any) -> str:
     """Validate that this is a valid topic name/filter."""
-    value = cv.string(value)
+    validated_topic = cv.string(topic)
     try:
-        raw_value = value.encode("utf-8")
+        raw_validated_topic = validated_topic.encode("utf-8")
     except UnicodeError as err:
         raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") from err
-    if not raw_value:
+    if not raw_validated_topic:
         raise vol.Invalid("MQTT topic name/filter must not be empty.")
-    if len(raw_value) > 65535:
+    if len(raw_validated_topic) > 65535:
         raise vol.Invalid(
             "MQTT topic name/filter must not be longer than 65535 encoded bytes."
         )
-    if "\0" in value:
+    if "\0" in validated_topic:
         raise vol.Invalid("MQTT topic name/filter must not contain null character.")
-    if any(char <= "\u001F" for char in value):
+    if any(char <= "\u001F" for char in validated_topic):
         raise vol.Invalid("MQTT topic name/filter must not contain control characters.")
-    if any("\u007f" <= char <= "\u009F" for char in value):
+    if any("\u007f" <= char <= "\u009F" for char in validated_topic):
         raise vol.Invalid("MQTT topic name/filter must not contain control characters.")
-    if any("\ufdd0" <= char <= "\ufdef" for char in value):
+    if any("\ufdd0" <= char <= "\ufdef" for char in validated_topic):
         raise vol.Invalid("MQTT topic name/filter must not contain non-characters.")
-    if any((ord(char) & 0xFFFF) in (0xFFFE, 0xFFFF) for char in value):
+    if any((ord(char) & 0xFFFF) in (0xFFFE, 0xFFFF) for char in validated_topic):
         raise vol.Invalid("MQTT topic name/filter must not contain noncharacters.")
 
-    return value
+    return validated_topic
 
 
-def valid_subscribe_topic(value: Any) -> str:
+def valid_subscribe_topic(topic: Any) -> str:
     """Validate that we can subscribe using this MQTT topic."""
-    value = valid_topic(value)
-    for i in (i for i, c in enumerate(value) if c == "+"):
-        if (i > 0 and value[i - 1] != "/") or (
-            i < len(value) - 1 and value[i + 1] != "/"
+    validated_topic = valid_topic(topic)
+    for i in (i for i, c in enumerate(validated_topic) if c == "+"):
+        if (i > 0 and validated_topic[i - 1] != "/") or (
+            i < len(validated_topic) - 1 and validated_topic[i + 1] != "/"
         ):
             raise vol.Invalid(
                 "Single-level wildcard must occupy an entire level of the filter"
             )
 
-    index = value.find("#")
+    index = validated_topic.find("#")
     if index != -1:
-        if index != len(value) - 1:
+        if index != len(validated_topic) - 1:
             # If there are multiple wildcards, this will also trigger
             raise vol.Invalid(
                 "Multi-level wildcard must be the last "
                 "character in the topic filter."
             )
-        if len(value) > 1 and value[index - 1] != "/":
+        if len(validated_topic) > 1 and validated_topic[index - 1] != "/":
             raise vol.Invalid(
                 "Multi-level wildcard must be after a topic level separator."
             )
 
-    return value
+    return validated_topic
 
 
 def valid_subscribe_topic_template(value: Any) -> template.Template:
@@ -104,12 +104,12 @@ def valid_subscribe_topic_template(value: Any) -> template.Template:
     return tpl
 
 
-def valid_publish_topic(value: Any) -> str:
+def valid_publish_topic(topic: Any) -> str:
     """Validate that we can publish using this MQTT topic."""
-    value = valid_topic(value)
-    if "+" in value or "#" in value:
+    validated_topic = valid_topic(topic)
+    if "+" in validated_topic or "#" in validated_topic:
         raise vol.Invalid("Wildcards can not be used in topic names")
-    return value
+    return validated_topic
 
 
 _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
-- 
GitLab


From 458f3d4d13823093aa91a939de7fa42ab859d989 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
Date: Wed, 26 Oct 2022 13:53:41 +0200
Subject: [PATCH 840/985] Use a unique cache folder per Velbus config entry
 (#79792)

---
 homeassistant/components/velbus/__init__.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py
index 67a4652d5e5..66d9fef71a3 100644
--- a/homeassistant/components/velbus/__init__.py
+++ b/homeassistant/components/velbus/__init__.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import logging
+import shutil
 
 from velbusaio.controller import Velbus
 import voluptuous as vol
@@ -12,6 +13,7 @@ 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.storage import STORAGE_DIR
 
 from .const import (
     CONF_INTERFACE,
@@ -64,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
     controller = Velbus(
         entry.data[CONF_PORT],
-        cache_dir=hass.config.path(".storage/velbuscache/"),
+        cache_dir=hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
     )
     hass.data[DOMAIN][entry.entry_id] = {}
     hass.data[DOMAIN][entry.entry_id]["cntrl"] = controller
@@ -138,6 +140,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
     await hass.data[DOMAIN][entry.entry_id]["cntrl"].stop()
     hass.data[DOMAIN].pop(entry.entry_id)
+    await hass.async_add_executor_job(
+        shutil.rmtree,
+        hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
+    )
     if not hass.data[DOMAIN]:
         hass.data.pop(DOMAIN)
         hass.services.async_remove(DOMAIN, SERVICE_SCAN)
-- 
GitLab


From 9d3442055ba6ce91ada6adcb7d7d07e51661a41b Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:54:57 +0200
Subject: [PATCH 841/985] Move options to SelectEntityDescription in goodwe
 (#80017)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
---
 homeassistant/components/goodwe/select.py | 20 +++++++++-----------
 1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py
index c8fa44b7e26..32472453c9c 100644
--- a/homeassistant/components/goodwe/select.py
+++ b/homeassistant/components/goodwe/select.py
@@ -14,18 +14,17 @@ from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER
 _LOGGER = logging.getLogger(__name__)
 
 
-INVERTER_OPERATION_MODES = [
-    "General mode",
-    "Off grid mode",
-    "Backup mode",
-    "Eco mode",
-]
-
 OPERATION_MODE = SelectEntityDescription(
     key="operation_mode",
     name="Inverter operation mode",
     icon="mdi:solar-power",
     entity_category=EntityCategory.CONFIG,
+    options=[
+        "General mode",
+        "Off grid mode",
+        "Backup mode",
+        "Eco mode",
+    ],
 )
 
 
@@ -45,14 +44,14 @@ async def async_setup_entry(
         # Inverter model does not support this setting
         _LOGGER.debug("Could not read inverter operation mode")
     else:
-        if 0 <= active_mode < len(INVERTER_OPERATION_MODES):
+        if (options := OPERATION_MODE.options) and 0 <= active_mode < len(options):
             async_add_entities(
                 [
                     InverterOperationModeEntity(
                         device_info,
                         OPERATION_MODE,
                         inverter,
-                        INVERTER_OPERATION_MODES[active_mode],
+                        options[active_mode],
                     )
                 ]
             )
@@ -74,12 +73,11 @@ class InverterOperationModeEntity(SelectEntity):
         self.entity_description = description
         self._attr_unique_id = f"{DOMAIN}-{description.key}-{inverter.serial_number}"
         self._attr_device_info = device_info
-        self._attr_options = INVERTER_OPERATION_MODES
         self._attr_current_option = current_mode
         self._inverter: Inverter = inverter
 
     async def async_select_option(self, option: str) -> None:
         """Change the selected option."""
-        await self._inverter.set_operation_mode(INVERTER_OPERATION_MODES.index(option))
+        await self._inverter.set_operation_mode(self.options.index(option))
         self._attr_current_option = option
         self.async_write_ha_state()
-- 
GitLab


From 842cb18d3948709fcb1579535d38cfc28356dfda Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:56:51 +0200
Subject: [PATCH 842/985] Migrate energy units to an enum (#80998)

---
 homeassistant/components/energy/sensor.py     | 25 ++++++-------
 homeassistant/components/energy/validate.py   | 21 +++++------
 .../components/energy/websocket_api.py        |  4 +--
 homeassistant/const.py                        | 14 +++++++-
 homeassistant/util/unit_conversion.py         | 23 ++++++------
 tests/components/energy/test_sensor.py        | 35 +++++++++----------
 tests/components/energy/test_validate.py      | 21 +++++------
 tests/util/test_unit_conversion.py            | 35 +++++++++----------
 8 files changed, 85 insertions(+), 93 deletions(-)

diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py
index c60c5c11519..540642a89da 100644
--- a/homeassistant/components/energy/sensor.py
+++ b/homeassistant/components/energy/sensor.py
@@ -17,12 +17,9 @@ from homeassistant.components.sensor import (
 from homeassistant.components.sensor.recorder import reset_detected
 from homeassistant.const import (
     ATTR_UNIT_OF_MEASUREMENT,
-    ENERGY_GIGA_JOULE,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_MEGA_WATT_HOUR,
-    ENERGY_WATT_HOUR,
     VOLUME_CUBIC_FEET,
     VOLUME_CUBIC_METERS,
+    UnitOfEnergy,
 )
 from homeassistant.core import (
     HomeAssistant,
@@ -46,10 +43,10 @@ SUPPORTED_STATE_CLASSES = [
     SensorStateClass.TOTAL_INCREASING,
 ]
 VALID_ENERGY_UNITS = [
-    ENERGY_WATT_HOUR,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_MEGA_WATT_HOUR,
-    ENERGY_GIGA_JOULE,
+    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
 _LOGGER = logging.getLogger(__name__)
@@ -286,17 +283,17 @@ class EnergyCostSensor(SensorEntity):
                 return
 
             if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
-                f"/{ENERGY_WATT_HOUR}"
+                f"/{UnitOfEnergy.WATT_HOUR}"
             ):
                 energy_price *= 1000.0
 
             if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
-                f"/{ENERGY_MEGA_WATT_HOUR}"
+                f"/{UnitOfEnergy.MEGA_WATT_HOUR}"
             ):
                 energy_price /= 1000.0
 
             if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
-                f"/{ENERGY_GIGA_JOULE}"
+                f"/{UnitOfEnergy.GIGA_JOULE}"
             ):
                 energy_price /= 1000 / 3.6
 
@@ -319,11 +316,11 @@ class EnergyCostSensor(SensorEntity):
             if energy_unit not in VALID_ENERGY_UNITS_GAS:
                 energy_unit = None
 
-        if energy_unit == ENERGY_WATT_HOUR:
+        if energy_unit == UnitOfEnergy.WATT_HOUR:
             energy_price /= 1000
-        elif energy_unit == ENERGY_MEGA_WATT_HOUR:
+        elif energy_unit == UnitOfEnergy.MEGA_WATT_HOUR:
             energy_price *= 1000
-        elif energy_unit == ENERGY_GIGA_JOULE:
+        elif energy_unit == UnitOfEnergy.GIGA_JOULE:
             energy_price *= 1000 / 3.6
 
         if energy_unit is None:
diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py
index 17ac7227bf8..f31eb53fb37 100644
--- a/homeassistant/components/energy/validate.py
+++ b/homeassistant/components/energy/validate.py
@@ -9,14 +9,11 @@ from typing import Any
 from homeassistant.components import recorder, sensor
 from homeassistant.const import (
     ATTR_DEVICE_CLASS,
-    ENERGY_GIGA_JOULE,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_MEGA_WATT_HOUR,
-    ENERGY_WATT_HOUR,
     STATE_UNAVAILABLE,
     STATE_UNKNOWN,
     VOLUME_CUBIC_FEET,
     VOLUME_CUBIC_METERS,
+    UnitOfEnergy,
 )
 from homeassistant.core import HomeAssistant, callback, valid_entity_id
 
@@ -26,10 +23,10 @@ from .const import DOMAIN
 ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
 ENERGY_USAGE_UNITS = {
     sensor.SensorDeviceClass.ENERGY: (
-        ENERGY_KILO_WATT_HOUR,
-        ENERGY_MEGA_WATT_HOUR,
-        ENERGY_WATT_HOUR,
-        ENERGY_GIGA_JOULE,
+        UnitOfEnergy.KILO_WATT_HOUR,
+        UnitOfEnergy.MEGA_WATT_HOUR,
+        UnitOfEnergy.WATT_HOUR,
+        UnitOfEnergy.GIGA_JOULE,
     )
 }
 ENERGY_PRICE_UNITS = tuple(
@@ -43,10 +40,10 @@ GAS_USAGE_DEVICE_CLASSES = (
 )
 GAS_USAGE_UNITS = {
     sensor.SensorDeviceClass.ENERGY: (
-        ENERGY_WATT_HOUR,
-        ENERGY_KILO_WATT_HOUR,
-        ENERGY_MEGA_WATT_HOUR,
-        ENERGY_GIGA_JOULE,
+        UnitOfEnergy.WATT_HOUR,
+        UnitOfEnergy.KILO_WATT_HOUR,
+        UnitOfEnergy.MEGA_WATT_HOUR,
+        UnitOfEnergy.GIGA_JOULE,
     ),
     sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
 }
diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py
index 8fa35e607b2..c2b693c0809 100644
--- a/homeassistant/components/energy/websocket_api.py
+++ b/homeassistant/components/energy/websocket_api.py
@@ -13,7 +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.const import UnitOfEnergy
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.helpers.integration_platform import (
     async_process_integration_platforms,
@@ -273,7 +273,7 @@ async def ws_get_fossil_energy_consumption(
         statistic_ids,
         "hour",
         True,
-        {"energy": ENERGY_KILO_WATT_HOUR},
+        {"energy": UnitOfEnergy.KILO_WATT_HOUR},
     )
 
     def _combine_sum_statistics(
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 1bac1c10c5a..096bdf1df99 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -486,11 +486,23 @@ POWER_BTU_PER_HOUR: Final = "BTU/h"
 # Reactive power units
 POWER_VOLT_AMPERE_REACTIVE: Final = "var"
 
+
 # Energy units
-ENERGY_GIGA_JOULE: Final = "GJ"
+class UnitOfEnergy(StrEnum):
+    """Energy units."""
+
+    GIGA_JOULE = "GJ"
+    KILO_WATT_HOUR = "kWh"
+    MEGA_WATT_HOUR = "MWh"
+    WATT_HOUR = "Wh"
+
+
 ENERGY_KILO_WATT_HOUR: Final = "kWh"
+"""Deprecated: please use UnitOfEnergy.KILO_WATT_HOUR."""
 ENERGY_MEGA_WATT_HOUR: Final = "MWh"
+"""Deprecated: please use UnitOfEnergy.MEGA_WATT_HOUR."""
 ENERGY_WATT_HOUR: Final = "Wh"
+"""Deprecated: please use UnitOfEnergy.WATT_HOUR."""
 
 # Electric_current units
 ELECTRIC_CURRENT_MILLIAMPERE: Final = "mA"
diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py
index cd27f79b045..1cf00230d1c 100644
--- a/homeassistant/util/unit_conversion.py
+++ b/homeassistant/util/unit_conversion.py
@@ -2,10 +2,6 @@
 from __future__ import annotations
 
 from homeassistant.const import (
-    ENERGY_GIGA_JOULE,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_MEGA_WATT_HOUR,
-    ENERGY_WATT_HOUR,
     LENGTH_CENTIMETERS,
     LENGTH_FEET,
     LENGTH_INCHES,
@@ -46,6 +42,7 @@ from homeassistant.const import (
     VOLUME_GALLONS,
     VOLUME_LITERS,
     VOLUME_MILLILITERS,
+    UnitOfEnergy,
     UnitOfVolumetricFlux,
 )
 from homeassistant.exceptions import HomeAssistantError
@@ -151,18 +148,18 @@ class EnergyConverter(BaseUnitConverter):
     """Utility to convert energy values."""
 
     UNIT_CLASS = "energy"
-    NORMALIZED_UNIT = ENERGY_KILO_WATT_HOUR
+    NORMALIZED_UNIT = UnitOfEnergy.KILO_WATT_HOUR
     _UNIT_CONVERSION: dict[str, float] = {
-        ENERGY_WATT_HOUR: 1 * 1000,
-        ENERGY_KILO_WATT_HOUR: 1,
-        ENERGY_MEGA_WATT_HOUR: 1 / 1000,
-        ENERGY_GIGA_JOULE: 3.6 / 1000,
+        UnitOfEnergy.WATT_HOUR: 1 * 1000,
+        UnitOfEnergy.KILO_WATT_HOUR: 1,
+        UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000,
+        UnitOfEnergy.GIGA_JOULE: 3.6 / 1000,
     }
     VALID_UNITS = {
-        ENERGY_WATT_HOUR,
-        ENERGY_KILO_WATT_HOUR,
-        ENERGY_MEGA_WATT_HOUR,
-        ENERGY_GIGA_JOULE,
+        UnitOfEnergy.WATT_HOUR,
+        UnitOfEnergy.KILO_WATT_HOUR,
+        UnitOfEnergy.MEGA_WATT_HOUR,
+        UnitOfEnergy.GIGA_JOULE,
     }
 
 
diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py
index 20dc533e70f..3b954e8d62b 100644
--- a/tests/components/energy/test_sensor.py
+++ b/tests/components/energy/test_sensor.py
@@ -16,13 +16,10 @@ from homeassistant.components.sensor.recorder import compile_statistics
 from homeassistant.const import (
     ATTR_DEVICE_CLASS,
     ATTR_UNIT_OF_MEASUREMENT,
-    ENERGY_GIGA_JOULE,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_MEGA_WATT_HOUR,
-    ENERGY_WATT_HOUR,
     STATE_UNKNOWN,
     VOLUME_CUBIC_FEET,
     VOLUME_CUBIC_METERS,
+    UnitOfEnergy,
 )
 from homeassistant.helpers import entity_registry as er
 from homeassistant.setup import async_setup_component
@@ -143,7 +140,7 @@ async def test_cost_sensor_price_entity_total_increasing(
         return compile_statistics(hass, now, now + timedelta(seconds=1)).platform_stats
 
     energy_attributes = {
-        ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+        ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
         ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
     }
 
@@ -347,7 +344,7 @@ async def test_cost_sensor_price_entity_total(
         return compile_statistics(hass, now, now + timedelta(seconds=1)).platform_stats
 
     energy_attributes = {
-        ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+        ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
         ATTR_STATE_CLASS: energy_state_class,
     }
 
@@ -553,7 +550,7 @@ async def test_cost_sensor_price_entity_total_no_reset(
         return compile_statistics(hass, now, now + timedelta(seconds=1)).platform_stats
 
     energy_attributes = {
-        ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+        ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
         ATTR_STATE_CLASS: energy_state_class,
     }
 
@@ -701,10 +698,10 @@ async def test_cost_sensor_price_entity_total_no_reset(
 @pytest.mark.parametrize(
     "energy_unit,factor",
     [
-        (ENERGY_WATT_HOUR, 1000),
-        (ENERGY_KILO_WATT_HOUR, 1),
-        (ENERGY_MEGA_WATT_HOUR, 0.001),
-        (ENERGY_GIGA_JOULE, 0.001 * 3.6),
+        (UnitOfEnergy.WATT_HOUR, 1000),
+        (UnitOfEnergy.KILO_WATT_HOUR, 1),
+        (UnitOfEnergy.MEGA_WATT_HOUR, 0.001),
+        (UnitOfEnergy.GIGA_JOULE, 0.001 * 3.6),
     ],
 )
 async def test_cost_sensor_handle_energy_units(
@@ -767,10 +764,10 @@ async def test_cost_sensor_handle_energy_units(
 @pytest.mark.parametrize(
     "price_unit,factor",
     [
-        (f"EUR/{ENERGY_WATT_HOUR}", 0.001),
-        (f"EUR/{ENERGY_KILO_WATT_HOUR}", 1),
-        (f"EUR/{ENERGY_MEGA_WATT_HOUR}", 1000),
-        (f"EUR/{ENERGY_GIGA_JOULE}", 1000 / 3.6),
+        (f"EUR/{UnitOfEnergy.WATT_HOUR}", 0.001),
+        (f"EUR/{UnitOfEnergy.KILO_WATT_HOUR}", 1),
+        (f"EUR/{UnitOfEnergy.MEGA_WATT_HOUR}", 1000),
+        (f"EUR/{UnitOfEnergy.GIGA_JOULE}", 1000 / 3.6),
     ],
 )
 async def test_cost_sensor_handle_price_units(
@@ -778,7 +775,7 @@ async def test_cost_sensor_handle_price_units(
 ) -> None:
     """Test energy cost price from sensor entity."""
     energy_attributes = {
-        ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+        ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
         ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
     }
     price_attributes = {
@@ -891,7 +888,7 @@ async def test_cost_sensor_handle_gas_kwh(
 ) -> None:
     """Test gas cost price from sensor entity."""
     energy_attributes = {
-        ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+        ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
         ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
     }
     energy_data = data.EnergyManager.default_preferences()
@@ -942,7 +939,7 @@ async def test_cost_sensor_wrong_state_class(
 ) -> None:
     """Test energy sensor rejects sensor with wrong state_class."""
     energy_attributes = {
-        ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+        ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
         ATTR_STATE_CLASS: state_class,
     }
     energy_data = data.EnergyManager.default_preferences()
@@ -1003,7 +1000,7 @@ async def test_cost_sensor_state_class_measurement_no_reset(
 ) -> None:
     """Test energy sensor rejects state_class measurement with no last_reset."""
     energy_attributes = {
-        ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
+        ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
         ATTR_STATE_CLASS: state_class,
     }
     energy_data = data.EnergyManager.default_preferences()
diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py
index 729e7685ac5..c8aa4299484 100644
--- a/tests/components/energy/test_validate.py
+++ b/tests/components/energy/test_validate.py
@@ -4,12 +4,7 @@ from unittest.mock import patch
 import pytest
 
 from homeassistant.components.energy import async_get_manager, validate
-from homeassistant.const import (
-    ENERGY_GIGA_JOULE,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_MEGA_WATT_HOUR,
-    ENERGY_WATT_HOUR,
-)
+from homeassistant.const import UnitOfEnergy
 from homeassistant.helpers.json import JSON_DUMP
 from homeassistant.setup import async_setup_component
 
@@ -68,13 +63,13 @@ async def test_validation_empty_config(hass):
 @pytest.mark.parametrize(
     "state_class, energy_unit, extra",
     [
-        ("total_increasing", ENERGY_KILO_WATT_HOUR, {}),
-        ("total_increasing", ENERGY_MEGA_WATT_HOUR, {}),
-        ("total_increasing", ENERGY_WATT_HOUR, {}),
-        ("total", ENERGY_KILO_WATT_HOUR, {}),
-        ("total", ENERGY_KILO_WATT_HOUR, {"last_reset": "abc"}),
-        ("measurement", ENERGY_KILO_WATT_HOUR, {"last_reset": "abc"}),
-        ("total_increasing", ENERGY_GIGA_JOULE, {}),
+        ("total_increasing", UnitOfEnergy.KILO_WATT_HOUR, {}),
+        ("total_increasing", UnitOfEnergy.MEGA_WATT_HOUR, {}),
+        ("total_increasing", UnitOfEnergy.WATT_HOUR, {}),
+        ("total", UnitOfEnergy.KILO_WATT_HOUR, {}),
+        ("total", UnitOfEnergy.KILO_WATT_HOUR, {"last_reset": "abc"}),
+        ("measurement", UnitOfEnergy.KILO_WATT_HOUR, {"last_reset": "abc"}),
+        ("total_increasing", UnitOfEnergy.GIGA_JOULE, {}),
     ],
 )
 async def test_validation(
diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py
index 454f0708aac..1b43af09aec 100644
--- a/tests/util/test_unit_conversion.py
+++ b/tests/util/test_unit_conversion.py
@@ -2,10 +2,6 @@
 import pytest
 
 from homeassistant.const import (
-    ENERGY_GIGA_JOULE,
-    ENERGY_KILO_WATT_HOUR,
-    ENERGY_MEGA_WATT_HOUR,
-    ENERGY_WATT_HOUR,
     LENGTH_CENTIMETERS,
     LENGTH_FEET,
     LENGTH_INCHES,
@@ -44,6 +40,7 @@ from homeassistant.const import (
     VOLUME_GALLONS,
     VOLUME_LITERS,
     VOLUME_MILLILITERS,
+    UnitOfEnergy,
     UnitOfVolumetricFlux,
 )
 from homeassistant.exceptions import HomeAssistantError
@@ -73,10 +70,10 @@ INVALID_SYMBOL = "bob"
         (DistanceConverter, LENGTH_YARD),
         (DistanceConverter, LENGTH_FEET),
         (DistanceConverter, LENGTH_INCHES),
-        (EnergyConverter, ENERGY_WATT_HOUR),
-        (EnergyConverter, ENERGY_KILO_WATT_HOUR),
-        (EnergyConverter, ENERGY_MEGA_WATT_HOUR),
-        (EnergyConverter, ENERGY_GIGA_JOULE),
+        (EnergyConverter, UnitOfEnergy.WATT_HOUR),
+        (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
+        (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR),
+        (EnergyConverter, UnitOfEnergy.GIGA_JOULE),
         (MassConverter, MASS_GRAMS),
         (MassConverter, MASS_KILOGRAMS),
         (MassConverter, MASS_MICROGRAMS),
@@ -120,7 +117,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str)
     "converter,valid_unit",
     [
         (DistanceConverter, LENGTH_KILOMETERS),
-        (EnergyConverter, ENERGY_KILO_WATT_HOUR),
+        (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
         (MassConverter, MASS_GRAMS),
         (PowerConverter, POWER_WATT),
         (PressureConverter, PRESSURE_PA),
@@ -146,7 +143,7 @@ def test_convert_invalid_unit(
     "converter,from_unit,to_unit",
     [
         (DistanceConverter, LENGTH_KILOMETERS, LENGTH_METERS),
-        (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR),
+        (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR),
         (MassConverter, MASS_GRAMS, MASS_KILOGRAMS),
         (PowerConverter, POWER_WATT, POWER_KILO_WATT),
         (PressureConverter, PRESSURE_HPA, PRESSURE_INHG),
@@ -167,7 +164,7 @@ def test_convert_nonnumeric_value(
     "converter,from_unit,to_unit,expected",
     [
         (DistanceConverter, LENGTH_KILOMETERS, LENGTH_METERS, 1 / 1000),
-        (EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, 1000),
+        (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000),
         (PowerConverter, POWER_WATT, POWER_KILO_WATT, 1000),
         (PressureConverter, PRESSURE_HPA, PRESSURE_INHG, pytest.approx(33.86389)),
         (
@@ -261,14 +258,14 @@ def test_distance_convert(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (10, ENERGY_WATT_HOUR, 0.01, ENERGY_KILO_WATT_HOUR),
-        (10, ENERGY_WATT_HOUR, 0.00001, ENERGY_MEGA_WATT_HOUR),
-        (10, ENERGY_KILO_WATT_HOUR, 10000, ENERGY_WATT_HOUR),
-        (10, ENERGY_KILO_WATT_HOUR, 0.01, ENERGY_MEGA_WATT_HOUR),
-        (10, ENERGY_MEGA_WATT_HOUR, 10000000, ENERGY_WATT_HOUR),
-        (10, ENERGY_MEGA_WATT_HOUR, 10000, ENERGY_KILO_WATT_HOUR),
-        (10, ENERGY_GIGA_JOULE, 10000 / 3.6, ENERGY_KILO_WATT_HOUR),
-        (10, ENERGY_GIGA_JOULE, 10 / 3.6, ENERGY_MEGA_WATT_HOUR),
+        (10, UnitOfEnergy.WATT_HOUR, 0.01, UnitOfEnergy.KILO_WATT_HOUR),
+        (10, UnitOfEnergy.WATT_HOUR, 0.00001, UnitOfEnergy.MEGA_WATT_HOUR),
+        (10, UnitOfEnergy.KILO_WATT_HOUR, 10000, UnitOfEnergy.WATT_HOUR),
+        (10, UnitOfEnergy.KILO_WATT_HOUR, 0.01, UnitOfEnergy.MEGA_WATT_HOUR),
+        (10, UnitOfEnergy.MEGA_WATT_HOUR, 10000000, UnitOfEnergy.WATT_HOUR),
+        (10, UnitOfEnergy.MEGA_WATT_HOUR, 10000, UnitOfEnergy.KILO_WATT_HOUR),
+        (10, UnitOfEnergy.GIGA_JOULE, 10000 / 3.6, UnitOfEnergy.KILO_WATT_HOUR),
+        (10, UnitOfEnergy.GIGA_JOULE, 10 / 3.6, UnitOfEnergy.MEGA_WATT_HOUR),
     ],
 )
 def test_energy_convert(
-- 
GitLab


From 91df68de1fa595af818e312d36f6e5f92b47fe82 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:57:49 +0200
Subject: [PATCH 843/985] Migrate length units to an enum (#81011)

---
 homeassistant/const.py | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 096bdf1df99..9fd17fa53e7 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -536,16 +536,37 @@ TIME_WEEKS: Final = "w"
 TIME_MONTHS: Final = "m"
 TIME_YEARS: Final = "y"
 
+
 # Length units
+class UnitOfLength(StrEnum):
+    """Length units."""
+
+    MILLIMETERS = "mm"
+    CENTIMETERS = "cm"
+    METERS = "m"
+    KILOMETERS = "km"
+    INCHES = "in"
+    FEET = "ft"
+    YARDS = "yd"
+    MILES = "mi"
+
+
 LENGTH_MILLIMETERS: Final = "mm"
+"""Deprecated: please use UnitOfLength.MILLIMETERS."""
 LENGTH_CENTIMETERS: Final = "cm"
+"""Deprecated: please use UnitOfLength.CENTIMETERS."""
 LENGTH_METERS: Final = "m"
+"""Deprecated: please use UnitOfLength.METERS."""
 LENGTH_KILOMETERS: Final = "km"
-
+"""Deprecated: please use UnitOfLength.KILOMETERS."""
 LENGTH_INCHES: Final = "in"
+"""Deprecated: please use UnitOfLength.INCHES."""
 LENGTH_FEET: Final = "ft"
+"""Deprecated: please use UnitOfLength.FEET."""
 LENGTH_YARD: Final = "yd"
+"""Deprecated: please use UnitOfLength.YARDS."""
 LENGTH_MILES: Final = "mi"
+"""Deprecated: please use UnitOfLength.MILES."""
 
 # Frequency units
 FREQUENCY_HERTZ: Final = "Hz"
-- 
GitLab


From 945c991e84078b9bd17a77d618b5b323fe510d2c Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:58:22 +0200
Subject: [PATCH 844/985] Migrate pressure units to an enum (#81009)

---
 homeassistant/const.py | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 9fd17fa53e7..4f9eb8a8288 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -574,16 +574,40 @@ FREQUENCY_KILOHERTZ: Final = "kHz"
 FREQUENCY_MEGAHERTZ: Final = "MHz"
 FREQUENCY_GIGAHERTZ: Final = "GHz"
 
+
 # Pressure units
+class UnitOfPressure(StrEnum):
+    """Pressure units."""
+
+    PA = "Pa"
+    HPA = "hPa"
+    KPA = "kPa"
+    BAR = "bar"
+    CBAR = "cbar"
+    MBAR = "mbar"
+    MMHG = "mmHg"
+    INHG = "inHg"
+    PSI = "psi"
+
+
 PRESSURE_PA: Final = "Pa"
+"""Deprecated: please use UnitOfPressure.PA"""
 PRESSURE_HPA: Final = "hPa"
+"""Deprecated: please use UnitOfPressure.HPA"""
 PRESSURE_KPA: Final = "kPa"
+"""Deprecated: please use UnitOfPressure.KPA"""
 PRESSURE_BAR: Final = "bar"
+"""Deprecated: please use UnitOfPressure.BAR"""
 PRESSURE_CBAR: Final = "cbar"
+"""Deprecated: please use UnitOfPressure.CBAR"""
 PRESSURE_MBAR: Final = "mbar"
+"""Deprecated: please use UnitOfPressure.MBAR"""
 PRESSURE_MMHG: Final = "mmHg"
+"""Deprecated: please use UnitOfPressure.MMHG"""
 PRESSURE_INHG: Final = "inHg"
+"""Deprecated: please use UnitOfPressure.INHG"""
 PRESSURE_PSI: Final = "psi"
+"""Deprecated: please use UnitOfPressure.PSI"""
 
 # Sound pressure units
 SOUND_PRESSURE_DB: Final = "dB"
-- 
GitLab


From 9ee4d77935901dd4292458794c0bc4716f5eb8c2 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 13:59:54 +0200
Subject: [PATCH 845/985] Migrate speed units to an enum (#81004)

---
 homeassistant/const.py | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 4f9eb8a8288..78188533cd7 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -701,12 +701,28 @@ CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³"
 CONCENTRATION_PARTS_PER_MILLION: Final = "ppm"
 CONCENTRATION_PARTS_PER_BILLION: Final = "ppb"
 
+
 # Speed units
+class UnitOfSpeed(StrEnum):
+    """Speed units."""
+
+    FEET_PER_SECOND = "ft/s"
+    METERS_PER_SECOND = "m/s"
+    KILOMETERS_PER_HOUR = "km/h"
+    KNOTS = "kn"
+    MILES_PER_HOUR = "mph"
+
+
 SPEED_FEET_PER_SECOND: Final = "ft/s"
+"""Deprecated: please use UnitOfSpeed.FEET_PER_SECOND"""
 SPEED_METERS_PER_SECOND: Final = "m/s"
+"""Deprecated: please use UnitOfSpeed.METERS_PER_SECOND"""
 SPEED_KILOMETERS_PER_HOUR: Final = "km/h"
+"""Deprecated: please use UnitOfSpeed.KILOMETERS_PER_HOUR"""
 SPEED_KNOTS: Final = "kn"
+"""Deprecated: please use UnitOfSpeed.KNOTS"""
 SPEED_MILES_PER_HOUR: Final = "mph"
+"""Deprecated: please use UnitOfSpeed.MILES_PER_HOUR"""
 
 SPEED_MILLIMETERS_PER_DAY: Final = "mm/d"
 """Deprecated: please use UnitOfVolumetricFlux.MILLIMETERS_PER_DAY"""
-- 
GitLab


From e0d94d799a85bab3b111e36cef4fe81d9679b82f Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 14:00:15 +0200
Subject: [PATCH 846/985] Migrate temperature units to an enum (#81006)

* Migrate temperature units to an enum

* Adjust spacing
---
 homeassistant/const.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 78188533cd7..348e3720ee0 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -520,10 +520,22 @@ CURRENCY_EURO: Final = "€"
 CURRENCY_DOLLAR: Final = "$"
 CURRENCY_CENT: Final = "¢"
 
+
 # Temperature units
+class UnitOfTemperature(StrEnum):
+    """Temperature units."""
+
+    CELSIUS = "°C"
+    FAHRENHEIT = "°F"
+    KELVIN = "K"
+
+
 TEMP_CELSIUS: Final = "°C"
+"""Deprecated: please use UnitOfTemperature.CELSIUS"""
 TEMP_FAHRENHEIT: Final = "°F"
+"""Deprecated: please use UnitOfTemperature.FAHRENHEIT"""
 TEMP_KELVIN: Final = "K"
+"""Deprecated: please use UnitOfTemperature.KELVIN"""
 
 # Time units
 TIME_MICROSECONDS: Final = "μs"
-- 
GitLab


From e53d74e3e83f5628fdd355ccb8d14f41b7e13070 Mon Sep 17 00:00:00 2001
From: "David F. Mulcahey" <david.mulcahey@me.com>
Date: Wed, 26 Oct 2022 08:16:49 -0400
Subject: [PATCH 847/985] Bump ZHA quirks to 0.0.84 (#81015)

---
 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 5796fc99f3f..4698c78d384 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -7,7 +7,7 @@
     "bellows==0.34.2",
     "pyserial==3.5",
     "pyserial-asyncio==0.6",
-    "zha-quirks==0.0.83",
+    "zha-quirks==0.0.84",
     "zigpy-deconz==0.19.0",
     "zigpy==0.51.3",
     "zigpy-xbee==0.16.2",
diff --git a/requirements_all.txt b/requirements_all.txt
index 9b5b1455efa..319e0dfbf84 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2604,7 +2604,7 @@ zengge==0.2
 zeroconf==0.39.2
 
 # homeassistant.components.zha
-zha-quirks==0.0.83
+zha-quirks==0.0.84
 
 # homeassistant.components.zhong_hong
 zhong_hong_hvac==1.0.9
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 2b5b8c92d7b..5269a91b646 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1805,7 +1805,7 @@ youless-api==0.16
 zeroconf==0.39.2
 
 # homeassistant.components.zha
-zha-quirks==0.0.83
+zha-quirks==0.0.84
 
 # homeassistant.components.zha
 zigpy-deconz==0.19.0
-- 
GitLab


From 6d78f6841ec19b0e00893300435130d3680913b0 Mon Sep 17 00:00:00 2001
From: kingy444 <toddlesking4@hotmail.com>
Date: Wed, 26 Oct 2022 23:24:17 +1100
Subject: [PATCH 848/985] Remove hardwired Powerview battery sensor (#81013)

---
 homeassistant/components/hunterdouglas_powerview/sensor.py | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py
index e4f6d52287b..a9ea823e0c6 100644
--- a/homeassistant/components/hunterdouglas_powerview/sensor.py
+++ b/homeassistant/components/hunterdouglas_powerview/sensor.py
@@ -19,8 +19,10 @@ from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import (
+    ATTR_BATTERY_KIND,
     ATTR_SIGNAL_STRENGTH,
     ATTR_SIGNAL_STRENGTH_MAX,
+    BATTERY_KIND_HARDWIRED,
     DOMAIN,
     ROOM_ID_IN_SHADE,
     ROOM_NAME_UNICODE,
@@ -60,7 +62,10 @@ SENSORS: Final = [
         native_value_fn=lambda shade: round(
             shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100
         ),
-        create_sensor_fn=lambda shade: bool(SHADE_BATTERY_LEVEL in shade.raw_data),
+        create_sensor_fn=lambda shade: bool(
+            shade.raw_data.get(ATTR_BATTERY_KIND) != BATTERY_KIND_HARDWIRED
+            and SHADE_BATTERY_LEVEL in shade.raw_data
+        ),
         update_fn=lambda shade: shade.refresh_battery(),
     ),
     PowerviewSensorDescription(
-- 
GitLab


From b786440edab3d41aa6a183df71f33c5bf4599fc7 Mon Sep 17 00:00:00 2001
From: Marvin Wichmann <me@marvin-wichmann.de>
Date: Wed, 26 Oct 2022 15:47:27 +0200
Subject: [PATCH 849/985] Add integration type to KNX integration (#81003)

---
 homeassistant/components/knx/manifest.json | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index 5107956050f..b29e44490e9 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -7,5 +7,6 @@
   "codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
   "quality_scale": "platinum",
   "iot_class": "local_push",
-  "loggers": ["xknx"]
+  "loggers": ["xknx"],
+  "integration_type": "hub"
 }
-- 
GitLab


From fbe7a0289cd2801f30b546e9871f3c50ea91d083 Mon Sep 17 00:00:00 2001
From: Tom Schneider <tom@sutomaji.net>
Date: Wed, 26 Oct 2022 15:48:13 +0200
Subject: [PATCH 850/985] Set hvv_departures device entry type to service
 (#80964)

---
 homeassistant/components/hvv_departures/binary_sensor.py | 2 ++
 homeassistant/components/hvv_departures/sensor.py        | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py
index 106279cbb91..2e47698ecd7 100644
--- a/homeassistant/components/hvv_departures/binary_sensor.py
+++ b/homeassistant/components/hvv_departures/binary_sensor.py
@@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import (
 )
 from homeassistant.config_entries import ConfigEntry
 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 homeassistant.helpers.update_coordinator import (
@@ -150,6 +151,7 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity):
     def device_info(self):
         """Return the device info for this sensor."""
         return DeviceInfo(
+            entry_type=DeviceEntryType.SERVICE,
             identifiers={
                 (
                     DOMAIN,
diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py
index e2de1758dd5..3a44bc59c72 100644
--- a/homeassistant/components/hvv_departures/sensor.py
+++ b/homeassistant/components/hvv_departures/sensor.py
@@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import ATTR_ID
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.device_registry import DeviceEntryType
 from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.util import Throttle
@@ -167,6 +168,7 @@ class HVVDepartureSensor(SensorEntity):
     def device_info(self):
         """Return the device info for this sensor."""
         return DeviceInfo(
+            entry_type=DeviceEntryType.SERVICE,
             identifiers={
                 (
                     DOMAIN,
-- 
GitLab


From 0d4b1866a7b0353090c0f85a9065482f4686ffb7 Mon Sep 17 00:00:00 2001
From: Tom Schneider <tom@sutomaji.net>
Date: Wed, 26 Oct 2022 15:49:10 +0200
Subject: [PATCH 851/985] Add has_entity_name support to hvv_departures
 (#80963)

---
 homeassistant/components/hvv_departures/binary_sensor.py | 5 +++--
 homeassistant/components/hvv_departures/sensor.py        | 5 +++--
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py
index 2e47698ecd7..a99dea57447 100644
--- a/homeassistant/components/hvv_departures/binary_sensor.py
+++ b/homeassistant/components/hvv_departures/binary_sensor.py
@@ -55,9 +55,9 @@ async def async_setup_entry(
                 description = elevator.get("description")
 
                 if label is not None:
-                    name = f"Elevator {label} at {station_name}"
+                    name = f"Elevator {label}"
                 else:
-                    name = f"Unknown elevator at {station_name}"
+                    name = "Unknown elevator"
 
                 if description is not None:
                     name += f" ({description})"
@@ -126,6 +126,7 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity):
     """HVVDepartureBinarySensor class."""
 
     _attr_attribution = ATTRIBUTION
+    _attr_has_entity_name = True
 
     def __init__(self, coordinator, idx, config_entry):
         """Initialize."""
diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py
index 3a44bc59c72..dfc69e51710 100644
--- a/homeassistant/components/hvv_departures/sensor.py
+++ b/homeassistant/components/hvv_departures/sensor.py
@@ -65,7 +65,8 @@ class HVVDepartureSensor(SensorEntity):
         self.station_name = self.config_entry.data[CONF_STATION]["name"]
         self._attr_extra_state_attributes = {}
         self._attr_available = False
-        self._attr_name = f"Departures at {self.station_name}"
+        self._attr_has_entity_name = True
+        self._attr_name = "Departures"
         self._last_error = None
 
         self.gti = hub.gti
@@ -178,5 +179,5 @@ class HVVDepartureSensor(SensorEntity):
                 )
             },
             manufacturer=MANUFACTURER,
-            name=self.name,
+            name=self.config_entry.data[CONF_STATION]["name"],
         )
-- 
GitLab


From dde763418a1c4ee0ecff17de76b6d670670a3bb7 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
Date: Thu, 27 Oct 2022 01:12:45 +1100
Subject: [PATCH 852/985] Add an RSSI sensor to the LIFX integration (#80993)

---
 homeassistant/components/lifx/__init__.py     |   9 +-
 .../components/lifx/binary_sensor.py          |  14 +-
 homeassistant/components/lifx/button.py       |  14 +-
 homeassistant/components/lifx/const.py        |   1 +
 homeassistant/components/lifx/coordinator.py  | 162 +++++++++++++-----
 homeassistant/components/lifx/entity.py       |  19 +-
 homeassistant/components/lifx/light.py        |   4 +-
 homeassistant/components/lifx/select.py       |  30 ++--
 homeassistant/components/lifx/sensor.py       |  74 ++++++++
 tests/components/lifx/__init__.py             |   7 +
 tests/components/lifx/test_binary_sensor.py   |   2 +-
 tests/components/lifx/test_sensor.py          | 131 ++++++++++++++
 12 files changed, 393 insertions(+), 74 deletions(-)
 create mode 100644 homeassistant/components/lifx/sensor.py
 create mode 100644 tests/components/lifx/test_sensor.py

diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py
index 2f20cb0e366..786ddd6abbf 100644
--- a/homeassistant/components/lifx/__init__.py
+++ b/homeassistant/components/lifx/__init__.py
@@ -57,7 +57,13 @@ CONFIG_SCHEMA = vol.All(
 )
 
 
-PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT, Platform.SELECT]
+PLATFORMS = [
+    Platform.BINARY_SENSOR,
+    Platform.BUTTON,
+    Platform.LIGHT,
+    Platform.SELECT,
+    Platform.SENSOR,
+]
 DISCOVERY_INTERVAL = timedelta(minutes=15)
 MIGRATION_INTERVAL = timedelta(minutes=5)
 
@@ -199,6 +205,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     coordinator.async_setup()
     try:
         await coordinator.async_config_entry_first_refresh()
+        await coordinator.sensor_coordinator.async_config_entry_first_refresh()
     except ConfigEntryNotReady:
         connection.async_stop()
         raise
diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py
index 273ef035757..bdc2c9a1ffa 100644
--- a/homeassistant/components/lifx/binary_sensor.py
+++ b/homeassistant/components/lifx/binary_sensor.py
@@ -12,8 +12,8 @@ from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import DOMAIN, HEV_CYCLE_STATE
-from .coordinator import LIFXUpdateCoordinator
-from .entity import LIFXEntity
+from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
+from .entity import LIFXSensorEntity
 from .util import lifx_features
 
 HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription(
@@ -34,28 +34,28 @@ async def async_setup_entry(
         async_add_entities(
             [
                 LIFXHevCycleBinarySensorEntity(
-                    coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR
+                    coordinator=coordinator.sensor_coordinator,
+                    description=HEV_CYCLE_STATE_SENSOR,
                 )
             ]
         )
 
 
-class LIFXHevCycleBinarySensorEntity(LIFXEntity, BinarySensorEntity):
+class LIFXHevCycleBinarySensorEntity(LIFXSensorEntity, BinarySensorEntity):
     """LIFX HEV cycle state binary sensor."""
 
     _attr_has_entity_name = True
 
     def __init__(
         self,
-        coordinator: LIFXUpdateCoordinator,
+        coordinator: LIFXSensorUpdateCoordinator,
         description: BinarySensorEntityDescription,
     ) -> None:
         """Initialise the sensor."""
         super().__init__(coordinator)
-
         self.entity_description = description
         self._attr_name = description.name
-        self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
+        self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
         self._async_update_attrs()
 
     @callback
diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py
index 76afdc785e9..4d917009c5d 100644
--- a/homeassistant/components/lifx/button.py
+++ b/homeassistant/components/lifx/button.py
@@ -12,8 +12,8 @@ from homeassistant.helpers.entity import EntityCategory
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 
 from .const import DOMAIN, IDENTIFY, RESTART
-from .coordinator import LIFXUpdateCoordinator
-from .entity import LIFXEntity
+from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
+from .entity import LIFXSensorEntity
 
 RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription(
     key=RESTART,
@@ -38,20 +38,22 @@ async def async_setup_entry(
     domain_data = hass.data[DOMAIN]
     coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id]
     async_add_entities(
-        cls(coordinator) for cls in (LIFXRestartButton, LIFXIdentifyButton)
+        cls(coordinator.sensor_coordinator)
+        for cls in (LIFXRestartButton, LIFXIdentifyButton)
     )
 
 
-class LIFXButton(LIFXEntity, ButtonEntity):
+class LIFXButton(LIFXSensorEntity, ButtonEntity):
     """Base LIFX button."""
 
     _attr_has_entity_name: bool = True
+    _attr_should_poll: bool = False
 
-    def __init__(self, coordinator: LIFXUpdateCoordinator) -> None:
+    def __init__(self, coordinator: LIFXSensorUpdateCoordinator) -> None:
         """Initialise a LIFX button."""
         super().__init__(coordinator)
         self._attr_unique_id = (
-            f"{coordinator.serial_number}_{self.entity_description.key}"
+            f"{coordinator.parent.serial_number}_{self.entity_description.key}"
         )
 
 
diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py
index 1502c51204b..af9dfa5a277 100644
--- a/homeassistant/components/lifx/const.py
+++ b/homeassistant/components/lifx/const.py
@@ -35,6 +35,7 @@ ATTR_INDICATION = "indication"
 ATTR_INFRARED = "infrared"
 ATTR_POWER = "power"
 ATTR_REMAINING = "remaining"
+ATTR_RSSI = "rssi"
 ATTR_ZONES = "zones"
 
 ATTR_THEME = "theme"
diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py
index 2ec3a6d3745..9343c3b7dad 100644
--- a/homeassistant/components/lifx/coordinator.py
+++ b/homeassistant/components/lifx/coordinator.py
@@ -2,9 +2,11 @@
 from __future__ import annotations
 
 import asyncio
+from collections.abc import Callable
 from datetime import timedelta
 from enum import IntEnum
 from functools import partial
+from math import floor, log10
 from typing import Any, cast
 
 from aiolifx.aiolifx import (
@@ -15,8 +17,13 @@ from aiolifx.aiolifx import (
 )
 from aiolifx.connection import LIFXConnection
 from aiolifx_themes.themes import ThemeLibrary, ThemePainter
+from awesomeversion import AwesomeVersion
 
-from homeassistant.const import Platform
+from homeassistant.const import (
+    SIGNAL_STRENGTH_DECIBELS,
+    SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+    Platform,
+)
 from homeassistant.core import HomeAssistant, callback
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers import entity_registry as er
@@ -41,8 +48,11 @@ from .util import (
     lifx_features,
 )
 
+LIGHT_UPDATE_INTERVAL = 10
+SENSOR_UPDATE_INTERVAL = 30
 REQUEST_REFRESH_DELAY = 0.35
 LIFX_IDENTIFY_DELAY = 3.0
+RSSI_DBM_FW = AwesomeVersion("2.77")
 
 
 class FirmwareEffect(IntEnum):
@@ -69,14 +79,13 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
         self.device: Light = connection.device
         self.lock = asyncio.Lock()
         self.active_effect = FirmwareEffect.OFF
-        update_interval = timedelta(seconds=10)
-        self.last_used_theme: str = ""
+        self.sensor_coordinator = LIFXSensorUpdateCoordinator(hass, self, title)
 
         super().__init__(
             hass,
             _LOGGER,
             name=f"{title} ({self.device.ip_addr})",
-            update_interval=update_interval,
+            update_interval=timedelta(seconds=LIGHT_UPDATE_INTERVAL),
             # We don't want an immediate refresh since the device
             # takes a moment to reflect the state change
             request_refresh_debouncer=Debouncer(
@@ -112,11 +121,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
         """Return the label of the bulb."""
         return cast(str, self.device.label)
 
-    @property
-    def current_infrared_brightness(self) -> str | None:
-        """Return the current infrared brightness as a string."""
-        return infrared_brightness_value_to_option(self.device.infrared_brightness)
-
     async def diagnostics(self) -> dict[str, Any]:
         """Return diagnostic information about the device."""
         features = lifx_features(self.device)
@@ -162,19 +166,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
             platform, DOMAIN, f"{self.serial_number}_{key}"
         )
 
-    async def async_identify_bulb(self) -> None:
-        """Identify the device by flashing it three times."""
-        bulb: Light = self.device
-        if bulb.power_level:
-            # just flash the bulb for three seconds
-            await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
-            return
-        # Turn the bulb on first, flash for 3 seconds, then turn off
-        await self.async_set_power(state=True, duration=1)
-        await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
-        await asyncio.sleep(LIFX_IDENTIFY_DELAY)
-        await self.async_set_power(state=False, duration=1)
-
     async def _async_update_data(self) -> None:
         """Fetch all device data from the api."""
         async with self.lock:
@@ -203,12 +194,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
                 await self.async_get_color_zones()
                 await self.async_get_multizone_effect()
 
-            if lifx_features(self.device)["hev"]:
-                await self.async_get_hev_cycle()
-
-            if lifx_features(self.device)["infrared"]:
-                response = await async_execute_lifx(self.device.get_infrared)
-
     async def async_get_color_zones(self) -> None:
         """Get updated color information for each zone."""
         zone = 0
@@ -234,17 +219,6 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
                 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:
-            return None
-        return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0)
-
-    async def async_get_hev_cycle(self) -> None:
-        """Update the HEV cycle status from a LIFX Clean bulb."""
-        if lifx_features(self.device)["hev"]:
-            await async_execute_lifx(self.device.get_hev_cycle)
-
     async def async_set_waveform_optional(
         self, value: dict[str, Any], rapid: bool = False
     ) -> None:
@@ -381,20 +355,118 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
         """Return the enum value of the currently active firmware effect."""
         return self.active_effect.value
 
-    async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None:
-        """Start or stop an HEV cycle on a LIFX Clean bulb."""
+
+class LIFXSensorUpdateCoordinator(DataUpdateCoordinator):
+    """DataUpdateCoordinator to gather data for a specific lifx device."""
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        parent: LIFXUpdateCoordinator,
+        title: str,
+    ) -> None:
+        """Initialize DataUpdateCoordinator."""
+        self.parent: LIFXUpdateCoordinator = parent
+        self.device: Light = parent.device
+        self._update_rssi: bool = False
+        self._rssi: int = 0
+        self.last_used_theme: str = ""
+
+        super().__init__(
+            hass,
+            _LOGGER,
+            name=f"{title} Sensors ({self.device.ip_addr})",
+            update_interval=timedelta(seconds=SENSOR_UPDATE_INTERVAL),
+            # Refresh immediately because the changes are not visible
+            request_refresh_debouncer=Debouncer(
+                hass, _LOGGER, cooldown=0, immediate=True
+            ),
+        )
+
+    @property
+    def rssi(self) -> int:
+        """Return stored RSSI value."""
+        return self._rssi
+
+    @property
+    def rssi_uom(self) -> str:
+        """Return the RSSI unit of measurement."""
+        if AwesomeVersion(self.device.host_firmware_version) <= RSSI_DBM_FW:
+            return SIGNAL_STRENGTH_DECIBELS
+
+        return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+
+    @property
+    def current_infrared_brightness(self) -> str | None:
+        """Return the current infrared brightness as a string."""
+        return infrared_brightness_value_to_option(self.device.infrared_brightness)
+
+    async def _async_update_data(self) -> None:
+        """Fetch all device data from the api."""
+
+        if self._update_rssi is True:
+            await self.async_update_rssi()
+
         if lifx_features(self.device)["hev"]:
-            await async_execute_lifx(
-                partial(self.device.set_hev_cycle, enable=enable, duration=duration)
-            )
+            await self.async_get_hev_cycle()
+
+        if lifx_features(self.device)["infrared"]:
+            await async_execute_lifx(self.device.get_infrared)
 
     async def async_set_infrared_brightness(self, option: str) -> None:
         """Set infrared brightness."""
         infrared_brightness = infrared_brightness_option_to_value(option)
         await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness))
 
+    async def async_identify_bulb(self) -> None:
+        """Identify the device by flashing it three times."""
+        bulb: Light = self.device
+        if bulb.power_level:
+            # just flash the bulb for three seconds
+            await self.parent.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
+            return
+        # Turn the bulb on first, flash for 3 seconds, then turn off
+        await self.parent.async_set_power(state=True, duration=1)
+        await self.parent.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
+        await asyncio.sleep(LIFX_IDENTIFY_DELAY)
+        await self.parent.async_set_power(state=False, duration=1)
+
+    def async_enable_rssi_updates(self) -> Callable[[], None]:
+        """Enable RSSI signal strength updates."""
+
+        @callback
+        def _async_disable_rssi_updates() -> None:
+            """Disable RSSI updates when sensor removed."""
+            self._update_rssi = False
+
+        self._update_rssi = True
+        return _async_disable_rssi_updates
+
+    async def async_update_rssi(self) -> None:
+        """Update RSSI value."""
+        resp = await async_execute_lifx(self.device.get_wifiinfo)
+        self._rssi = int(floor(10 * log10(resp.signal) + 0.5))
+
+    def async_get_hev_cycle_state(self) -> bool | None:
+        """Return the current HEV cycle state."""
+        if self.device.hev_cycle is None:
+            return None
+        return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0)
+
+    async def async_get_hev_cycle(self) -> None:
+        """Update the HEV cycle status from a LIFX Clean bulb."""
+        if lifx_features(self.device)["hev"]:
+            await async_execute_lifx(self.device.get_hev_cycle)
+
+    async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None:
+        """Start or stop an HEV cycle on a LIFX Clean bulb."""
+        if lifx_features(self.device)["hev"]:
+            await async_execute_lifx(
+                partial(self.device.set_hev_cycle, enable=enable, duration=duration)
+            )
+
     async def async_apply_theme(self, theme_name: str) -> None:
         """Apply the selected theme to the device."""
         self.last_used_theme = theme_name
         theme = ThemeLibrary().get_theme(theme_name)
-        await ThemePainter(self.hass.loop).paint(theme, [self.device])
+        await ThemePainter(self.hass.loop).paint(theme, [self.parent.device])
diff --git a/homeassistant/components/lifx/entity.py b/homeassistant/components/lifx/entity.py
index 0007ab998a9..a500e353fbf 100644
--- a/homeassistant/components/lifx/entity.py
+++ b/homeassistant/components/lifx/entity.py
@@ -8,7 +8,7 @@ from homeassistant.helpers.entity import DeviceInfo
 from homeassistant.helpers.update_coordinator import CoordinatorEntity
 
 from .const import DOMAIN
-from .coordinator import LIFXUpdateCoordinator
+from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
 
 
 class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]):
@@ -26,3 +26,20 @@ class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]):
             model=products.product_map.get(self.bulb.product, "LIFX Bulb"),
             sw_version=self.bulb.host_firmware_version,
         )
+
+
+class LIFXSensorEntity(CoordinatorEntity[LIFXSensorUpdateCoordinator]):
+    """Representation of a LIFX sensor entity with a sensor coordinator."""
+
+    def __init__(self, coordinator: LIFXSensorUpdateCoordinator) -> None:
+        """Initialise the sensor."""
+        super().__init__(coordinator)
+        self.bulb = coordinator.parent.device
+        self._attr_device_info = DeviceInfo(
+            identifiers={(DOMAIN, coordinator.parent.serial_number)},
+            connections={(dr.CONNECTION_NETWORK_MAC, coordinator.parent.mac_address)},
+            manufacturer="LIFX",
+            name=coordinator.parent.label,
+            model=products.product_map.get(self.bulb.product, "LIFX Bulb"),
+            sw_version=self.bulb.host_firmware_version,
+        )
diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py
index 3b9b83cd1fc..7b23e1d34c4 100644
--- a/homeassistant/components/lifx/light.py
+++ b/homeassistant/components/lifx/light.py
@@ -271,7 +271,9 @@ class LIFXLight(LIFXEntity, LightEntity):
                 "This device does not support setting HEV cycle state"
             )
 
-        await self.coordinator.async_set_hev_cycle_state(power, duration or 0)
+        await self.coordinator.sensor_coordinator.async_set_hev_cycle_state(
+            power, duration or 0
+        )
         await self.update_during_transition(duration or 0)
 
     async def set_power(
diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py
index fe49e9d8599..681fe41cc05 100644
--- a/homeassistant/components/lifx/select.py
+++ b/homeassistant/components/lifx/select.py
@@ -15,8 +15,8 @@ from .const import (
     INFRARED_BRIGHTNESS,
     INFRARED_BRIGHTNESS_VALUES_MAP,
 )
-from .coordinator import LIFXUpdateCoordinator
-from .entity import LIFXEntity
+from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
+from .entity import LIFXSensorEntity
 from .util import lifx_features
 
 THEME_NAMES = [theme_name.lower() for theme_name in ThemeLibrary().themes]
@@ -41,36 +41,41 @@ async def async_setup_entry(
 ) -> None:
     """Set up LIFX from a config entry."""
     coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
-    entities: list[LIFXEntity] = []
+
+    entities: list[LIFXSensorEntity] = []
 
     if lifx_features(coordinator.device)["infrared"]:
         entities.append(
             LIFXInfraredBrightnessSelectEntity(
-                coordinator=coordinator, description=INFRARED_BRIGHTNESS_ENTITY
+                coordinator.sensor_coordinator, description=INFRARED_BRIGHTNESS_ENTITY
             )
         )
 
     if lifx_features(coordinator.device)["multizone"] is True:
         entities.append(
-            LIFXThemeSelectEntity(coordinator=coordinator, description=THEME_ENTITY)
+            LIFXThemeSelectEntity(
+                coordinator.sensor_coordinator, description=THEME_ENTITY
+            )
         )
 
     async_add_entities(entities)
 
 
-class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
+class LIFXInfraredBrightnessSelectEntity(LIFXSensorEntity, SelectEntity):
     """LIFX Nightvision infrared brightness configuration entity."""
 
     _attr_has_entity_name = True
 
     def __init__(
-        self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription
+        self,
+        coordinator: LIFXSensorUpdateCoordinator,
+        description: SelectEntityDescription,
     ) -> None:
         """Initialise the IR brightness config entity."""
         super().__init__(coordinator)
         self.entity_description = description
         self._attr_name = description.name
-        self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
+        self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
         self._attr_current_option = coordinator.current_infrared_brightness
 
     @callback
@@ -89,21 +94,22 @@ class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity):
         await self.coordinator.async_set_infrared_brightness(option)
 
 
-class LIFXThemeSelectEntity(LIFXEntity, SelectEntity):
+class LIFXThemeSelectEntity(LIFXSensorEntity, SelectEntity):
     """Theme entity for LIFX multizone devices."""
 
     _attr_has_entity_name = True
-    _attr_should_poll = False
 
     def __init__(
-        self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription
+        self,
+        coordinator: LIFXSensorUpdateCoordinator,
+        description: SelectEntityDescription,
     ) -> None:
         """Initialise the theme selection entity."""
 
         super().__init__(coordinator)
         self.entity_description = description
         self._attr_name = description.name
-        self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
+        self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
         self._attr_current_option = None
 
     @callback
diff --git a/homeassistant/components/lifx/sensor.py b/homeassistant/components/lifx/sensor.py
new file mode 100644
index 00000000000..bff04f0a807
--- /dev/null
+++ b/homeassistant/components/lifx/sensor.py
@@ -0,0 +1,74 @@
+"""Sensors for LIFX lights."""
+from __future__ import annotations
+
+from datetime import timedelta
+
+from homeassistant.components.sensor import (
+    SensorDeviceClass,
+    SensorEntity,
+    SensorEntityDescription,
+    SensorStateClass,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity import EntityCategory
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import ATTR_RSSI, DOMAIN
+from .coordinator import LIFXSensorUpdateCoordinator, LIFXUpdateCoordinator
+from .entity import LIFXSensorEntity
+
+SCAN_INTERVAL = timedelta(seconds=30)
+
+RSSI_SENSOR = SensorEntityDescription(
+    key=ATTR_RSSI,
+    name="RSSI",
+    device_class=SensorDeviceClass.SIGNAL_STRENGTH,
+    entity_category=EntityCategory.DIAGNOSTIC,
+    state_class=SensorStateClass.MEASUREMENT,
+    entity_registry_enabled_default=False,
+)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+    """Set up LIFX sensor from config entry."""
+    coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
+    async_add_entities([LIFXRssiSensor(coordinator.sensor_coordinator, RSSI_SENSOR)])
+
+
+class LIFXRssiSensor(LIFXSensorEntity, SensorEntity):
+    """LIFX RSSI sensor."""
+
+    _attr_has_entity_name = True
+
+    def __init__(
+        self,
+        coordinator: LIFXSensorUpdateCoordinator,
+        description: SensorEntityDescription,
+    ) -> None:
+        """Initialise the RSSI sensor."""
+
+        super().__init__(coordinator)
+        self.entity_description = description
+        self._attr_name = description.name
+        self._attr_unique_id = f"{coordinator.parent.serial_number}_{description.key}"
+        self._attr_native_unit_of_measurement = coordinator.rssi_uom
+
+    @callback
+    def _handle_coordinator_update(self) -> None:
+        """Handle updated data from the coordinator."""
+        self._async_update_attrs()
+        super()._handle_coordinator_update()
+
+    @callback
+    def _async_update_attrs(self) -> None:
+        """Handle coordinator updates."""
+        self._attr_native_value = self.coordinator.rssi
+
+    @callback
+    async def async_added_to_hass(self) -> None:
+        """Enable RSSI updates."""
+        self.async_on_remove(self.coordinator.async_enable_rssi_updates())
+        return await super().async_added_to_hass()
diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py
index 543f2f7d7a2..774376e1a99 100644
--- a/tests/components/lifx/__init__.py
+++ b/tests/components/lifx/__init__.py
@@ -91,6 +91,7 @@ def _mocked_bulb() -> Light:
     bulb.set_power = MockLifxCommand(bulb)
     bulb.set_color = MockLifxCommand(bulb)
     bulb.get_hostfirmware = MockLifxCommand(bulb)
+    bulb.get_wifiinfo = MockLifxCommand(bulb, signal=100)
     bulb.get_version = MockLifxCommand(bulb)
     bulb.set_waveform_optional = MockLifxCommand(bulb)
     bulb.product = 1  # LIFX Original 1000
@@ -168,6 +169,12 @@ def _mocked_tile() -> Light:
     return bulb
 
 
+def _mocked_bulb_old_firmware() -> Light:
+    bulb = _mocked_bulb()
+    bulb.host_firmware_version = "2.77"
+    return bulb
+
+
 def _mocked_bulb_new_firmware() -> Light:
     bulb = _mocked_bulb()
     bulb.host_firmware_version = "3.90"
diff --git a/tests/components/lifx/test_binary_sensor.py b/tests/components/lifx/test_binary_sensor.py
index bb0b210704a..40db6ce1148 100644
--- a/tests/components/lifx/test_binary_sensor.py
+++ b/tests/components/lifx/test_binary_sensor.py
@@ -1,4 +1,4 @@
-"""Test the lifx binary sensor platwform."""
+"""Test the lifx binary sensor platform."""
 from __future__ import annotations
 
 from datetime import timedelta
diff --git a/tests/components/lifx/test_sensor.py b/tests/components/lifx/test_sensor.py
new file mode 100644
index 00000000000..a36e151849b
--- /dev/null
+++ b/tests/components/lifx/test_sensor.py
@@ -0,0 +1,131 @@
+"""Test the LIFX sensor platform."""
+from __future__ import annotations
+
+from datetime import timedelta
+
+from homeassistant.components import lifx
+from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
+from homeassistant.const import (
+    ATTR_DEVICE_CLASS,
+    ATTR_UNIT_OF_MEASUREMENT,
+    CONF_HOST,
+    SIGNAL_STRENGTH_DECIBELS,
+    SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+)
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util
+
+from . import (
+    DEFAULT_ENTRY_TITLE,
+    IP_ADDRESS,
+    MAC_ADDRESS,
+    _mocked_bulb,
+    _mocked_bulb_old_firmware,
+    _patch_config_flow_try_connect,
+    _patch_device,
+    _patch_discovery,
+)
+
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+async def test_rssi_sensor(hass: HomeAssistant) -> None:
+    """Test LIFX RSSI sensor entity."""
+
+    config_entry = MockConfigEntry(
+        domain=lifx.DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_bulb()
+    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 = "sensor.my_bulb_rssi"
+    entity_registry = er.async_get(hass)
+
+    entry = entity_registry.entities.get(entity_id)
+    assert entry
+    assert entry.disabled
+    assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
+
+    # Test enabling entity
+    updated_entry = entity_registry.async_update_entity(
+        entry.entity_id, **{"disabled_by": None}
+    )
+
+    with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
+        device=bulb
+    ), _patch_device(device=bulb):
+        await hass.config_entries.async_reload(config_entry.entry_id)
+        await hass.async_block_till_done()
+
+    assert updated_entry != entry
+    assert updated_entry.disabled is False
+    assert updated_entry.unit_of_measurement == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+
+    async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120))
+    await hass.async_block_till_done()
+
+    rssi = hass.states.get(entity_id)
+    assert (
+        rssi.attributes[ATTR_UNIT_OF_MEASUREMENT] == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+    )
+    assert rssi.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SIGNAL_STRENGTH
+    assert rssi.attributes["state_class"] == SensorStateClass.MEASUREMENT
+
+
+async def test_rssi_sensor_old_firmware(hass: HomeAssistant) -> None:
+    """Test LIFX RSSI sensor entity."""
+
+    config_entry = MockConfigEntry(
+        domain=lifx.DOMAIN,
+        title=DEFAULT_ENTRY_TITLE,
+        data={CONF_HOST: IP_ADDRESS},
+        unique_id=MAC_ADDRESS,
+    )
+    config_entry.add_to_hass(hass)
+    bulb = _mocked_bulb_old_firmware()
+    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 = "sensor.my_bulb_rssi"
+    entity_registry = er.async_get(hass)
+
+    entry = entity_registry.entities.get(entity_id)
+    assert entry
+    assert entry.disabled
+    assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
+
+    # Test enabling entity
+    updated_entry = entity_registry.async_update_entity(
+        entry.entity_id, **{"disabled_by": None}
+    )
+
+    with _patch_discovery(device=bulb), _patch_config_flow_try_connect(
+        device=bulb
+    ), _patch_device(device=bulb):
+        await hass.config_entries.async_reload(config_entry.entry_id)
+        await hass.async_block_till_done()
+
+    assert updated_entry != entry
+    assert updated_entry.disabled is False
+    assert updated_entry.unit_of_measurement == SIGNAL_STRENGTH_DECIBELS
+
+    async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120))
+    await hass.async_block_till_done()
+
+    rssi = hass.states.get(entity_id)
+    assert rssi.attributes[ATTR_UNIT_OF_MEASUREMENT] == SIGNAL_STRENGTH_DECIBELS
+    assert rssi.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.SIGNAL_STRENGTH
+    assert rssi.attributes["state_class"] == SensorStateClass.MEASUREMENT
-- 
GitLab


From e18adb379d4212256165d7676af62ce4adf25142 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 16:21:29 +0200
Subject: [PATCH 853/985] Migrate mass units to an enum (#81021)

---
 homeassistant/const.py | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 348e3720ee0..766d78e7ac6 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -644,14 +644,31 @@ VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = "ft³/m"
 # Area units
 AREA_SQUARE_METERS: Final = "m²"
 
+
 # Mass units
+class UnitOfMass(StrEnum):
+    """Mass units."""
+
+    GRAMS = "g"
+    KILOGRAMS = "kg"
+    MILLIGRAMS = "mg"
+    MICROGRAMS = "µg"
+    OUNCES = "oz"
+    POUNDS = "lb"
+
+
 MASS_GRAMS: Final = "g"
+"""Deprecated: please use UnitOfMass.GRAMS"""
 MASS_KILOGRAMS: Final = "kg"
+"""Deprecated: please use UnitOfMass.KILOGRAMS"""
 MASS_MILLIGRAMS: Final = "mg"
+"""Deprecated: please use UnitOfMass.MILLIGRAMS"""
 MASS_MICROGRAMS: Final = "µg"
-
+"""Deprecated: please use UnitOfMass.MICROGRAMS"""
 MASS_OUNCES: Final = "oz"
+"""Deprecated: please use UnitOfMass.OUNCES"""
 MASS_POUNDS: Final = "lb"
+"""Deprecated: please use UnitOfMass.POUNDS"""
 
 # Conductivity units
 CONDUCTIVITY: Final = "µS/cm"
-- 
GitLab


From 33ef2abf9e3ae51f6aeb3957b81311ace3cae647 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
Date: Wed, 26 Oct 2022 16:44:24 +0200
Subject: [PATCH 854/985] Add a new clear cache service to the velbus
 integration (#79995)

---
 homeassistant/components/velbus/__init__.py   | 40 +++++++++++++++++++
 homeassistant/components/velbus/const.py      |  1 +
 homeassistant/components/velbus/services.yaml | 23 +++++++++++
 3 files changed, 64 insertions(+)

diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py
index 66d9fef71a3..585e093a42c 100644
--- a/homeassistant/components/velbus/__init__.py
+++ b/homeassistant/components/velbus/__init__.py
@@ -1,7 +1,9 @@
 """Support for Velbus devices."""
 from __future__ import annotations
 
+from contextlib import suppress
 import logging
+import os
 import shutil
 
 from velbusaio.controller import Velbus
@@ -19,6 +21,7 @@ from .const import (
     CONF_INTERFACE,
     CONF_MEMO_TEXT,
     DOMAIN,
+    SERVICE_CLEAR_CACHE,
     SERVICE_SCAN,
     SERVICE_SET_MEMO_TEXT,
     SERVICE_SYNC,
@@ -132,6 +135,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         ),
     )
 
+    async def clear_cache(call: ServiceCall) -> None:
+        """Handle a clear cache service call."""
+        # clear the cache
+        with suppress(FileNotFoundError):
+            if call.data[CONF_ADDRESS]:
+                await hass.async_add_executor_job(
+                    os.unlink,
+                    hass.config.path(
+                        STORAGE_DIR,
+                        f"velbuscache-{call.data[CONF_INTERFACE]}/{call.data[CONF_ADDRESS]}.p",
+                    ),
+                )
+            else:
+                await hass.async_add_executor_job(
+                    shutil.rmtree,
+                    hass.config.path(
+                        STORAGE_DIR, f"velbuscache-{call.data[CONF_INTERFACE]}/"
+                    ),
+                )
+        # call a scan to repopulate
+        await scan(call)
+
+    hass.services.async_register(
+        DOMAIN,
+        SERVICE_CLEAR_CACHE,
+        clear_cache,
+        vol.Schema(
+            {
+                vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
+                vol.Optional(CONF_ADDRESS): vol.All(
+                    vol.Coerce(int), vol.Range(min=0, max=255)
+                ),
+            }
+        ),
+    )
+
     return True
 
 
@@ -149,4 +188,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         hass.services.async_remove(DOMAIN, SERVICE_SCAN)
         hass.services.async_remove(DOMAIN, SERVICE_SYNC)
         hass.services.async_remove(DOMAIN, SERVICE_SET_MEMO_TEXT)
+        hass.services.async_remove(DOMAIN, SERVICE_CLEAR_CACHE)
     return unload_ok
diff --git a/homeassistant/components/velbus/const.py b/homeassistant/components/velbus/const.py
index 7c41274f11c..a3949646598 100644
--- a/homeassistant/components/velbus/const.py
+++ b/homeassistant/components/velbus/const.py
@@ -16,6 +16,7 @@ CONF_MEMO_TEXT: Final = "memo_text"
 SERVICE_SCAN: Final = "scan"
 SERVICE_SYNC: Final = "sync_clock"
 SERVICE_SET_MEMO_TEXT: Final = "set_memo_text"
+SERVICE_CLEAR_CACHE: Final = "clear_cache"
 
 PRESET_MODES: Final = {
     PRESET_ECO: "safe",
diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml
index 6dbf5e8cb47..32cda00f708 100644
--- a/homeassistant/components/velbus/services.yaml
+++ b/homeassistant/components/velbus/services.yaml
@@ -24,6 +24,29 @@ scan:
       selector:
         text:
 
+clear_cache:
+  name: Clear cache
+  description: Clears the velbuscache and then starts a new scan
+  fields:
+    interface:
+      name: Interface
+      description: The velbus interface to send the command to, this will be the same value as used during configuration
+      required: true
+      example: "192.168.1.5:27015"
+      default: ""
+      selector:
+        text:
+    address:
+      name: Address
+      description: >
+        The module address in decimal format, if this is provided we only clear this module, if nothing is provided we clear the whole cache directory (all modules)
+        The decimal addresses are displayed in front of the modules listed at the integration page.
+      required: false
+      selector:
+        number:
+          min: 1
+          max: 254
+
 set_memo_text:
   name: Set memo text
   description: >
-- 
GitLab


From c4f6b8a55b20af058e9d713a936783f09510cf6a Mon Sep 17 00:00:00 2001
From: Xeevis <Xeevis@users.noreply.github.com>
Date: Wed, 26 Oct 2022 16:50:01 +0200
Subject: [PATCH 855/985] Use scan_interval in `netdata` (#80959)

Co-authored-by: Franck Nijhof <git@frenck.dev>
---
 homeassistant/components/netdata/sensor.py | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py
index 8c51a3fd9a6..6606604ac90 100644
--- a/homeassistant/components/netdata/sensor.py
+++ b/homeassistant/components/netdata/sensor.py
@@ -1,7 +1,6 @@
 """Support gathering system information of hosts which are running netdata."""
 from __future__ import annotations
 
-from datetime import timedelta
 import logging
 
 from netdata import Netdata
@@ -22,12 +21,9 @@ from homeassistant.exceptions import PlatformNotReady
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
-from homeassistant.util import Throttle
 
 _LOGGER = logging.getLogger(__name__)
 
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
-
 CONF_DATA_GROUP = "data_group"
 CONF_ELEMENT = "element"
 CONF_INVERT = "invert"
@@ -223,7 +219,6 @@ class NetdataData:
         self.api = api
         self.available = True
 
-    @Throttle(MIN_TIME_BETWEEN_UPDATES)
     async def async_update(self):
         """Get the latest data from the Netdata REST API."""
 
-- 
GitLab


From 0e2bea038d1f6ef9b5f4ff507198d61342eaa208 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
Date: Wed, 26 Oct 2022 07:57:49 -0700
Subject: [PATCH 856/985] Update Google Calendar to synchronize calendar events
 efficiently (#80925)

* Sync google calendar and serve from local storage

Update to use new gcal_sync APIs
Update google calendar filter logic
Remove storage on config entry removal
Make timeline queries timezone aware
Do not block startup while syncing

* Minor readability tweaks

* Remove unnecessary args to async_add_entities

* Change how task is created on startup

* Update homeassistant/components/google/calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Revert min time between updates

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 homeassistant/components/google/__init__.py |  11 ++
 homeassistant/components/google/calendar.py | 110 +++++++++-------
 homeassistant/components/google/const.py    |   1 +
 homeassistant/components/google/store.py    |  53 ++++++++
 tests/components/google/conftest.py         |  12 +-
 tests/components/google/test_calendar.py    | 131 ++++++++++----------
 tests/components/google/test_init.py        |  21 ++++
 7 files changed, 223 insertions(+), 116 deletions(-)
 create mode 100644 homeassistant/components/google/store.py

diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py
index 33a1216085d..c4e51739efd 100644
--- a/homeassistant/components/google/__init__.py
+++ b/homeassistant/components/google/__init__.py
@@ -36,6 +36,7 @@ from homeassistant.helpers.entity import generate_entity_id
 from .api import ApiAuthImpl, get_feature_access
 from .const import (
     DATA_SERVICE,
+    DATA_STORE,
     DOMAIN,
     EVENT_DESCRIPTION,
     EVENT_END_DATE,
@@ -49,6 +50,7 @@ from .const import (
     EVENT_TYPES_CONF,
     FeatureAccess,
 )
+from .store import LocalCalendarStore
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -171,6 +173,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         ApiAuthImpl(async_get_clientsession(hass), session)
     )
     hass.data[DOMAIN][entry.entry_id][DATA_SERVICE] = calendar_service
+    hass.data[DOMAIN][entry.entry_id][DATA_STORE] = LocalCalendarStore(
+        hass, entry.entry_id
+    )
 
     if entry.unique_id is None:
         try:
@@ -213,6 +218,12 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
         await hass.config_entries.async_reload(entry.entry_id)
 
 
+async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+    """Handle removal of a local storage."""
+    store = LocalCalendarStore(hass, entry.entry_id)
+    await store.async_remove()
+
+
 async def async_setup_add_event_service(
     hass: HomeAssistant,
     calendar_service: GoogleCalendarService,
diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py
index 995fb1ec98f..ca1228759cd 100644
--- a/homeassistant/components/google/calendar.py
+++ b/homeassistant/components/google/calendar.py
@@ -2,13 +2,17 @@
 
 from __future__ import annotations
 
+import asyncio
 from datetime import datetime, timedelta
 import logging
 from typing import Any
 
-from gcal_sync.api import GoogleCalendarService, ListEventsRequest
+from gcal_sync.api import SyncEventsRequest
 from gcal_sync.exceptions import ApiException
 from gcal_sync.model import DateOrDatetime, Event
+from gcal_sync.store import ScopedCalendarStore
+from gcal_sync.sync import CalendarEventSyncManager
+from gcal_sync.timeline import Timeline
 import voluptuous as vol
 
 from homeassistant.components.calendar import (
@@ -34,12 +38,12 @@ from homeassistant.helpers.update_coordinator import (
     DataUpdateCoordinator,
     UpdateFailed,
 )
+from homeassistant.util import dt as dt_util
 
 from . import (
     CONF_IGNORE_AVAILABILITY,
     CONF_SEARCH,
     CONF_TRACK,
-    DATA_SERVICE,
     DEFAULT_CONF_OFFSET,
     DOMAIN,
     YAML_DEVICES,
@@ -49,6 +53,8 @@ from . import (
 )
 from .api import get_feature_access
 from .const import (
+    DATA_SERVICE,
+    DATA_STORE,
     EVENT_DESCRIPTION,
     EVENT_END_DATE,
     EVENT_END_DATETIME,
@@ -66,6 +72,10 @@ _LOGGER = logging.getLogger(__name__)
 
 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
 
+# Avoid syncing super old data on initial syncs. Note that old but active
+# recurring events are still included.
+SYNC_EVENT_MIN_TIME = timedelta(days=-90)
+
 # Events have a transparency that determine whether or not they block time on calendar.
 # When an event is opaque, it means "Show me as busy" which is the default.  Events that
 # are not opaque are ignored by default.
@@ -115,6 +125,7 @@ async def async_setup_entry(
 ) -> None:
     """Set up the google calendar platform."""
     calendar_service = hass.data[DOMAIN][config_entry.entry_id][DATA_SERVICE]
+    store = hass.data[DOMAIN][config_entry.entry_id][DATA_STORE]
     try:
         result = await calendar_service.async_list_calendars()
     except ApiException as err:
@@ -185,12 +196,20 @@ async def async_setup_entry(
                     entity_registry.async_remove(
                         entity_entry.entity_id,
                     )
+            request_template = SyncEventsRequest(
+                calendar_id=calendar_id,
+                search=data.get(CONF_SEARCH),
+                start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
+            )
+            sync = CalendarEventSyncManager(
+                calendar_service,
+                store=ScopedCalendarStore(store, unique_id or entity_name),
+                request_template=request_template,
+            )
             coordinator = CalendarUpdateCoordinator(
                 hass,
-                calendar_service,
+                sync,
                 data[CONF_NAME],
-                calendar_id,
-                data.get(CONF_SEARCH),
             )
             entities.append(
                 GoogleCalendarEntity(
@@ -203,7 +222,7 @@ async def async_setup_entry(
                 )
             )
 
-    async_add_entities(entities, True)
+    async_add_entities(entities)
 
     if calendars and new_calendars:
 
@@ -223,16 +242,14 @@ async def async_setup_entry(
         )
 
 
-class CalendarUpdateCoordinator(DataUpdateCoordinator):
+class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
     """Coordinator for calendar RPC calls."""
 
     def __init__(
         self,
         hass: HomeAssistant,
-        calendar_service: GoogleCalendarService,
+        sync: CalendarEventSyncManager,
         name: str,
-        calendar_id: str,
-        search: str | None,
     ) -> None:
         """Create the Calendar event device."""
         super().__init__(
@@ -241,38 +258,18 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator):
             name=name,
             update_interval=MIN_TIME_BETWEEN_UPDATES,
         )
-        self.calendar_service = calendar_service
-        self.calendar_id = calendar_id
-        self._search = search
-
-    async def async_get_events(
-        self, start_date: datetime, end_date: datetime
-    ) -> list[Event]:
-        """Get all events in a specific time frame."""
-        request = ListEventsRequest(
-            calendar_id=self.calendar_id,
-            start_time=start_date,
-            end_time=end_date,
-            search=self._search,
-        )
-        result_items = []
-        try:
-            result = await self.calendar_service.async_list_events(request)
-            async for result_page in result:
-                result_items.extend(result_page.items)
-        except ApiException as err:
-            self.async_set_update_error(err)
-            raise HomeAssistantError(str(err)) from err
-        return result_items
+        self.sync = sync
 
-    async def _async_update_data(self) -> list[Event]:
+    async def _async_update_data(self) -> Timeline:
         """Fetch data from API endpoint."""
-        request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search)
         try:
-            result = await self.calendar_service.async_list_events(request)
+            await self.sync.run()
         except ApiException as err:
             raise UpdateFailed(f"Error communicating with API: {err}") from err
-        return result.items
+
+        return await self.sync.store_service.async_get_timeline(
+            dt_util.DEFAULT_TIME_ZONE
+        )
 
 
 class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
@@ -341,16 +338,28 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
     async def async_added_to_hass(self) -> None:
         """When entity is added to hass."""
         await super().async_added_to_hass()
+
         # We do not ask for an update with async_add_entities()
-        # because it will update disabled entities
-        await self.coordinator.async_request_refresh()
-        self._apply_coordinator_update()
+        # because it will update disabled entities. This is started as a
+        # task to let if sync in the background without blocking startup
+        async def refresh() -> None:
+            await self.coordinator.async_request_refresh()
+            self._apply_coordinator_update()
+
+        asyncio.create_task(refresh())
 
     async def async_get_events(
         self, hass: HomeAssistant, start_date: datetime, end_date: datetime
     ) -> list[CalendarEvent]:
         """Get all events in a specific time frame."""
-        result_items = await self.coordinator.async_get_events(start_date, end_date)
+        if not (timeline := self.coordinator.data):
+            raise HomeAssistantError(
+                "Unable to get events: Sync from server has not completed"
+            )
+        result_items = timeline.overlapping(
+            dt_util.as_local(start_date),
+            dt_util.as_local(end_date),
+        )
         return [
             _get_calendar_event(event)
             for event in filter(self._event_filter, result_items)
@@ -358,13 +367,21 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
 
     def _apply_coordinator_update(self) -> None:
         """Copy state from the coordinator to this entity."""
-        events = self.coordinator.data
-        api_event = next(filter(self._event_filter, iter(events)), None)
-        self._event = _get_calendar_event(api_event) if api_event else None
-        if self._event:
+        if (timeline := self.coordinator.data) and (
+            api_event := next(
+                filter(
+                    self._event_filter,
+                    timeline.active_after(dt_util.now()),
+                ),
+                None,
+            )
+        ):
+            self._event = _get_calendar_event(api_event)
             (self._event.summary, self._offset_value) = extract_offset(
                 self._event.summary, self._offset
             )
+        else:
+            self._event = None
 
     @callback
     def _handle_coordinator_update(self) -> None:
@@ -432,7 +449,7 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) ->
         raise ValueError("Missing required fields to set start or end date/datetime")
 
     try:
-        await entity.coordinator.calendar_service.async_create_event(
+        await entity.coordinator.sync.api.async_create_event(
             entity.calendar_id,
             Event(
                 summary=call.data[EVENT_SUMMARY],
@@ -443,3 +460,4 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) ->
         )
     except ApiException as err:
         raise HomeAssistantError(str(err)) from err
+    entity.async_write_ha_state()
diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py
index f07958c2e6e..6a2c1974f66 100644
--- a/homeassistant/components/google/const.py
+++ b/homeassistant/components/google/const.py
@@ -10,6 +10,7 @@ CONF_CALENDAR_ACCESS = "calendar_access"
 DATA_CALENDARS = "calendars"
 DATA_SERVICE = "service"
 DATA_CONFIG = "config"
+DATA_STORE = "store"
 
 
 class FeatureAccess(Enum):
diff --git a/homeassistant/components/google/store.py b/homeassistant/components/google/store.py
new file mode 100644
index 00000000000..c4d9e4c3e9c
--- /dev/null
+++ b/homeassistant/components/google/store.py
@@ -0,0 +1,53 @@
+"""Google Calendar local storage."""
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from gcal_sync.store import CalendarStore
+
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.storage import Store
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+STORAGE_KEY_FORMAT = "{domain}.{entry_id}"
+STORAGE_VERSION = 1
+# Buffer writes every few minutes (plus guaranteed to be written at shutdown)
+STORAGE_SAVE_DELAY_SECONDS = 120
+
+
+class LocalCalendarStore(CalendarStore):
+    """Storage for local persistence of calendar and event data."""
+
+    def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
+        """Initialize LocalCalendarStore."""
+        self._store = Store[dict[str, Any]](
+            hass,
+            STORAGE_VERSION,
+            STORAGE_KEY_FORMAT.format(domain=DOMAIN, entry_id=entry_id),
+            private=True,
+        )
+        self._data: dict[str, Any] | None = None
+
+    async def async_load(self) -> dict[str, Any] | None:
+        """Load data."""
+        if self._data is None:
+            self._data = await self._store.async_load() or {}
+        return self._data
+
+    async def async_save(self, data: dict[str, Any]) -> None:
+        """Save data."""
+        self._data = data
+
+        def provide_data() -> dict:
+            return data
+
+        self._store.async_delay_save(provide_data, STORAGE_SAVE_DELAY_SECONDS)
+
+    async def async_remove(self) -> None:
+        """Remove data."""
+        await self._store.async_remove()
diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py
index e6b7c26ca84..2f5efd829bf 100644
--- a/tests/components/google/conftest.py
+++ b/tests/components/google/conftest.py
@@ -206,9 +206,13 @@ def mock_events_list(
     ) -> None:
         if calendar_id is None:
             calendar_id = CALENDAR_ID
+        resp = {
+            **response,
+            "nextSyncToken": "sync-token",
+        }
         aioclient_mock.get(
             f"{API_BASE_URL}/calendars/{calendar_id}/events",
-            json=response,
+            json=resp,
             exc=exc,
         )
         return
@@ -236,9 +240,13 @@ def mock_calendars_list(
     """Fixture to construct a fake calendar list API response."""
 
     def _result(response: dict[str, Any], exc: ClientError | None = None) -> None:
+        resp = {
+            **response,
+            "nextSyncToken": "sync-token",
+        }
         aioclient_mock.get(
             f"{API_BASE_URL}/users/me/calendarList",
-            json=response,
+            json=resp,
             exc=exc,
         )
         return
diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py
index 778e4a843eb..3bd584f4c6f 100644
--- a/tests/components/google/test_calendar.py
+++ b/tests/components/google/test_calendar.py
@@ -2,7 +2,6 @@
 
 from __future__ import annotations
 
-import copy
 import datetime
 from http import HTTPStatus
 from typing import Any
@@ -10,7 +9,6 @@ from unittest.mock import patch
 import urllib
 
 from aiohttp.client_exceptions import ClientError
-from gcal_sync.auth import API_BASE_URL
 import pytest
 
 from homeassistant.components.google.const import DOMAIN
@@ -28,7 +26,6 @@ from .conftest import (
 )
 
 from tests.common import async_fire_time_changed
-from tests.test_util.aiohttp import AiohttpClientMockResponse
 
 TEST_ENTITY = TEST_API_ENTITY
 TEST_ENTITY_NAME = TEST_API_ENTITY_NAME
@@ -76,6 +73,11 @@ def mock_test_setup(
     return
 
 
+def get_events_url(entity: str, start: str, end: str) -> str:
+    """Create a url to get events during the specified time range."""
+    return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}"
+
+
 def upcoming() -> dict[str, Any]:
     """Create a test event with an arbitrary start/end time fetched from the api url."""
     now = dt_util.now()
@@ -90,7 +92,7 @@ def upcoming_event_url(entity: str = TEST_ENTITY) -> str:
     now = dt_util.now()
     start = (now - datetime.timedelta(minutes=60)).isoformat()
     end = (now + datetime.timedelta(minutes=60)).isoformat()
-    return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}"
+    return get_events_url(entity, start, end)
 
 
 async def test_all_day_event(hass, mock_events_list_items, component_setup):
@@ -406,14 +408,12 @@ async def test_http_event_api_failure(
     aioclient_mock,
 ):
     """Test the Rest API response during a calendar failure."""
-    mock_events_list({})
+    mock_events_list({}, exc=ClientError())
+
     assert await component_setup()
 
     client = await hass_client()
 
-    aioclient_mock.clear_requests()
-    mock_events_list({}, exc=ClientError())
-
     response = await client.get(upcoming_event_url())
     assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR
 
@@ -472,66 +472,6 @@ async def test_http_api_all_day_event(
     }
 
 
-@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00")
-async def test_http_api_event_paging(
-    hass, hass_client, aioclient_mock, component_setup
-):
-    """Test paging through results from the server."""
-    hass.config.set_time_zone("Asia/Baghdad")
-
-    responses = [
-        {
-            "nextPageToken": "page-token",
-            "items": [
-                {
-                    **TEST_EVENT,
-                    "summary": "event 1",
-                    **upcoming(),
-                }
-            ],
-        },
-        {
-            "items": [
-                {
-                    **TEST_EVENT,
-                    "summary": "event 2",
-                    **upcoming(),
-                }
-            ],
-        },
-    ]
-
-    def next_response(response_list):
-        results = copy.copy(response_list)
-
-        async def get(method, url, data):
-            return AiohttpClientMockResponse(method, url, json=results.pop(0))
-
-        return get
-
-    # Setup response for initial entity load
-    aioclient_mock.get(
-        f"{API_BASE_URL}/calendars/{CALENDAR_ID}/events",
-        side_effect=next_response(responses),
-    )
-    assert await component_setup()
-
-    # Setup response for API request
-    aioclient_mock.clear_requests()
-    aioclient_mock.get(
-        f"{API_BASE_URL}/calendars/{CALENDAR_ID}/events",
-        side_effect=next_response(responses),
-    )
-
-    client = await hass_client()
-    response = await client.get(upcoming_event_url())
-    assert response.status == HTTPStatus.OK
-    events = await response.json()
-    assert len(events) == 2
-    assert events[0]["summary"] == "event 1"
-    assert events[1]["summary"] == "event 2"
-
-
 @pytest.mark.parametrize(
     "calendars_config_ignore_availability,transparency,expect_visible_event",
     [
@@ -781,3 +721,58 @@ async def test_invalid_unique_id_cleanup(
         entity_registry, config_entry.entry_id
     )
     assert not registry_entries
+
+
+@pytest.mark.parametrize(
+    "time_zone,event_order",
+    [
+        ("America/Los_Angeles", ["One", "Two", "All Day Event"]),
+        ("America/Regina", ["One", "Two", "All Day Event"]),
+        ("UTC", ["One", "All Day Event", "Two"]),
+        ("Asia/Tokyo", ["All Day Event", "One", "Two"]),
+    ],
+)
+async def test_all_day_iter_order(
+    hass,
+    hass_client,
+    mock_events_list_items,
+    component_setup,
+    time_zone,
+    event_order,
+):
+    """Test the sort order of an all day events depending on the time zone."""
+    hass.config.set_time_zone(time_zone)
+    mock_events_list_items(
+        [
+            {
+                **TEST_EVENT,
+                "id": "event-id-3",
+                "summary": "All Day Event",
+                "start": {"date": "2022-10-08"},
+                "end": {"date": "2022-10-09"},
+            },
+            {
+                **TEST_EVENT,
+                "id": "event-id-1",
+                "summary": "One",
+                "start": {"dateTime": "2022-10-07T23:00:00+00:00"},
+                "end": {"dateTime": "2022-10-07T23:30:00+00:00"},
+            },
+            {
+                **TEST_EVENT,
+                "id": "event-id-2",
+                "summary": "Two",
+                "start": {"dateTime": "2022-10-08T01:00:00+00:00"},
+                "end": {"dateTime": "2022-10-08T02:00:00+00:00"},
+            },
+        ]
+    )
+    assert await component_setup()
+
+    client = await hass_client()
+    response = await client.get(
+        get_events_url(TEST_ENTITY, "2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z")
+    )
+    assert response.status == HTTPStatus.OK
+    events = await response.json()
+    assert [event["summary"] for event in events] == event_order
diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py
index 613aa6dbb70..5e7696eec68 100644
--- a/tests/components/google/test_init.py
+++ b/tests/components/google/test_init.py
@@ -818,3 +818,24 @@ async def test_assign_unique_id_failure(
 
     assert config_entry.state is config_entry_status
     assert config_entry.unique_id is None
+
+
+async def test_remove_entry(
+    hass: HomeAssistant,
+    mock_calendars_list: ApiResult,
+    component_setup: ComponentSetup,
+    test_api_calendar: dict[str, Any],
+    mock_events_list: ApiResult,
+) -> None:
+    """Test load and remove of a ConfigEntry."""
+    mock_calendars_list({"items": [test_api_calendar]})
+    mock_events_list({})
+    assert await component_setup()
+
+    entries = hass.config_entries.async_entries(DOMAIN)
+    assert len(entries) == 1
+    entry = entries[0]
+    assert entry.state is ConfigEntryState.LOADED
+
+    assert await hass.config_entries.async_remove(entry.entry_id)
+    assert entry.state == ConfigEntryState.NOT_LOADED
-- 
GitLab


From abb3ce6d69d7d351fd668fe598efd068a7307cde Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Wed, 26 Oct 2022 10:06:56 -0500
Subject: [PATCH 857/985] Fix zeroconf starting later than expected (#81023)

---
 homeassistant/components/zeroconf/__init__.py | 11 ++++-------
 tests/components/zeroconf/test_init.py        | 13 ++++++++++++-
 2 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index 5a2fc61f897..211617bade3 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -22,11 +22,7 @@ from homeassistant import config_entries
 from homeassistant.components import network
 from homeassistant.components.network import MDNS_TARGET_IP, async_get_source_ip
 from homeassistant.components.network.models import Adapter
-from homeassistant.const import (
-    EVENT_HOMEASSISTANT_START,
-    EVENT_HOMEASSISTANT_STOP,
-    __version__,
-)
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__
 from homeassistant.core import Event, HomeAssistant, callback
 from homeassistant.data_entry_flow import BaseServiceInfo
 from homeassistant.helpers import discovery_flow, instance_id
@@ -40,6 +36,7 @@ from homeassistant.loader import (
     async_get_zeroconf,
     bind_hass,
 )
+from homeassistant.setup import async_when_setup_or_start
 
 from .models import HaAsyncServiceBrowser, HaAsyncZeroconf, HaZeroconf
 from .usage import install_multiple_zeroconf_catcher
@@ -194,7 +191,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
     discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models, ipv6)
     await discovery.async_setup()
 
-    async def _async_zeroconf_hass_start(_event: Event) -> None:
+    async def _async_zeroconf_hass_start(hass: HomeAssistant, comp: str) -> None:
         """Expose Home Assistant on zeroconf when it starts.
 
         Wait till started or otherwise HTTP is not up and running.
@@ -206,7 +203,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
         await discovery.async_stop()
 
     hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop)
-    hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start)
+    async_when_setup_or_start(hass, "frontend", _async_zeroconf_hass_start)
 
     return True
 
diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py
index 0de9929fcf8..87e4c129a46 100644
--- a/tests/components/zeroconf/test_init.py
+++ b/tests/components/zeroconf/test_init.py
@@ -13,12 +13,13 @@ from homeassistant.components.zeroconf import (
     _get_announced_addresses,
 )
 from homeassistant.const import (
+    EVENT_COMPONENT_LOADED,
     EVENT_HOMEASSISTANT_START,
     EVENT_HOMEASSISTANT_STARTED,
     EVENT_HOMEASSISTANT_STOP,
 )
 from homeassistant.generated import zeroconf as zc_gen
-from homeassistant.setup import async_setup_component
+from homeassistant.setup import ATTR_COMPONENT, async_setup_component
 
 NON_UTF8_VALUE = b"ABCDEF\x8a"
 NON_ASCII_KEY = b"non-ascii-key\x8a"
@@ -1159,3 +1160,13 @@ async def test_no_name(hass, mock_async_zeroconf):
     register_call = mock_async_zeroconf.async_register_service.mock_calls[-1]
     info = register_call.args[0]
     assert info.name == "Home._home-assistant._tcp.local."
+
+
+async def test_start_with_frontend(hass, mock_async_zeroconf):
+    """Test we start with the frontend."""
+    with patch("homeassistant.components.zeroconf.HaZeroconf"):
+        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
+        hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "frontend"})
+        await hass.async_block_till_done()
+
+    mock_async_zeroconf.async_register_service.assert_called_once()
-- 
GitLab


From a1a0284e818e75cbbb8d0d3ab4df10cc7761d355 Mon Sep 17 00:00:00 2001
From: Martin Hjelmare <marhje52@gmail.com>
Date: Wed, 26 Oct 2022 17:12:30 +0200
Subject: [PATCH 858/985] Fix zwave_js port enumeration (#81020)

---
 .../components/zwave_js/config_flow.py        | 19 ++++++++++++-------
 tests/components/zwave_js/test_config_flow.py |  7 ++++++-
 2 files changed, 18 insertions(+), 8 deletions(-)

diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py
index 378580160d1..0a084b3a309 100644
--- a/homeassistant/components/zwave_js/config_flow.py
+++ b/homeassistant/components/zwave_js/config_flow.py
@@ -126,15 +126,20 @@ def get_usb_ports() -> dict[str, str]:
     ports = list_ports.comports()
     port_descriptions = {}
     for port in ports:
-        usb_device = usb.usb_device_from_port(port)
-        dev_path = usb.get_serial_by_id(usb_device.device)
+        vid: str | None = None
+        pid: str | None = None
+        if port.vid is not None and port.pid is not None:
+            usb_device = usb.usb_device_from_port(port)
+            vid = usb_device.vid
+            pid = usb_device.pid
+        dev_path = usb.get_serial_by_id(port.device)
         human_name = usb.human_readable_device_name(
             dev_path,
-            usb_device.serial_number,
-            usb_device.manufacturer,
-            usb_device.description,
-            usb_device.vid,
-            usb_device.pid,
+            port.serial_number,
+            port.manufacturer,
+            port.description,
+            vid,
+            pid,
         )
         port_descriptions[dev_path] = human_name
     return port_descriptions
diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py
index 5e58ef25339..f58b4187469 100644
--- a/tests/components/zwave_js/test_config_flow.py
+++ b/tests/components/zwave_js/test_config_flow.py
@@ -162,7 +162,12 @@ def mock_list_ports_fixture(serial_port) -> Generator[MagicMock, None, None]:
         another_port.description = "New serial port"
         another_port.serial_number = "5678"
         another_port.pid = 8765
-        mock_list_ports.return_value = [serial_port, another_port]
+        no_vid_port = copy(serial_port)
+        no_vid_port.device = "/no_vid"
+        no_vid_port.description = "Port without vid"
+        no_vid_port.serial_number = "9123"
+        no_vid_port.vid = None
+        mock_list_ports.return_value = [serial_port, another_port, no_vid_port]
         yield mock_list_ports
 
 
-- 
GitLab


From 3eb574edca9f1bcf53e579c5f6828a9c830f302f Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 17:14:31 +0200
Subject: [PATCH 859/985] Remove YAML import from coinbase (#80084)

---
 homeassistant/components/coinbase/__init__.py | 48 +-------------
 .../components/coinbase/config_flow.py        | 43 +++---------
 homeassistant/components/coinbase/const.py    |  4 --
 .../components/coinbase/strings.json          |  6 --
 .../components/coinbase/translations/en.json  |  6 --
 tests/components/coinbase/test_config_flow.py | 65 -------------------
 tests/components/coinbase/test_init.py        | 34 ----------
 7 files changed, 13 insertions(+), 193 deletions(-)

diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py
index 49f62a751ab..ecba1900b64 100644
--- a/homeassistant/components/coinbase/__init__.py
+++ b/homeassistant/components/coinbase/__init__.py
@@ -6,14 +6,12 @@ import logging
 
 from coinbase.wallet.client import Client
 from coinbase.wallet.error import AuthenticationError
-import voluptuous as vol
 
-from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform
 from homeassistant.core import HomeAssistant
-from homeassistant.helpers import entity_registry, issue_registry as ir
+from homeassistant.helpers import entity_registry
 import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.typing import ConfigType
 from homeassistant.util import Throttle
 
 from .const import (
@@ -22,7 +20,6 @@ from .const import (
     CONF_CURRENCIES,
     CONF_EXCHANGE_BASE,
     CONF_EXCHANGE_RATES,
-    CONF_YAML_API_TOKEN,
     DOMAIN,
 )
 
@@ -32,46 +29,7 @@ PLATFORMS = [Platform.SENSOR]
 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
 
 
-CONFIG_SCHEMA = vol.Schema(
-    cv.deprecated(DOMAIN),
-    {
-        DOMAIN: vol.Schema(
-            {
-                vol.Required(CONF_API_KEY): cv.string,
-                vol.Required(CONF_YAML_API_TOKEN): cv.string,
-                vol.Optional(CONF_CURRENCIES): vol.All(cv.ensure_list, [cv.string]),
-                vol.Optional(CONF_EXCHANGE_RATES, default=[]): vol.All(
-                    cv.ensure_list, [cv.string]
-                ),
-            },
-        )
-    },
-    extra=vol.ALLOW_EXTRA,
-)
-
-
-async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
-    """Set up the Coinbase component."""
-    if DOMAIN not in config:
-        return True
-    ir.async_create_issue(
-        hass,
-        DOMAIN,
-        "remove_yaml",
-        breaks_in_ha_version="2022.12.0",
-        is_fixable=False,
-        severity=ir.IssueSeverity.WARNING,
-        translation_key="removed_yaml",
-    )
-    hass.async_create_task(
-        hass.config_entries.flow.async_init(
-            DOMAIN,
-            context={"source": SOURCE_IMPORT},
-            data=config[DOMAIN],
-        )
-    )
-
-    return True
+CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
 
 
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py
index 6582acc6549..5dc60f535d7 100644
--- a/homeassistant/components/coinbase/config_flow.py
+++ b/homeassistant/components/coinbase/config_flow.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import logging
+from typing import Any
 
 from coinbase.wallet.client import Client
 from coinbase.wallet.error import AuthenticationError
@@ -10,6 +11,7 @@ import voluptuous as vol
 from homeassistant import config_entries, core, exceptions
 from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
 from homeassistant.core import callback
+from homeassistant.data_entry_flow import FlowResult
 import homeassistant.helpers.config_validation as cv
 
 from . import get_accounts
@@ -23,8 +25,6 @@ from .const import (
     CONF_EXCHANGE_PRECISION,
     CONF_EXCHANGE_PRECISION_DEFAULT,
     CONF_EXCHANGE_RATES,
-    CONF_OPTIONS,
-    CONF_YAML_API_TOKEN,
     DOMAIN,
     RATES,
     WALLETS,
@@ -104,9 +104,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
 
     VERSION = 1
 
-    async def async_step_user(self, user_input=None):
+    async def async_step_user(
+        self, user_input: dict[str, str] | None = None
+    ) -> FlowResult:
         """Handle the initial step."""
-        errors = {}
+        errors: dict[str, str] = {}
         if user_input is None:
             return self.async_show_form(
                 step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -114,11 +116,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
 
         self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
 
-        options = {}
-
-        if CONF_OPTIONS in user_input:
-            options = user_input.pop(CONF_OPTIONS)
-
         try:
             info = await validate_api(self.hass, user_input)
         except CannotConnect:
@@ -133,33 +130,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
             _LOGGER.exception("Unexpected exception")
             errors["base"] = "unknown"
         else:
-            return self.async_create_entry(
-                title=info["title"], data=user_input, options=options
-            )
+            return self.async_create_entry(title=info["title"], data=user_input)
         return self.async_show_form(
             step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
         )
 
-    async def async_step_import(self, config):
-        """Handle import of Coinbase config from YAML."""
-
-        cleaned_data = {
-            CONF_API_KEY: config[CONF_API_KEY],
-            CONF_API_TOKEN: config[CONF_YAML_API_TOKEN],
-        }
-        cleaned_data[CONF_OPTIONS] = {
-            CONF_CURRENCIES: [],
-            CONF_EXCHANGE_RATES: [],
-        }
-        if CONF_CURRENCIES in config:
-            cleaned_data[CONF_OPTIONS][CONF_CURRENCIES] = config[CONF_CURRENCIES]
-        if CONF_EXCHANGE_RATES in config:
-            cleaned_data[CONF_OPTIONS][CONF_EXCHANGE_RATES] = config[
-                CONF_EXCHANGE_RATES
-            ]
-
-        return await self.async_step_user(user_input=cleaned_data)
-
     @staticmethod
     @callback
     def async_get_options_flow(
@@ -176,7 +151,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:
         """Manage the options."""
 
         errors = {}
diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py
index 48d4b1a6307..13415147ef9 100644
--- a/homeassistant/components/coinbase/const.py
+++ b/homeassistant/components/coinbase/const.py
@@ -5,13 +5,9 @@ CONF_EXCHANGE_BASE = "exchange_base"
 CONF_EXCHANGE_RATES = "exchange_rate_currencies"
 CONF_EXCHANGE_PRECISION = "exchange_rate_precision"
 CONF_EXCHANGE_PRECISION_DEFAULT = 2
-CONF_OPTIONS = "options"
 CONF_TITLE = "title"
 DOMAIN = "coinbase"
 
-# These are constants used by the previous YAML configuration
-CONF_YAML_API_TOKEN = "api_secret"
-
 # Constants for data returned by Coinbase API
 API_ACCOUNT_AMOUNT = "amount"
 API_ACCOUNT_BALANCE = "balance"
diff --git a/homeassistant/components/coinbase/strings.json b/homeassistant/components/coinbase/strings.json
index 6edd0f25338..96bf021e394 100644
--- a/homeassistant/components/coinbase/strings.json
+++ b/homeassistant/components/coinbase/strings.json
@@ -38,11 +38,5 @@
       "currency_unavailable": "One or more of the requested currency balances is not provided by your Coinbase API.",
       "exchange_rate_unavailable": "One or more of the requested exchange rates is not provided by Coinbase."
     }
-  },
-  "issues": {
-    "removed_yaml": {
-      "title": "The Coinbase YAML configuration has been removed",
-      "description": "Configuring Coinbase using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
-    }
   }
 }
diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json
index d755427d446..019159c8057 100644
--- a/homeassistant/components/coinbase/translations/en.json
+++ b/homeassistant/components/coinbase/translations/en.json
@@ -21,12 +21,6 @@
             }
         }
     },
-    "issues": {
-        "removed_yaml": {
-            "description": "Configuring Coinbase using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
-            "title": "The Coinbase YAML configuration has been removed"
-        }
-    },
     "options": {
         "error": {
             "currency_unavailable": "One or more of the requested currency balances is not provided by your Coinbase API.",
diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py
index b4927ca1b66..80d394c38ef 100644
--- a/tests/components/coinbase/test_config_flow.py
+++ b/tests/components/coinbase/test_config_flow.py
@@ -10,7 +10,6 @@ from homeassistant.components.coinbase.const import (
     CONF_CURRENCIES,
     CONF_EXCHANGE_PRECISION,
     CONF_EXCHANGE_RATES,
-    CONF_YAML_API_TOKEN,
     DOMAIN,
 )
 from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
@@ -23,8 +22,6 @@ from .common import (
 )
 from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHANGE_RATE
 
-from tests.common import MockConfigEntry
-
 
 async def test_form(hass):
     """Test we get the form."""
@@ -44,8 +41,6 @@ async def test_form(hass):
         "coinbase.wallet.client.Client.get_exchange_rates",
         return_value=mock_get_exchange_rates(),
     ), patch(
-        "homeassistant.components.coinbase.async_setup", return_value=True
-    ) as mock_setup, patch(
         "homeassistant.components.coinbase.async_setup_entry",
         return_value=True,
     ) as mock_setup_entry:
@@ -61,7 +56,6 @@ async def test_form(hass):
     assert result2["type"] == "create_entry"
     assert result2["title"] == "Test User"
     assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}
-    assert len(mock_setup.mock_calls) == 1
     assert len(mock_setup_entry.mock_calls) == 1
 
 
@@ -303,62 +297,3 @@ async def test_option_catch_all_exception(hass):
 
     assert result2["type"] == "form"
     assert result2["errors"] == {"base": "unknown"}
-
-
-async def test_yaml_import(hass):
-    """Test YAML import works."""
-    conf = {
-        CONF_API_KEY: "123456",
-        CONF_YAML_API_TOKEN: "AbCDeF",
-        CONF_CURRENCIES: ["BTC", "USD"],
-        CONF_EXCHANGE_RATES: ["ATOM", "BTC"],
-    }
-    with patch(
-        "coinbase.wallet.client.Client.get_current_user",
-        return_value=mock_get_current_user(),
-    ), patch(
-        "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
-    ), patch(
-        "coinbase.wallet.client.Client.get_exchange_rates",
-        return_value=mock_get_exchange_rates(),
-    ), patch(
-        "homeassistant.components.coinbase.async_setup", return_value=True
-    ) as mock_setup, patch(
-        "homeassistant.components.coinbase.async_setup_entry",
-        return_value=True,
-    ) as mock_setup_entry:
-        result = await hass.config_entries.flow.async_init(
-            DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
-        )
-    assert result["type"] == "create_entry"
-    assert result["title"] == "Test User"
-    assert result["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}
-    assert result["options"] == {
-        CONF_CURRENCIES: ["BTC", "USD"],
-        CONF_EXCHANGE_RATES: ["ATOM", "BTC"],
-    }
-    assert len(mock_setup.mock_calls) == 1
-    assert len(mock_setup_entry.mock_calls) == 1
-
-
-async def test_yaml_existing(hass):
-    """Test YAML ignored when already processed."""
-    MockConfigEntry(
-        domain=DOMAIN,
-        data={
-            CONF_API_KEY: "123456",
-            CONF_API_TOKEN: "AbCDeF",
-        },
-    ).add_to_hass(hass)
-
-    result = await hass.config_entries.flow.async_init(
-        DOMAIN,
-        context={"source": config_entries.SOURCE_IMPORT},
-        data={
-            CONF_API_KEY: "123456",
-            CONF_YAML_API_TOKEN: "AbCDeF",
-        },
-    )
-
-    assert result["type"] == "abort"
-    assert result["reason"] == "already_configured"
diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py
index efb5ba85f73..4f8538a5446 100644
--- a/tests/components/coinbase/test_init.py
+++ b/tests/components/coinbase/test_init.py
@@ -6,13 +6,10 @@ from homeassistant.components.coinbase.const import (
     API_TYPE_VAULT,
     CONF_CURRENCIES,
     CONF_EXCHANGE_RATES,
-    CONF_YAML_API_TOKEN,
     DOMAIN,
 )
-from homeassistant.const import CONF_API_KEY
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import entity_registry
-from homeassistant.setup import async_setup_component
 
 from .common import (
     init_mock_coinbase,
@@ -28,37 +25,6 @@ from .const import (
 )
 
 
-async def test_setup(hass):
-    """Test setting up from configuration.yaml."""
-    conf = {
-        DOMAIN: {
-            CONF_API_KEY: "123456",
-            CONF_YAML_API_TOKEN: "AbCDeF",
-            CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2],
-            CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2],
-        }
-    }
-    with patch(
-        "coinbase.wallet.client.Client.get_current_user",
-        return_value=mock_get_current_user(),
-    ), patch(
-        "coinbase.wallet.client.Client.get_accounts",
-        new=mocked_get_accounts,
-    ), patch(
-        "coinbase.wallet.client.Client.get_exchange_rates",
-        return_value=mock_get_exchange_rates(),
-    ):
-        assert await async_setup_component(hass, DOMAIN, conf)
-        entries = hass.config_entries.async_entries(DOMAIN)
-        assert len(entries) == 1
-        assert entries[0].title == "Test User"
-        assert entries[0].source == config_entries.SOURCE_IMPORT
-        assert entries[0].options == {
-            CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2],
-            CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2],
-        }
-
-
 async def test_unload_entry(hass):
     """Test successful unload of entry."""
     with patch(
-- 
GitLab


From 8645e47b07fd2ab3386012fe492fa9ab690830b5 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 17:28:23 +0200
Subject: [PATCH 860/985] Migrate power units to an enum (#81026)

---
 homeassistant/const.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 766d78e7ac6..7601dd05dd4 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -478,10 +478,22 @@ ATTR_PERSONS: Final = "persons"
 # Apparent power units
 POWER_VOLT_AMPERE: Final = "VA"
 
+
 # Power units
+class UnitOfPower(StrEnum):
+    """Power units."""
+
+    WATT = "W"
+    KILO_WATT = "kW"
+    BTU_PER_HOUR = "BTU/h"
+
+
 POWER_WATT: Final = "W"
+"""Deprecated: please use UnitOfPower.WATT."""
 POWER_KILO_WATT: Final = "kW"
+"""Deprecated: please use UnitOfPower.KILO_WATT."""
 POWER_BTU_PER_HOUR: Final = "BTU/h"
+"""Deprecated: please use UnitOfPower.BTU_PER_HOUR."""
 
 # Reactive power units
 POWER_VOLT_AMPERE_REACTIVE: Final = "var"
-- 
GitLab


From a72e906ac1b43c521bf8aa5ba5c6eceecf9c8810 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
Date: Wed, 26 Oct 2022 10:39:13 -0500
Subject: [PATCH 861/985] Fix zeroconf when location name has a period (#81022)

---
 homeassistant/components/zeroconf/__init__.py | 10 +++++++++-
 tests/components/zeroconf/test_init.py        | 19 +++++++++++++++++++
 2 files changed, 28 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index 211617bade3..62783e641d3 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -234,12 +234,20 @@ def _get_announced_addresses(
     return address_list
 
 
+def _filter_disallowed_characters(name: str) -> str:
+    """Filter disallowed characters from a string.
+
+    . is a reversed character for zeroconf.
+    """
+    return name.replace(".", " ")
+
+
 async def _async_register_hass_zc_service(
     hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str
 ) -> None:
     # Get instance UUID
     valid_location_name = _truncate_location_name_to_valid(
-        hass.config.location_name or "Home"
+        _filter_disallowed_characters(hass.config.location_name or "Home")
     )
 
     params = {
diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py
index 87e4c129a46..039672b9955 100644
--- a/tests/components/zeroconf/test_init.py
+++ b/tests/components/zeroconf/test_init.py
@@ -1162,6 +1162,25 @@ async def test_no_name(hass, mock_async_zeroconf):
     assert info.name == "Home._home-assistant._tcp.local."
 
 
+async def test_setup_with_disallowed_characters_in_local_name(
+    hass, mock_async_zeroconf, caplog
+):
+    """Test we still setup with disallowed characters in the location name."""
+    with patch.object(hass.config_entries.flow, "async_init"), patch.object(
+        zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock
+    ), patch.object(
+        hass.config,
+        "location_name",
+        "My.House",
+    ):
+        assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
+        hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+        await hass.async_block_till_done()
+
+    calls = mock_async_zeroconf.async_register_service.mock_calls
+    assert calls[0][1][0].name == "My House._home-assistant._tcp.local."
+
+
 async def test_start_with_frontend(hass, mock_async_zeroconf):
     """Test we start with the frontend."""
     with patch("homeassistant.components.zeroconf.HaZeroconf"):
-- 
GitLab


From 3eea61361c3adb3cfc835c673587a400f4689403 Mon Sep 17 00:00:00 2001
From: Maikel Punie <maikel.punie@gmail.com>
Date: Wed, 26 Oct 2022 17:42:15 +0200
Subject: [PATCH 862/985] Clean up velbus cache folder only on removal of the
 config entry (#81031)

---
 homeassistant/components/velbus/__init__.py | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py
index 585e093a42c..2ac751e283d 100644
--- a/homeassistant/components/velbus/__init__.py
+++ b/homeassistant/components/velbus/__init__.py
@@ -175,14 +175,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
 
 
 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
-    """Remove the velbus connection."""
+    """Unload (close) the velbus connection."""
     unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
     await hass.data[DOMAIN][entry.entry_id]["cntrl"].stop()
     hass.data[DOMAIN].pop(entry.entry_id)
-    await hass.async_add_executor_job(
-        shutil.rmtree,
-        hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
-    )
     if not hass.data[DOMAIN]:
         hass.data.pop(DOMAIN)
         hass.services.async_remove(DOMAIN, SERVICE_SCAN)
@@ -190,3 +186,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         hass.services.async_remove(DOMAIN, SERVICE_SET_MEMO_TEXT)
         hass.services.async_remove(DOMAIN, SERVICE_CLEAR_CACHE)
     return unload_ok
+
+
+async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
+    """Remove the velbus entry, so we also have to cleanup the cache dir."""
+    await hass.async_add_executor_job(
+        shutil.rmtree,
+        hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
+    )
-- 
GitLab


From a28b0e1b6ffb91f476cbf249832e2fa3fd5151f9 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 17:57:41 +0200
Subject: [PATCH 863/985] Migrate volume units to an enum (#81028)

* Migrate volume units to an enum

* Adjust docstring

* Deprecate -> Deprecated

* Plural
---
 homeassistant/const.py | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/homeassistant/const.py b/homeassistant/const.py
index 7601dd05dd4..112b1637c46 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -637,17 +637,38 @@ PRESSURE_PSI: Final = "psi"
 SOUND_PRESSURE_DB: Final = "dB"
 SOUND_PRESSURE_WEIGHTED_DBA: Final = "dBa"
 
+
 # Volume units
+class UnitOfVolume(StrEnum):
+    """Volume units."""
+
+    CUBIC_FEET = "ft³"
+    CUBIC_METERS = "m³"
+    LITERS = "L"
+    MILLILITERS = "mL"
+    GALLONS = "gal"
+    """Assumed to be US gallons in conversion utilities.
+
+    British/Imperial gallons are not yet supported"""
+    FLUID_OUNCES = "fl. oz."
+    """Assumed to be US fluid ounces in conversion utilities.
+
+    British/Imperial fluid ounces are not yet supported"""
+
+
 VOLUME_LITERS: Final = "L"
+"""Deprecated: please use UnitOfVolume.LITERS"""
 VOLUME_MILLILITERS: Final = "mL"
+"""Deprecated: please use UnitOfVolume.MILLILITERS"""
 VOLUME_CUBIC_METERS: Final = "m³"
+"""Deprecated: please use UnitOfVolume.CUBIC_METERS"""
 VOLUME_CUBIC_FEET: Final = "ft³"
+"""Deprecated: please use UnitOfVolume.CUBIC_FEET"""
 
 VOLUME_GALLONS: Final = "gal"
-"""US gallon (British gallon is not yet supported)"""
-
+"""Deprecated: please use UnitOfVolume.GALLONS"""
 VOLUME_FLUID_OUNCE: Final = "fl. oz."
-"""US fluid ounce (British fluid ounce is not yet supported)"""
+"""Deprecated: please use UnitOfVolume.FLUID_OUNCES"""
 
 # Volume Flow Rate units
 VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = "m³/h"
-- 
GitLab


From b148cdd64ad4465f6fc2ed0e7d4e9737262910bc Mon Sep 17 00:00:00 2001
From: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Date: Wed, 26 Oct 2022 18:19:54 +0200
Subject: [PATCH 864/985] Add integration type to fritzbox_callmointor
 integration (#81032)

---
 homeassistant/components/fritzbox_callmonitor/manifest.json | 1 +
 homeassistant/generated/integrations.json                   | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json
index 9dba2143ce1..79ca743bbeb 100644
--- a/homeassistant/components/fritzbox_callmonitor/manifest.json
+++ b/homeassistant/components/fritzbox_callmonitor/manifest.json
@@ -1,6 +1,7 @@
 {
   "domain": "fritzbox_callmonitor",
   "name": "AVM FRITZ!Box Call Monitor",
+  "integration_type": "device",
   "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor",
   "requirements": ["fritzconnection==1.10.3"],
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index f3b9fea78bf..e37fa473e81 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -1718,7 +1718,7 @@
           "name": "AVM FRITZ!SmartHome"
         },
         "fritzbox_callmonitor": {
-          "integration_type": "hub",
+          "integration_type": "device",
           "config_flow": true,
           "iot_class": "local_polling",
           "name": "AVM FRITZ!Box Call Monitor"
-- 
GitLab


From a1c18b06fbc42e61f4cb1b98fcba2492c85fe4db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Huryn?= <michalhuryn@gmail.com>
Date: Wed, 26 Oct 2022 18:20:49 +0200
Subject: [PATCH 865/985] Blebox typehints in binary sensor tests (#80676)

---
 tests/components/blebox/test_binary_sensor.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/tests/components/blebox/test_binary_sensor.py b/tests/components/blebox/test_binary_sensor.py
index 36258198d91..c9181762f3e 100644
--- a/tests/components/blebox/test_binary_sensor.py
+++ b/tests/components/blebox/test_binary_sensor.py
@@ -1,20 +1,21 @@
 """Blebox binary_sensor entities test."""
-from unittest.mock import PropertyMock
+from unittest.mock import AsyncMock, PropertyMock
 
 import blebox_uniapi
 import pytest
 
 from homeassistant.components.binary_sensor import BinarySensorDeviceClass
 from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON
+from homeassistant.core import HomeAssistant
 from homeassistant.helpers import device_registry as dr
 
 from .conftest import async_setup_entity, mock_feature
 
 
 @pytest.fixture(name="rainsensor")
-def airsensor_fixture():
+def airsensor_fixture() -> tuple[AsyncMock, str]:
     """Return a default air quality fixture."""
-    feature = mock_feature(
+    feature: AsyncMock = mock_feature(
         "binary_sensors",
         blebox_uniapi.binary_sensor.Rain,
         unique_id="BleBox-windRainSensor-ea68e74f4f49-0.rain",
@@ -24,10 +25,10 @@ def airsensor_fixture():
     product = feature.product
     type(product).name = PropertyMock(return_value="My rain sensor")
     type(product).model = PropertyMock(return_value="rainSensor")
-    return (feature, "binary_sensor.windrainsensor_0_rain")
+    return feature, "binary_sensor.windrainsensor_0_rain"
 
 
-async def test_init(rainsensor, hass, config):
+async def test_init(rainsensor: AsyncMock, hass: HomeAssistant, config: dict):
     """Test binary_sensor initialisation."""
     _, entity_id = rainsensor
     entry = await async_setup_entity(hass, config, entity_id)
-- 
GitLab


From b2b3c479174e3a646def776902e9c32fb8384921 Mon Sep 17 00:00:00 2001
From: Daniel Gangl <31815106+killer0071234@users.noreply.github.com>
Date: Wed, 26 Oct 2022 18:35:12 +0200
Subject: [PATCH 866/985] Add config flow to zamg (#66469)

---
 .coveragerc                                   |   3 +
 CODEOWNERS                                    |   2 +
 homeassistant/components/zamg/__init__.py     |  32 ++
 homeassistant/components/zamg/config_flow.py  | 136 +++++++
 homeassistant/components/zamg/const.py        |  26 ++
 homeassistant/components/zamg/coordinator.py  |  46 +++
 homeassistant/components/zamg/manifest.json   |   4 +-
 homeassistant/components/zamg/sensor.py       | 350 ++++++------------
 homeassistant/components/zamg/strings.json    |  26 ++
 .../components/zamg/translations/de.json      |  21 ++
 .../components/zamg/translations/en.json      |  26 ++
 homeassistant/components/zamg/weather.py      | 147 ++++----
 homeassistant/generated/config_flows.py       |   1 +
 homeassistant/generated/integrations.json     |   2 +-
 requirements_all.txt                          |   3 +
 requirements_test_all.txt                     |   3 +
 tests/components/zamg/__init__.py             |   1 +
 tests/components/zamg/conftest.py             |  95 +++++
 tests/components/zamg/fixtures/data.json      |   6 +
 tests/components/zamg/test_config_flow.py     | 194 ++++++++++
 20 files changed, 817 insertions(+), 307 deletions(-)
 create mode 100644 homeassistant/components/zamg/config_flow.py
 create mode 100644 homeassistant/components/zamg/const.py
 create mode 100644 homeassistant/components/zamg/coordinator.py
 create mode 100644 homeassistant/components/zamg/strings.json
 create mode 100644 homeassistant/components/zamg/translations/de.json
 create mode 100644 homeassistant/components/zamg/translations/en.json
 create mode 100644 tests/components/zamg/__init__.py
 create mode 100644 tests/components/zamg/conftest.py
 create mode 100644 tests/components/zamg/fixtures/data.json
 create mode 100644 tests/components/zamg/test_config_flow.py

diff --git a/.coveragerc b/.coveragerc
index 08607ace1f7..9d33acf6c75 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1582,6 +1582,9 @@ omit =
     homeassistant/components/youless/const.py
     homeassistant/components/youless/sensor.py
     homeassistant/components/zabbix/*
+    homeassistant/components/zamg/__init__.py
+    homeassistant/components/zamg/const.py
+    homeassistant/components/zamg/coordinator.py
     homeassistant/components/zamg/sensor.py
     homeassistant/components/zamg/weather.py
     homeassistant/components/zengge/light.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 35faab87e86..acc723a3493 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1318,6 +1318,8 @@ build.json @home-assistant/supervisor
 /tests/components/yolink/ @matrixd2
 /homeassistant/components/youless/ @gjong
 /tests/components/youless/ @gjong
+/homeassistant/components/zamg/ @killer0071234
+/tests/components/zamg/ @killer0071234
 /homeassistant/components/zengge/ @emontnemery
 /homeassistant/components/zeroconf/ @bdraco
 /tests/components/zeroconf/ @bdraco
diff --git a/homeassistant/components/zamg/__init__.py b/homeassistant/components/zamg/__init__.py
index a0f80956d98..67fe7521f95 100644
--- a/homeassistant/components/zamg/__init__.py
+++ b/homeassistant/components/zamg/__init__.py
@@ -1 +1,33 @@
 """The zamg component."""
+from __future__ import annotations
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+
+from .const import CONF_STATION_ID, DOMAIN
+from .coordinator import ZamgDataUpdateCoordinator
+
+PLATFORMS = (Platform.WEATHER, Platform.SENSOR)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up Zamg from config entry."""
+    coordinator = ZamgDataUpdateCoordinator(hass, entry=entry)
+    station_id = entry.data[CONF_STATION_ID]
+    coordinator.zamg.set_default_station(station_id)
+    await coordinator.async_config_entry_first_refresh()
+
+    hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
+
+    # Set up all platforms for this device/entry.
+    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload ZAMG 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/zamg/config_flow.py b/homeassistant/components/zamg/config_flow.py
new file mode 100644
index 00000000000..43434b1e8bd
--- /dev/null
+++ b/homeassistant/components/zamg/config_flow.py
@@ -0,0 +1,136 @@
+"""Config Flow for zamg the Austrian "Zentralanstalt für Meteorologie und Geodynamik" integration."""
+from __future__ import annotations
+
+from typing import Any
+
+import voluptuous as vol
+from zamg import ZamgData
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.data_entry_flow import FlowResult
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
+
+from .const import CONF_STATION_ID, DOMAIN, LOGGER
+
+
+class ZamgConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+    """Config flow for zamg integration."""
+
+    VERSION = 1
+
+    _client: ZamgData | None = None
+
+    async def async_step_user(
+        self, user_input: dict[str, Any] | None = None
+    ) -> FlowResult:
+        """Handle a flow initiated by the user."""
+        errors: dict[str, Any] = {}
+
+        if self._client is None:
+            self._client = ZamgData()
+            self._client.session = async_get_clientsession(self.hass)
+
+        if user_input is None:
+            closest_station_id = await self._client.closest_station(
+                self.hass.config.latitude,
+                self.hass.config.longitude,
+            )
+            LOGGER.debug("config_flow: closest station = %s", str(closest_station_id))
+            stations = await self._client.zamg_stations()
+            user_input = {}
+
+            schema = vol.Schema(
+                {
+                    vol.Required(
+                        CONF_STATION_ID, default=int(closest_station_id)
+                    ): vol.In(
+                        {
+                            int(station): f"{stations[station][2]} ({station})"
+                            for station in stations
+                        }
+                    )
+                }
+            )
+            return self.async_show_form(step_id="user", data_schema=schema)
+
+        station_id = str(user_input[CONF_STATION_ID])
+
+        # Check if already configured
+        await self.async_set_unique_id(station_id)
+        self._abort_if_unique_id_configured()
+
+        try:
+            self._client.set_default_station(station_id)
+            await self._client.update()
+        except (ValueError, TypeError) as err:
+            LOGGER.error("Config_flow: Received error from ZAMG: %s", err)
+            errors["base"] = "cannot_connect"
+            return self.async_abort(
+                reason="cannot_connect", description_placeholders=errors
+            )
+
+        return self.async_create_entry(
+            title=user_input.get(CONF_NAME) or self._client.get_station_name,
+            data={CONF_STATION_ID: station_id},
+        )
+
+    async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
+        """Handle ZAMG configuration import."""
+        station_id = str(config.get(CONF_STATION_ID))
+        station_name = config.get(CONF_NAME)
+        # create issue every time after restart
+        # parameter is_persistent seems not working
+        async_create_issue(
+            self.hass,
+            DOMAIN,
+            "deprecated_yaml",
+            breaks_in_ha_version="2023.1.0",
+            is_fixable=False,
+            severity=IssueSeverity.WARNING,
+            translation_key="deprecated_yaml",
+        )
+
+        for entry in self.hass.config_entries.async_entries(DOMAIN):
+            if station_id in entry.data[CONF_STATION_ID]:
+                return self.async_abort(
+                    reason="already_configured",
+                )
+
+        if self._client is None:
+            self._client = ZamgData()
+            self._client.session = async_get_clientsession(self.hass)
+
+        if station_id not in await self._client.zamg_stations():
+            LOGGER.warning(
+                "Configured station_id %s could not be found at zamg, adding the nearest weather station instead",
+                station_id,
+            )
+            latitude = config.get(CONF_LATITUDE) or self.hass.config.latitude
+            longitude = config.get(CONF_LONGITUDE) or self.hass.config.longitude
+            station_id = await self._client.closest_station(latitude, longitude)
+
+        if not station_name:
+            await self._client.zamg_stations()
+            self._client.set_default_station(station_id)
+            station_name = self._client.get_station_name
+
+        for entry in self.hass.config_entries.async_entries(DOMAIN):
+            if station_id in entry.data[CONF_STATION_ID]:
+                return self.async_abort(
+                    reason="already_configured",
+                )
+
+        LOGGER.debug(
+            "importing zamg station from configuration.yaml: station_id = %s, name = %s",
+            station_id,
+            station_name,
+        )
+
+        return await self.async_step_user(
+            user_input={
+                CONF_STATION_ID: int(station_id),
+                CONF_NAME: station_name,
+            }
+        )
diff --git a/homeassistant/components/zamg/const.py b/homeassistant/components/zamg/const.py
new file mode 100644
index 00000000000..08dfd1779ea
--- /dev/null
+++ b/homeassistant/components/zamg/const.py
@@ -0,0 +1,26 @@
+"""Constants for zamg the Austrian "Zentralanstalt für Meteorologie und Geodynamik" integration."""
+
+from datetime import timedelta
+import logging
+
+from homeassistant.const import Platform
+from homeassistant.util import dt as dt_util
+
+DOMAIN = "zamg"
+
+PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
+
+LOGGER = logging.getLogger(__package__)
+
+ATTR_STATION = "station"
+ATTR_UPDATED = "updated"
+ATTRIBUTION = "Data provided by ZAMG"
+
+CONF_STATION_ID = "station_id"
+
+DEFAULT_NAME = "zamg"
+
+MANUFACTURER_URL = "https://www.zamg.ac.at"
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
+VIENNA_TIME_ZONE = dt_util.get_time_zone("Europe/Vienna")
diff --git a/homeassistant/components/zamg/coordinator.py b/homeassistant/components/zamg/coordinator.py
new file mode 100644
index 00000000000..69113e8e23f
--- /dev/null
+++ b/homeassistant/components/zamg/coordinator.py
@@ -0,0 +1,46 @@
+"""Data Update coordinator for ZAMG weather data."""
+from __future__ import annotations
+
+from zamg import ZamgData as ZamgDevice
+
+from homeassistant.config_entries import ConfigEntry
+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 CONF_STATION_ID, DOMAIN, LOGGER, MIN_TIME_BETWEEN_UPDATES
+
+
+class ZamgDataUpdateCoordinator(DataUpdateCoordinator[ZamgDevice]):
+    """Class to manage fetching ZAMG weather data."""
+
+    config_entry: ConfigEntry
+    data: dict = {}
+
+    def __init__(
+        self,
+        hass: HomeAssistant,
+        *,
+        entry: ConfigEntry,
+    ) -> None:
+        """Initialize global ZAMG data updater."""
+        self.zamg = ZamgDevice(session=async_get_clientsession(hass))
+        self.zamg.set_default_station(entry.data[CONF_STATION_ID])
+        super().__init__(
+            hass,
+            LOGGER,
+            name=DOMAIN,
+            update_interval=MIN_TIME_BETWEEN_UPDATES,
+        )
+
+    async def _async_update_data(self) -> ZamgDevice:
+        """Fetch data from ZAMG api."""
+        try:
+            await self.zamg.zamg_stations()
+            device = await self.zamg.update()
+        except ValueError as error:
+            raise UpdateFailed(f"Invalid response from API: {error}") from error
+        self.data = device
+        self.data["last_update"] = self.zamg.last_update
+        self.data["Name"] = self.zamg.get_station_name
+        return device
diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json
index fc434514189..a6383ce8584 100644
--- a/homeassistant/components/zamg/manifest.json
+++ b/homeassistant/components/zamg/manifest.json
@@ -2,6 +2,8 @@
   "domain": "zamg",
   "name": "Zentralanstalt f\u00fcr Meteorologie und Geodynamik (ZAMG)",
   "documentation": "https://www.home-assistant.io/integrations/zamg",
-  "codeowners": [],
+  "requirements": ["zamg==0.1.1"],
+  "codeowners": ["@killer0071234"],
+  "config_flow": true,
   "iot_class": "cloud_polling"
 }
diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py
index 73902b1982f..e40f42abf0b 100644
--- a/homeassistant/components/zamg/sensor.py
+++ b/homeassistant/components/zamg/sensor.py
@@ -1,55 +1,51 @@
-"""Sensor for the Austrian "Zentralanstalt für Meteorologie und Geodynamik"."""
+"""Sensor for zamg the Austrian "Zentralanstalt für Meteorologie und Geodynamik" integration."""
 from __future__ import annotations
 
-import csv
+from collections.abc import Mapping
 from dataclasses import dataclass
-from datetime import datetime, timedelta
-import gzip
-import json
-import logging
-import os
 from typing import Union
 
-import requests
 import voluptuous as vol
 
 from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
     SensorEntityDescription,
+    SensorStateClass,
 )
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
 from homeassistant.const import (
-    AREA_SQUARE_METERS,
+    ATTR_ATTRIBUTION,
     CONF_LATITUDE,
     CONF_LONGITUDE,
     CONF_MONITORED_CONDITIONS,
     CONF_NAME,
     DEGREE,
-    LENGTH_METERS,
+    LENGTH_CENTIMETERS,
+    LENGTH_MILLIMETERS,
     PERCENTAGE,
     PRESSURE_HPA,
-    SPEED_KILOMETERS_PER_HOUR,
+    SPEED_METERS_PER_SECOND,
     TEMP_CELSIUS,
-    __version__,
+    TIME_SECONDS,
 )
 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.typing import ConfigType, DiscoveryInfoType
-from homeassistant.util import Throttle, dt as dt_util
-
-_LOGGER = logging.getLogger(__name__)
-
-ATTR_STATION = "station"
-ATTR_UPDATED = "updated"
-ATTRIBUTION = "Data provided by ZAMG"
-
-CONF_STATION_ID = "station_id"
-
-DEFAULT_NAME = "zamg"
-
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
-VIENNA_TIME_ZONE = dt_util.get_time_zone("Europe/Vienna")
+from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import (
+    ATTR_STATION,
+    ATTR_UPDATED,
+    ATTRIBUTION,
+    CONF_STATION_ID,
+    DEFAULT_NAME,
+    DOMAIN,
+    MANUFACTURER_URL,
+)
 
 _DType = Union[type[int], type[float], type[str]]
 
@@ -58,7 +54,7 @@ _DType = Union[type[int], type[float], type[str]]
 class ZamgRequiredKeysMixin:
     """Mixin for required keys."""
 
-    col_heading: str
+    para_name: str
     dtype: _DType
 
 
@@ -72,56 +68,67 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = (
         key="pressure",
         name="Pressure",
         native_unit_of_measurement=PRESSURE_HPA,
-        col_heading="LDstat hPa",
+        device_class=SensorDeviceClass.PRESSURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="P",
         dtype=float,
     ),
     ZamgSensorEntityDescription(
         key="pressure_sealevel",
         name="Pressure at Sea Level",
         native_unit_of_measurement=PRESSURE_HPA,
-        col_heading="LDred hPa",
+        device_class=SensorDeviceClass.PRESSURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="PRED",
         dtype=float,
     ),
     ZamgSensorEntityDescription(
         key="humidity",
         name="Humidity",
         native_unit_of_measurement=PERCENTAGE,
-        col_heading="RF %",
+        device_class=SensorDeviceClass.HUMIDITY,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="RFAM",
         dtype=int,
     ),
     ZamgSensorEntityDescription(
         key="wind_speed",
         name="Wind Speed",
-        native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
-        col_heading=f"WG {SPEED_KILOMETERS_PER_HOUR}",
+        native_unit_of_measurement=SPEED_METERS_PER_SECOND,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="FFAM",
         dtype=float,
     ),
     ZamgSensorEntityDescription(
         key="wind_bearing",
         name="Wind Bearing",
         native_unit_of_measurement=DEGREE,
-        col_heading=f"WR {DEGREE}",
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="DD",
         dtype=int,
     ),
     ZamgSensorEntityDescription(
         key="wind_max_speed",
         name="Top Wind Speed",
-        native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
-        col_heading=f"WSG {SPEED_KILOMETERS_PER_HOUR}",
+        native_unit_of_measurement=SPEED_METERS_PER_SECOND,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="FFX",
         dtype=float,
     ),
     ZamgSensorEntityDescription(
         key="wind_max_bearing",
         name="Top Wind Bearing",
         native_unit_of_measurement=DEGREE,
-        col_heading=f"WSR {DEGREE}",
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="DDX",
         dtype=int,
     ),
     ZamgSensorEntityDescription(
-        key="sun_last_hour",
-        name="Sun Last Hour",
-        native_unit_of_measurement=PERCENTAGE,
-        col_heading=f"SO {PERCENTAGE}",
+        key="sun_last_10min",
+        name="Sun Last 10 Minutes",
+        native_unit_of_measurement=TIME_SECONDS,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="SO",
         dtype=int,
     ),
     ZamgSensorEntityDescription(
@@ -129,14 +136,33 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = (
         name="Temperature",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
-        col_heading=f"T {TEMP_CELSIUS}",
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="TL",
+        dtype=float,
+    ),
+    ZamgSensorEntityDescription(
+        key="temperature_average",
+        name="Temperature Average",
+        native_unit_of_measurement=TEMP_CELSIUS,
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="TLAM",
         dtype=float,
     ),
     ZamgSensorEntityDescription(
         key="precipitation",
         name="Precipitation",
-        native_unit_of_measurement=f"l/{AREA_SQUARE_METERS}",
-        col_heading=f"N l/{AREA_SQUARE_METERS}",
+        native_unit_of_measurement=LENGTH_MILLIMETERS,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="RR",
+        dtype=float,
+    ),
+    ZamgSensorEntityDescription(
+        key="snow",
+        name="Snow",
+        native_unit_of_measurement=LENGTH_CENTIMETERS,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="SCHNEE",
         dtype=float,
     ),
     ZamgSensorEntityDescription(
@@ -144,42 +170,25 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = (
         name="Dew Point",
         native_unit_of_measurement=TEMP_CELSIUS,
         device_class=SensorDeviceClass.TEMPERATURE,
-        col_heading=f"TP {TEMP_CELSIUS}",
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="TP",
         dtype=float,
     ),
-    # The following probably not useful for general consumption,
-    # but we need them to fill in internal attributes
-    ZamgSensorEntityDescription(
-        key="station_name",
-        name="Station Name",
-        col_heading="Name",
-        dtype=str,
-    ),
     ZamgSensorEntityDescription(
-        key="station_elevation",
-        name="Station Elevation",
-        native_unit_of_measurement=LENGTH_METERS,
-        col_heading=f"Höhe {LENGTH_METERS}",
-        dtype=int,
-    ),
-    ZamgSensorEntityDescription(
-        key="update_date",
-        name="Update Date",
-        col_heading="Datum",
-        dtype=str,
-    ),
-    ZamgSensorEntityDescription(
-        key="update_time",
-        name="Update Time",
-        col_heading="Zeit",
-        dtype=str,
+        key="dewpoint_average",
+        name="Dew Point Average",
+        native_unit_of_measurement=TEMP_CELSIUS,
+        device_class=SensorDeviceClass.TEMPERATURE,
+        state_class=SensorStateClass.MEASUREMENT,
+        para_name="TPAM",
+        dtype=float,
     ),
 )
 
 SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
 
 API_FIELDS: dict[str, tuple[str, _DType]] = {
-    desc.col_heading: (desc.key, desc.dtype) for desc in SENSOR_TYPES
+    desc.para_name: (desc.key, desc.dtype) for desc in SENSOR_TYPES
 }
 
 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
@@ -199,187 +208,70 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
 )
 
 
-def setup_platform(
+async def async_setup_platform(
     hass: HomeAssistant,
     config: ConfigType,
-    add_entities: AddEntitiesCallback,
+    async_add_entities: AddEntitiesCallback,
     discovery_info: DiscoveryInfoType | None = None,
 ) -> None:
     """Set up the ZAMG sensor platform."""
-    name = config[CONF_NAME]
-    latitude = config.get(CONF_LATITUDE, hass.config.latitude)
-    longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
-
-    station_id = config.get(CONF_STATION_ID) or closest_station(
-        latitude, longitude, hass.config.config_dir
+    # trigger import flow
+    await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_IMPORT},
+        data=config,
     )
-    if station_id not in _get_ogd_stations():
-        _LOGGER.error(
-            "Configured ZAMG %s (%s) is not a known station",
-            CONF_STATION_ID,
-            station_id,
-        )
-        return
 
-    probe = ZamgData(station_id=station_id)
-    try:
-        probe.update()
-    except (ValueError, TypeError) as err:
-        _LOGGER.error("Received error from ZAMG: %s", err)
-        return
 
-    monitored_conditions = config[CONF_MONITORED_CONDITIONS]
-    add_entities(
-        [
-            ZamgSensor(probe, name, description)
-            for description in SENSOR_TYPES
-            if description.key in monitored_conditions
-        ],
-        True,
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+    """Set up the ZAMG sensor platform."""
+    coordinator = hass.data[DOMAIN][entry.entry_id]
+
+    async_add_entities(
+        ZamgSensor(coordinator, entry.title, entry.data[CONF_STATION_ID], description)
+        for description in SENSOR_TYPES
     )
 
 
-class ZamgSensor(SensorEntity):
+class ZamgSensor(CoordinatorEntity, SensorEntity):
     """Implementation of a ZAMG sensor."""
 
     _attr_attribution = ATTRIBUTION
     entity_description: ZamgSensorEntityDescription
 
-    def __init__(self, probe, name, description: ZamgSensorEntityDescription):
+    def __init__(
+        self, coordinator, name, station_id, description: ZamgSensorEntityDescription
+    ):
         """Initialize the sensor."""
+        super().__init__(coordinator)
         self.entity_description = description
-        self.probe = probe
-        self._attr_name = f"{name} {description.key}"
+        self._attr_name = f"{name} {description.name}"
+        self._attr_unique_id = f"{station_id}_{description.key}"
+        self.station_id = f"{station_id}"
+        self._attr_device_info = DeviceInfo(
+            entry_type=DeviceEntryType.SERVICE,
+            identifiers={(DOMAIN, station_id)},
+            manufacturer=ATTRIBUTION,
+            configuration_url=MANUFACTURER_URL,
+            name=coordinator.name,
+        )
 
     @property
-    def native_value(self):
+    def native_value(self) -> StateType:
         """Return the state of the sensor."""
-        return self.probe.get_data(self.entity_description.key)
+        return self.coordinator.data[self.station_id].get(
+            self.entity_description.para_name
+        )["data"]
 
     @property
-    def extra_state_attributes(self):
+    def extra_state_attributes(self) -> Mapping[str, str]:
         """Return the state attributes."""
+        update_time = self.coordinator.data.get("last_update", "")
         return {
-            ATTR_STATION: self.probe.get_data("station_name"),
-            ATTR_UPDATED: self.probe.last_update.isoformat(),
+            ATTR_ATTRIBUTION: ATTRIBUTION,
+            ATTR_STATION: self.coordinator.data.get("Name"),
+            CONF_STATION_ID: self.station_id,
+            ATTR_UPDATED: update_time.isoformat(),
         }
-
-    def update(self) -> None:
-        """Delegate update to probe."""
-        self.probe.update()
-
-
-class ZamgData:
-    """The class for handling the data retrieval."""
-
-    API_URL = "http://www.zamg.ac.at/ogd/"
-    API_HEADERS = {"User-Agent": f"home-assistant.zamg/ {__version__}"}
-
-    def __init__(self, station_id):
-        """Initialize the probe."""
-        self._station_id = station_id
-        self.data = {}
-
-    @property
-    def last_update(self):
-        """Return the timestamp of the most recent data."""
-        date, time = self.data.get("update_date"), self.data.get("update_time")
-        if date is not None and time is not None:
-            return datetime.strptime(date + time, "%d-%m-%Y%H:%M").replace(
-                tzinfo=VIENNA_TIME_ZONE
-            )
-
-    @classmethod
-    def current_observations(cls):
-        """Fetch the latest CSV data."""
-        try:
-            response = requests.get(cls.API_URL, headers=cls.API_HEADERS, timeout=15)
-            response.raise_for_status()
-            response.encoding = "UTF8"
-            return csv.DictReader(
-                response.text.splitlines(), delimiter=";", quotechar='"'
-            )
-        except requests.exceptions.HTTPError:
-            _LOGGER.error("While fetching data")
-
-    @Throttle(MIN_TIME_BETWEEN_UPDATES)
-    def update(self):
-        """Get the latest data from ZAMG."""
-        if self.last_update and (
-            self.last_update + timedelta(hours=1)
-            > datetime.utcnow().replace(tzinfo=dt_util.UTC)
-        ):
-            return  # Not time to update yet; data is only hourly
-
-        for row in self.current_observations():
-            if row.get("Station") == self._station_id:
-                self.data = {
-                    API_FIELDS[col_heading][0]: API_FIELDS[col_heading][1](
-                        v.replace(",", ".")
-                    )
-                    for col_heading, v in row.items()
-                    if col_heading in API_FIELDS and v
-                }
-                break
-        else:
-            raise ValueError(f"No weather data for station {self._station_id}")
-
-    def get_data(self, variable):
-        """Get the data."""
-        return self.data.get(variable)
-
-
-def _get_ogd_stations():
-    """Return all stations in the OGD dataset."""
-    return {r["Station"] for r in ZamgData.current_observations()}
-
-
-def _get_zamg_stations():
-    """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config."""
-    capital_stations = _get_ogd_stations()
-    req = requests.get(
-        "https://www.zamg.ac.at/cms/en/documents/climate/"
-        "doc_metnetwork/zamg-observation-points",
-        timeout=15,
-    )
-    stations = {}
-    for row in csv.DictReader(req.text.splitlines(), delimiter=";", quotechar='"'):
-        if row.get("synnr") in capital_stations:
-            try:
-                stations[row["synnr"]] = tuple(
-                    float(row[coord].replace(",", "."))
-                    for coord in ("breite_dezi", "länge_dezi")
-                )
-            except KeyError:
-                _LOGGER.error("ZAMG schema changed again, cannot autodetect station")
-    return stations
-
-
-def zamg_stations(cache_dir):
-    """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.
-
-    Results from internet requests are cached as compressed json, making
-    subsequent calls very much faster.
-    """
-    cache_file = os.path.join(cache_dir, ".zamg-stations.json.gz")
-    if not os.path.isfile(cache_file):
-        stations = _get_zamg_stations()
-        with gzip.open(cache_file, "wt") as cache:
-            json.dump(stations, cache, sort_keys=True)
-        return stations
-    with gzip.open(cache_file, "rt") as cache:
-        return {k: tuple(v) for k, v in json.load(cache).items()}
-
-
-def closest_station(lat, lon, cache_dir):
-    """Return the ZONE_ID.WMO_ID of the closest station to our lat/lon."""
-    if lat is None or lon is None or not os.path.isdir(cache_dir):
-        return
-    stations = zamg_stations(cache_dir)
-
-    def comparable_dist(zamg_id):
-        """Calculate the pseudo-distance from lat/lon."""
-        station_lat, station_lon = stations[zamg_id]
-        return (lat - station_lat) ** 2 + (lon - station_lon) ** 2
-
-    return min(stations, key=comparable_dist)
diff --git a/homeassistant/components/zamg/strings.json b/homeassistant/components/zamg/strings.json
new file mode 100644
index 00000000000..74b3c7c9fa2
--- /dev/null
+++ b/homeassistant/components/zamg/strings.json
@@ -0,0 +1,26 @@
+{
+  "config": {
+    "flow_title": "{name}",
+    "step": {
+      "user": {
+        "description": "Set up ZAMG to integrate with Home Assistant.",
+        "data": {
+          "station_id": "Station ID (Defaults to nearest station)"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+    }
+  },
+  "issues": {
+    "deprecated_yaml": {
+      "title": "The ZAMG YAML configuration is being removed",
+      "description": "Configuring ZAMG using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the ZAMG YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
+    }
+  }
+}
diff --git a/homeassistant/components/zamg/translations/de.json b/homeassistant/components/zamg/translations/de.json
new file mode 100644
index 00000000000..084d65de978
--- /dev/null
+++ b/homeassistant/components/zamg/translations/de.json
@@ -0,0 +1,21 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Wetterstation ist bereits konfiguriert",
+            "cannot_connect": "Verbindung fehlgeschlagen"
+        },
+        "error": {
+            "unknown": "ID der Wetterstation ist unbekannt",
+            "cannot_connect": "Verbindung fehlgeschlagen"
+        },
+        "flow_title": "{name}",
+        "step": {
+            "user": {
+                "data": {
+                    "station_id": "ID der Wetterstation (nächstgelegene Station as Defaultwert)"
+                },
+                "description": "Richte zamg f\u00fcr die Integration mit Home Assistant ein."
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zamg/translations/en.json b/homeassistant/components/zamg/translations/en.json
new file mode 100644
index 00000000000..6931f9f96f5
--- /dev/null
+++ b/homeassistant/components/zamg/translations/en.json
@@ -0,0 +1,26 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Device is already configured",
+            "cannot_connect": "Failed to connect"
+        },
+        "error": {
+            "cannot_connect": "Failed to connect"
+        },
+        "flow_title": "{name}",
+        "step": {
+            "user": {
+                "data": {
+                    "station_id": "Station ID (Defaults to nearest station)"
+                },
+                "description": "Set up ZAMG to integrate with Home Assistant."
+            }
+        }
+    },
+    "issues": {
+        "deprecated_yaml": {
+            "description": "Configuring ZAMG using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the ZAMG YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
+            "title": "The ZAMG YAML configuration is being removed"
+        }
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py
index eb2992df64f..b9d8ba67bbf 100644
--- a/homeassistant/components/zamg/weather.py
+++ b/homeassistant/components/zamg/weather.py
@@ -1,42 +1,29 @@
-"""Sensor for data from Austrian Zentralanstalt für Meteorologie."""
+"""Sensor for zamg the Austrian "Zentralanstalt für Meteorologie und Geodynamik" integration."""
 from __future__ import annotations
 
-import logging
-
 import voluptuous as vol
 
-from homeassistant.components.weather import (
-    ATTR_WEATHER_HUMIDITY,
-    ATTR_WEATHER_PRESSURE,
-    ATTR_WEATHER_TEMPERATURE,
-    ATTR_WEATHER_WIND_BEARING,
-    ATTR_WEATHER_WIND_SPEED,
-    PLATFORM_SCHEMA,
-    WeatherEntity,
-)
+from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
 from homeassistant.const import (
     CONF_LATITUDE,
     CONF_LONGITUDE,
     CONF_NAME,
+    LENGTH_MILLIMETERS,
     PRESSURE_HPA,
-    SPEED_KILOMETERS_PER_HOUR,
+    SPEED_METERS_PER_SECOND,
     TEMP_CELSIUS,
 )
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import 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.typing import ConfigType, DiscoveryInfoType
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
 
-# Reuse data and API logic from the sensor implementation
-from .sensor import (
-    ATTRIBUTION,
-    CONF_STATION_ID,
-    ZamgData,
-    closest_station,
-    zamg_stations,
-)
-
-_LOGGER = logging.getLogger(__name__)
+from .const import ATTRIBUTION, CONF_STATION_ID, DOMAIN, MANUFACTURER_URL
+from .coordinator import ZamgDataUpdateCoordinator
 
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
     {
@@ -52,93 +39,101 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
 )
 
 
-def setup_platform(
+async def async_setup_platform(
     hass: HomeAssistant,
     config: ConfigType,
-    add_entities: AddEntitiesCallback,
+    async_add_entities: AddEntitiesCallback,
     discovery_info: DiscoveryInfoType | None = None,
 ) -> None:
     """Set up the ZAMG weather platform."""
-    name = config.get(CONF_NAME)
-    latitude = config.get(CONF_LATITUDE, hass.config.latitude)
-    longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
-
-    station_id = config.get(CONF_STATION_ID) or closest_station(
-        latitude, longitude, hass.config.config_dir
+    # trigger import flow
+    await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_IMPORT},
+        data=config,
     )
-    if station_id not in zamg_stations(hass.config.config_dir):
-        _LOGGER.error(
-            "Configured ZAMG %s (%s) is not a known station",
-            CONF_STATION_ID,
-            station_id,
-        )
-        return
 
-    probe = ZamgData(station_id=station_id)
-    try:
-        probe.update()
-    except (ValueError, TypeError) as err:
-        _LOGGER.error("Received error from ZAMG: %s", err)
-        return
 
-    add_entities([ZamgWeather(probe, name)], True)
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
+) -> None:
+    """Set up the ZAMG weather platform."""
+    coordinator = hass.data[DOMAIN][entry.entry_id]
+    async_add_entities(
+        [ZamgWeather(coordinator, entry.title, entry.data[CONF_STATION_ID])]
+    )
 
 
-class ZamgWeather(WeatherEntity):
+class ZamgWeather(CoordinatorEntity, WeatherEntity):
     """Representation of a weather condition."""
 
-    _attr_native_pressure_unit = PRESSURE_HPA
-    _attr_native_temperature_unit = TEMP_CELSIUS
-    _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
-
-    def __init__(self, zamg_data, stationname=None):
+    def __init__(
+        self, coordinator: ZamgDataUpdateCoordinator, name, station_id
+    ) -> None:
         """Initialise the platform with a data instance and station name."""
-        self.zamg_data = zamg_data
-        self.stationname = stationname
-
-    @property
-    def name(self):
-        """Return the name of the sensor."""
-        return (
-            self.stationname
-            or f"ZAMG {self.zamg_data.data.get('Name') or '(unknown station)'}"
+        super().__init__(coordinator)
+        self._attr_unique_id = f"{name}_{station_id}"
+        self._attr_name = f"ZAMG {name}"
+        self.station_id = f"{station_id}"
+        self._attr_device_info = DeviceInfo(
+            entry_type=DeviceEntryType.SERVICE,
+            identifiers={(DOMAIN, station_id)},
+            manufacturer=ATTRIBUTION,
+            configuration_url=MANUFACTURER_URL,
+            name=coordinator.name,
         )
+        # set units of ZAMG API
+        self._attr_native_temperature_unit = TEMP_CELSIUS
+        self._attr_native_pressure_unit = PRESSURE_HPA
+        self._attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND
+        self._attr_native_precipitation_unit = LENGTH_MILLIMETERS
 
     @property
-    def condition(self):
+    def condition(self) -> str | None:
         """Return the current condition."""
         return None
 
     @property
-    def attribution(self):
+    def attribution(self) -> str | None:
         """Return the attribution."""
         return ATTRIBUTION
 
     @property
-    def native_temperature(self):
+    def native_temperature(self) -> float | None:
         """Return the platform temperature."""
-        return self.zamg_data.get_data(ATTR_WEATHER_TEMPERATURE)
+        try:
+            return float(self.coordinator.data[self.station_id].get("TL")["data"])
+        except (TypeError, ValueError):
+            return None
 
     @property
-    def native_pressure(self):
+    def native_pressure(self) -> float | None:
         """Return the pressure."""
-        return self.zamg_data.get_data(ATTR_WEATHER_PRESSURE)
+        try:
+            return float(self.coordinator.data[self.station_id].get("P")["data"])
+        except (TypeError, ValueError):
+            return None
 
     @property
-    def humidity(self):
+    def humidity(self) -> float | None:
         """Return the humidity."""
-        return self.zamg_data.get_data(ATTR_WEATHER_HUMIDITY)
+        try:
+            return float(self.coordinator.data[self.station_id].get("RFAM")["data"])
+        except (TypeError, ValueError):
+            return None
 
     @property
-    def native_wind_speed(self):
+    def native_wind_speed(self) -> float | None:
         """Return the wind speed."""
-        return self.zamg_data.get_data(ATTR_WEATHER_WIND_SPEED)
+        try:
+            return float(self.coordinator.data[self.station_id].get("FF")["data"])
+        except (TypeError, ValueError):
+            return None
 
     @property
-    def wind_bearing(self):
+    def wind_bearing(self) -> float | str | None:
         """Return the wind bearing."""
-        return self.zamg_data.get_data(ATTR_WEATHER_WIND_BEARING)
-
-    def update(self) -> None:
-        """Update current conditions."""
-        self.zamg_data.update()
+        try:
+            return self.coordinator.data[self.station_id].get("DD")["data"]
+        except (TypeError, ValueError):
+            return None
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 6cac800d7c8..772068401a5 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -461,6 +461,7 @@ FLOWS = {
         "yeelight",
         "yolink",
         "youless",
+        "zamg",
         "zerproc",
         "zha",
         "zwave_js",
diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json
index e37fa473e81..09a7a1f4a16 100644
--- a/homeassistant/generated/integrations.json
+++ b/homeassistant/generated/integrations.json
@@ -6093,7 +6093,7 @@
     "zamg": {
       "name": "Zentralanstalt f\u00fcr Meteorologie und Geodynamik (ZAMG)",
       "integration_type": "hub",
-      "config_flow": false,
+      "config_flow": true,
       "iot_class": "cloud_polling"
     },
     "zengge": {
diff --git a/requirements_all.txt b/requirements_all.txt
index 319e0dfbf84..a056ce0a3d0 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2597,6 +2597,9 @@ youless-api==0.16
 # homeassistant.components.media_extractor
 youtube_dl==2021.12.17
 
+# homeassistant.components.zamg
+zamg==0.1.1
+
 # homeassistant.components.zengge
 zengge==0.2
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 5269a91b646..965d184dc19 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1801,6 +1801,9 @@ yolink-api==0.1.0
 # homeassistant.components.youless
 youless-api==0.16
 
+# homeassistant.components.zamg
+zamg==0.1.1
+
 # homeassistant.components.zeroconf
 zeroconf==0.39.2
 
diff --git a/tests/components/zamg/__init__.py b/tests/components/zamg/__init__.py
new file mode 100644
index 00000000000..9c6415d7f84
--- /dev/null
+++ b/tests/components/zamg/__init__.py
@@ -0,0 +1 @@
+"""Tests for the ZAMG component."""
diff --git a/tests/components/zamg/conftest.py b/tests/components/zamg/conftest.py
new file mode 100644
index 00000000000..62ef191cb48
--- /dev/null
+++ b/tests/components/zamg/conftest.py
@@ -0,0 +1,95 @@
+"""Fixtures for Zamg integration tests."""
+from collections.abc import Generator
+import json
+from unittest.mock import MagicMock, patch
+
+import pytest
+from zamg import ZamgData as ZamgDevice
+
+from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN
+from homeassistant.core import HomeAssistant
+
+from tests.common import MockConfigEntry, load_fixture
+
+TEST_STATION_ID = "11240"
+TEST_STATION_NAME = "Graz/Flughafen"
+
+
+@pytest.fixture
+def mock_config_entry() -> MockConfigEntry:
+    """Return the default mocked config entry."""
+    return MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_STATION_ID: TEST_STATION_ID},
+        unique_id=TEST_STATION_ID,
+    )
+
+
+@pytest.fixture
+def mock_setup_entry() -> Generator[None, None, None]:
+    """Mock setting up a config entry."""
+    with patch("homeassistant.components.zamg.async_setup_entry", return_value=True):
+        yield
+
+
+@pytest.fixture
+def mock_zamg_config_flow(
+    request: pytest.FixtureRequest,
+) -> Generator[None, MagicMock, None]:
+    """Return a mocked Zamg client."""
+    with patch(
+        "homeassistant.components.zamg.sensor.ZamgData", autospec=True
+    ) as zamg_mock:
+        zamg = zamg_mock.return_value
+        zamg.update.return_value = ZamgDevice(
+            json.loads(load_fixture("zamg/data.json"))
+        )
+        zamg.get_data.return_value = zamg.get_data(TEST_STATION_ID)
+        yield zamg
+
+
+@pytest.fixture
+def mock_zamg(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
+    """Return a mocked Zamg client."""
+
+    with patch(
+        "homeassistant.components.zamg.config_flow.ZamgData", autospec=True
+    ) as zamg_mock:
+        zamg = zamg_mock.return_value
+        zamg.update.return_value = {TEST_STATION_ID: {"Name": TEST_STATION_NAME}}
+        zamg.zamg_stations.return_value = {
+            TEST_STATION_ID: (46.99305556, 15.43916667, TEST_STATION_NAME),
+            "11244": (46.8722229, 15.90361118, "BAD GLEICHENBERG"),
+        }
+        zamg.closest_station.return_value = TEST_STATION_ID
+        zamg.get_data.return_value = TEST_STATION_ID
+        zamg.get_station_name = TEST_STATION_NAME
+        yield zamg
+
+
+@pytest.fixture
+def mock_zamg_stations(
+    request: pytest.FixtureRequest,
+) -> Generator[None, MagicMock, None]:
+    """Return a mocked Zamg client."""
+    with patch(
+        "homeassistant.components.zamg.config_flow.ZamgData.zamg_stations"
+    ) as zamg_mock:
+        zamg_mock.return_value = {
+            "11240": (46.99305556, 15.43916667, "GRAZ-FLUGHAFEN"),
+            "11244": (46.87222222, 15.90361111, "BAD GLEICHENBERG"),
+        }
+        yield zamg_mock
+
+
+@pytest.fixture
+async def init_integration(
+    hass: HomeAssistant,
+) -> MockConfigEntry:
+    """Set up the Zamg 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/zamg/fixtures/data.json b/tests/components/zamg/fixtures/data.json
new file mode 100644
index 00000000000..2f0f3329b44
--- /dev/null
+++ b/tests/components/zamg/fixtures/data.json
@@ -0,0 +1,6 @@
+{
+  "data": {
+    "station_id": "11240",
+    "station_name": "Graz/Flughafen"
+  }
+}
diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py
new file mode 100644
index 00000000000..dc2eb62f1b9
--- /dev/null
+++ b/tests/components/zamg/test_config_flow.py
@@ -0,0 +1,194 @@
+"""Tests for the Zamg config flow."""
+from unittest.mock import MagicMock
+
+from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN, LOGGER
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+from homeassistant.const import CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResultType
+
+from .conftest import TEST_STATION_ID, TEST_STATION_NAME
+
+
+async def test_full_user_flow_implementation(
+    hass: HomeAssistant,
+    mock_zamg: MagicMock,
+    mock_setup_entry: None,
+) -> None:
+    """Test the full manual user flow from start to finish."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_USER},
+    )
+    assert result.get("step_id") == "user"
+    assert result.get("type") == FlowResultType.FORM
+    LOGGER.debug(result)
+    assert result.get("data_schema") != ""
+    assert "flow_id" in result
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+    )
+    assert result.get("type") == FlowResultType.CREATE_ENTRY
+    assert "data" in result
+    assert result["data"][CONF_STATION_ID] == TEST_STATION_ID
+    assert "result" in result
+    assert result["result"].unique_id == TEST_STATION_ID
+
+
+async def test_error_update(
+    hass: HomeAssistant,
+    mock_zamg: MagicMock,
+    mock_setup_entry: None,
+) -> None:
+    """Test with error of reading from Zamg."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_USER},
+    )
+    assert result.get("step_id") == "user"
+    assert result.get("type") == FlowResultType.FORM
+    LOGGER.debug(result)
+    assert result.get("data_schema") != ""
+    mock_zamg.update.side_effect = ValueError
+    assert "flow_id" in result
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+    )
+    assert result.get("type") == FlowResultType.ABORT
+    assert result.get("reason") == "cannot_connect"
+
+
+async def test_full_import_flow_implementation(
+    hass: HomeAssistant,
+    mock_zamg: MagicMock,
+    mock_setup_entry: None,
+) -> None:
+    """Test the full import flow from start to finish."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_IMPORT},
+        data={CONF_STATION_ID: TEST_STATION_ID, CONF_NAME: TEST_STATION_NAME},
+    )
+    assert result.get("type") == FlowResultType.CREATE_ENTRY
+    assert result.get("data") == {CONF_STATION_ID: TEST_STATION_ID}
+
+
+async def test_user_flow_duplicate(
+    hass: HomeAssistant,
+    mock_zamg: MagicMock,
+    mock_setup_entry: None,
+) -> None:
+    """Test the full manual user flow from start to finish."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_USER},
+    )
+
+    assert result.get("step_id") == "user"
+    assert result.get("type") == FlowResultType.FORM
+    assert "flow_id" in result
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+    )
+    assert result.get("type") == FlowResultType.CREATE_ENTRY
+    assert "data" in result
+    assert result["data"][CONF_STATION_ID] == TEST_STATION_ID
+    assert "result" in result
+    assert result["result"].unique_id == TEST_STATION_ID
+    # try to add another instance
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_USER},
+    )
+    assert result.get("step_id") == "user"
+    assert result.get("type") == FlowResultType.FORM
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+    )
+    assert result.get("type") == FlowResultType.ABORT
+    assert result.get("reason") == "already_configured"
+
+
+async def test_import_flow_duplicate(
+    hass: HomeAssistant,
+    mock_zamg: MagicMock,
+    mock_setup_entry: None,
+) -> None:
+    """Test import flow with duplicate entry."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_USER},
+    )
+
+    assert result.get("step_id") == "user"
+    assert result.get("type") == FlowResultType.FORM
+    assert "flow_id" in result
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+    )
+    assert result.get("type") == FlowResultType.CREATE_ENTRY
+    assert "data" in result
+    assert result["data"][CONF_STATION_ID] == TEST_STATION_ID
+    assert "result" in result
+    assert result["result"].unique_id == TEST_STATION_ID
+    # try to add another instance
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_IMPORT},
+        data={CONF_STATION_ID: TEST_STATION_ID, CONF_NAME: TEST_STATION_NAME},
+    )
+    assert result.get("type") == FlowResultType.ABORT
+    assert result.get("reason") == "already_configured"
+
+
+async def test_import_flow_duplicate_after_position(
+    hass: HomeAssistant,
+    mock_zamg: MagicMock,
+    mock_setup_entry: None,
+) -> None:
+    """Test import flow with duplicate entry."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_USER},
+    )
+
+    assert result.get("step_id") == "user"
+    assert result.get("type") == FlowResultType.FORM
+    assert "flow_id" in result
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+    )
+    assert result.get("type") == FlowResultType.CREATE_ENTRY
+    assert "data" in result
+    assert result["data"][CONF_STATION_ID] == TEST_STATION_ID
+    assert "result" in result
+    assert result["result"].unique_id == TEST_STATION_ID
+    # try to add another instance
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_IMPORT},
+        data={CONF_STATION_ID: "123", CONF_NAME: TEST_STATION_NAME},
+    )
+    assert result.get("type") == FlowResultType.ABORT
+    assert result.get("reason") == "already_configured"
+
+
+async def test_import_flow_no_name(
+    hass: HomeAssistant,
+    mock_zamg: MagicMock,
+    mock_setup_entry: None,
+) -> None:
+    """Test the full import flow from start to finish."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": SOURCE_IMPORT},
+        data={CONF_STATION_ID: TEST_STATION_ID},
+    )
+    assert result.get("type") == FlowResultType.CREATE_ENTRY
+    assert result.get("data") == {CONF_STATION_ID: TEST_STATION_ID}
-- 
GitLab


From 4da3fb5baa708b85f42387389902cc75cd46bc8f Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Wed, 26 Oct 2022 20:26:05 +0200
Subject: [PATCH 867/985] Correct Import of ReceivePayloadType (#81035)

---
 homeassistant/components/mqtt/mixins.py       | 8 ++++++--
 homeassistant/components/mysensors/gateway.py | 4 ++--
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py
index a91a7fc7a88..6b181f2e4b5 100644
--- a/homeassistant/components/mqtt/mixins.py
+++ b/homeassistant/components/mqtt/mixins.py
@@ -53,7 +53,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
 from homeassistant.helpers.event import async_track_entity_registry_updated_event
 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
 from homeassistant.helpers.json import json_loads
-from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
 
 from . import debug_info, subscription
@@ -82,7 +81,12 @@ from .discovery import (
     clear_discovery_hash,
     set_discovery_hash,
 )
-from .models import MqttValueTemplate, PublishPayloadType, ReceiveMessage
+from .models import (
+    MqttValueTemplate,
+    PublishPayloadType,
+    ReceiveMessage,
+    ReceivePayloadType,
+)
 from .subscription import (
     EntitySubscription,
     async_prepare_subscribe_topics,
diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py
index eace7b355d7..a920faf49fa 100644
--- a/homeassistant/components/mysensors/gateway.py
+++ b/homeassistant/components/mysensors/gateway.py
@@ -13,8 +13,8 @@ import async_timeout
 from mysensors import BaseAsyncGateway, Message, Sensor, mysensors
 import voluptuous as vol
 
-from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
-from homeassistant.components.mqtt.models import (
+from homeassistant.components.mqtt import (
+    DOMAIN as MQTT_DOMAIN,
     ReceiveMessage as MQTTReceiveMessage,
     ReceivePayloadType,
 )
-- 
GitLab


From 37cfa3e19be4daf1596acb3efe2659f44e93ab35 Mon Sep 17 00:00:00 2001
From: Felipe Santos <felipecassiors@gmail.com>
Date: Wed, 26 Oct 2022 15:37:31 -0300
Subject: [PATCH 868/985] Allow to cast in HLS format when using WebRTC
 (#80646)

---
 homeassistant/components/camera/media_source.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py
index e386e864ded..e681ddbbd7e 100644
--- a/homeassistant/components/camera/media_source.py
+++ b/homeassistant/components/camera/media_source.py
@@ -46,15 +46,17 @@ class CameraMediaSource(MediaSource):
                 f"/api/camera_proxy_stream/{camera.entity_id}", camera.content_type
             )
 
-        if stream_type != StreamType.HLS:
-            raise Unresolvable("Camera does not support MJPEG or HLS streaming.")
-
         if "stream" not in self.hass.config.components:
             raise Unresolvable("Stream integration not loaded")
 
         try:
             url = await _async_stream_endpoint_url(self.hass, camera, HLS_PROVIDER)
         except HomeAssistantError as err:
+            # Handle known error
+            if stream_type != StreamType.HLS:
+                raise Unresolvable(
+                    "Camera does not support MJPEG or HLS streaming."
+                ) from err
             raise Unresolvable(str(err)) from err
 
         return PlayMedia(url, FORMAT_CONTENT_TYPE[HLS_PROVIDER])
-- 
GitLab


From 2a2e097e174204e3710161898b4302e1bceca1e5 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Wed, 26 Oct 2022 20:47:17 +0200
Subject: [PATCH 869/985] Use unit enums in unit utilities (#81030)

---
 homeassistant/util/unit_conversion.py | 203 ++++----
 homeassistant/util/unit_system.py     | 150 +++---
 tests/components/flipr/test_sensor.py |   2 +-
 tests/util/test_unit_conversion.py    | 670 ++++++++++++++++----------
 tests/util/test_unit_system.py        | 256 +++++-----
 5 files changed, 690 insertions(+), 591 deletions(-)

diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py
index 1cf00230d1c..aa2782e423f 100644
--- a/homeassistant/util/unit_conversion.py
+++ b/homeassistant/util/unit_conversion.py
@@ -2,39 +2,6 @@
 from __future__ import annotations
 
 from homeassistant.const import (
-    LENGTH_CENTIMETERS,
-    LENGTH_FEET,
-    LENGTH_INCHES,
-    LENGTH_KILOMETERS,
-    LENGTH_METERS,
-    LENGTH_MILES,
-    LENGTH_MILLIMETERS,
-    LENGTH_YARD,
-    MASS_GRAMS,
-    MASS_KILOGRAMS,
-    MASS_MICROGRAMS,
-    MASS_MILLIGRAMS,
-    MASS_OUNCES,
-    MASS_POUNDS,
-    POWER_KILO_WATT,
-    POWER_WATT,
-    PRESSURE_BAR,
-    PRESSURE_CBAR,
-    PRESSURE_HPA,
-    PRESSURE_INHG,
-    PRESSURE_KPA,
-    PRESSURE_MBAR,
-    PRESSURE_MMHG,
-    PRESSURE_PA,
-    PRESSURE_PSI,
-    SPEED_FEET_PER_SECOND,
-    SPEED_KILOMETERS_PER_HOUR,
-    SPEED_KNOTS,
-    SPEED_METERS_PER_SECOND,
-    SPEED_MILES_PER_HOUR,
-    TEMP_CELSIUS,
-    TEMP_FAHRENHEIT,
-    TEMP_KELVIN,
     UNIT_NOT_RECOGNIZED_TEMPLATE,
     VOLUME_CUBIC_FEET,
     VOLUME_CUBIC_METERS,
@@ -43,6 +10,12 @@ from homeassistant.const import (
     VOLUME_LITERS,
     VOLUME_MILLILITERS,
     UnitOfEnergy,
+    UnitOfLength,
+    UnitOfMass,
+    UnitOfPower,
+    UnitOfPressure,
+    UnitOfSpeed,
+    UnitOfTemperature,
     UnitOfVolumetricFlux,
 )
 from homeassistant.exceptions import HomeAssistantError
@@ -121,26 +94,26 @@ class DistanceConverter(BaseUnitConverter):
     """Utility to convert distance values."""
 
     UNIT_CLASS = "distance"
-    NORMALIZED_UNIT = LENGTH_METERS
+    NORMALIZED_UNIT = UnitOfLength.METERS
     _UNIT_CONVERSION: dict[str, float] = {
-        LENGTH_METERS: 1,
-        LENGTH_MILLIMETERS: 1 / _MM_TO_M,
-        LENGTH_CENTIMETERS: 1 / _CM_TO_M,
-        LENGTH_KILOMETERS: 1 / _KM_TO_M,
-        LENGTH_INCHES: 1 / _IN_TO_M,
-        LENGTH_FEET: 1 / _FOOT_TO_M,
-        LENGTH_YARD: 1 / _YARD_TO_M,
-        LENGTH_MILES: 1 / _MILE_TO_M,
+        UnitOfLength.METERS: 1,
+        UnitOfLength.MILLIMETERS: 1 / _MM_TO_M,
+        UnitOfLength.CENTIMETERS: 1 / _CM_TO_M,
+        UnitOfLength.KILOMETERS: 1 / _KM_TO_M,
+        UnitOfLength.INCHES: 1 / _IN_TO_M,
+        UnitOfLength.FEET: 1 / _FOOT_TO_M,
+        UnitOfLength.YARDS: 1 / _YARD_TO_M,
+        UnitOfLength.MILES: 1 / _MILE_TO_M,
     }
     VALID_UNITS = {
-        LENGTH_KILOMETERS,
-        LENGTH_MILES,
-        LENGTH_FEET,
-        LENGTH_METERS,
-        LENGTH_CENTIMETERS,
-        LENGTH_MILLIMETERS,
-        LENGTH_INCHES,
-        LENGTH_YARD,
+        UnitOfLength.KILOMETERS,
+        UnitOfLength.MILES,
+        UnitOfLength.FEET,
+        UnitOfLength.METERS,
+        UnitOfLength.CENTIMETERS,
+        UnitOfLength.MILLIMETERS,
+        UnitOfLength.INCHES,
+        UnitOfLength.YARDS,
     }
 
 
@@ -167,22 +140,22 @@ class MassConverter(BaseUnitConverter):
     """Utility to convert mass values."""
 
     UNIT_CLASS = "mass"
-    NORMALIZED_UNIT = MASS_GRAMS
+    NORMALIZED_UNIT = UnitOfMass.GRAMS
     _UNIT_CONVERSION: dict[str, float] = {
-        MASS_MICROGRAMS: 1 * 1000 * 1000,
-        MASS_MILLIGRAMS: 1 * 1000,
-        MASS_GRAMS: 1,
-        MASS_KILOGRAMS: 1 / 1000,
-        MASS_OUNCES: 1 / _OUNCE_TO_G,
-        MASS_POUNDS: 1 / _POUND_TO_G,
+        UnitOfMass.MICROGRAMS: 1 * 1000 * 1000,
+        UnitOfMass.MILLIGRAMS: 1 * 1000,
+        UnitOfMass.GRAMS: 1,
+        UnitOfMass.KILOGRAMS: 1 / 1000,
+        UnitOfMass.OUNCES: 1 / _OUNCE_TO_G,
+        UnitOfMass.POUNDS: 1 / _POUND_TO_G,
     }
     VALID_UNITS = {
-        MASS_GRAMS,
-        MASS_KILOGRAMS,
-        MASS_MILLIGRAMS,
-        MASS_MICROGRAMS,
-        MASS_OUNCES,
-        MASS_POUNDS,
+        UnitOfMass.GRAMS,
+        UnitOfMass.KILOGRAMS,
+        UnitOfMass.MILLIGRAMS,
+        UnitOfMass.MICROGRAMS,
+        UnitOfMass.OUNCES,
+        UnitOfMass.POUNDS,
     }
 
 
@@ -190,14 +163,14 @@ class PowerConverter(BaseUnitConverter):
     """Utility to convert power values."""
 
     UNIT_CLASS = "power"
-    NORMALIZED_UNIT = POWER_WATT
+    NORMALIZED_UNIT = UnitOfPower.WATT
     _UNIT_CONVERSION: dict[str, float] = {
-        POWER_WATT: 1,
-        POWER_KILO_WATT: 1 / 1000,
+        UnitOfPower.WATT: 1,
+        UnitOfPower.KILO_WATT: 1 / 1000,
     }
     VALID_UNITS = {
-        POWER_WATT,
-        POWER_KILO_WATT,
+        UnitOfPower.WATT,
+        UnitOfPower.KILO_WATT,
     }
 
 
@@ -205,28 +178,30 @@ class PressureConverter(BaseUnitConverter):
     """Utility to convert pressure values."""
 
     UNIT_CLASS = "pressure"
-    NORMALIZED_UNIT = PRESSURE_PA
+    NORMALIZED_UNIT = UnitOfPressure.PA
     _UNIT_CONVERSION: dict[str, float] = {
-        PRESSURE_PA: 1,
-        PRESSURE_HPA: 1 / 100,
-        PRESSURE_KPA: 1 / 1000,
-        PRESSURE_BAR: 1 / 100000,
-        PRESSURE_CBAR: 1 / 1000,
-        PRESSURE_MBAR: 1 / 100,
-        PRESSURE_INHG: 1 / (_IN_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
-        PRESSURE_PSI: 1 / 6894.757,
-        PRESSURE_MMHG: 1 / (_MM_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
+        UnitOfPressure.PA: 1,
+        UnitOfPressure.HPA: 1 / 100,
+        UnitOfPressure.KPA: 1 / 1000,
+        UnitOfPressure.BAR: 1 / 100000,
+        UnitOfPressure.CBAR: 1 / 1000,
+        UnitOfPressure.MBAR: 1 / 100,
+        UnitOfPressure.INHG: 1
+        / (_IN_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
+        UnitOfPressure.PSI: 1 / 6894.757,
+        UnitOfPressure.MMHG: 1
+        / (_MM_TO_M * 1000 * _STANDARD_GRAVITY * _MERCURY_DENSITY),
     }
     VALID_UNITS = {
-        PRESSURE_PA,
-        PRESSURE_HPA,
-        PRESSURE_KPA,
-        PRESSURE_BAR,
-        PRESSURE_CBAR,
-        PRESSURE_MBAR,
-        PRESSURE_INHG,
-        PRESSURE_PSI,
-        PRESSURE_MMHG,
+        UnitOfPressure.PA,
+        UnitOfPressure.HPA,
+        UnitOfPressure.KPA,
+        UnitOfPressure.BAR,
+        UnitOfPressure.CBAR,
+        UnitOfPressure.MBAR,
+        UnitOfPressure.INHG,
+        UnitOfPressure.PSI,
+        UnitOfPressure.MMHG,
     }
 
 
@@ -234,28 +209,28 @@ class SpeedConverter(BaseUnitConverter):
     """Utility to convert speed values."""
 
     UNIT_CLASS = "speed"
-    NORMALIZED_UNIT = SPEED_METERS_PER_SECOND
+    NORMALIZED_UNIT = UnitOfSpeed.METERS_PER_SECOND
     _UNIT_CONVERSION: dict[str, float] = {
         UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
         UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
         UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
         UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: _HRS_TO_SECS / _MM_TO_M,
-        SPEED_FEET_PER_SECOND: 1 / _FOOT_TO_M,
-        SPEED_KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M,
-        SPEED_KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M,
-        SPEED_METERS_PER_SECOND: 1,
-        SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M,
+        UnitOfSpeed.FEET_PER_SECOND: 1 / _FOOT_TO_M,
+        UnitOfSpeed.KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M,
+        UnitOfSpeed.KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M,
+        UnitOfSpeed.METERS_PER_SECOND: 1,
+        UnitOfSpeed.MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M,
     }
     VALID_UNITS = {
         UnitOfVolumetricFlux.INCHES_PER_DAY,
         UnitOfVolumetricFlux.INCHES_PER_HOUR,
         UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
         UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
-        SPEED_FEET_PER_SECOND,
-        SPEED_KILOMETERS_PER_HOUR,
-        SPEED_KNOTS,
-        SPEED_METERS_PER_SECOND,
-        SPEED_MILES_PER_HOUR,
+        UnitOfSpeed.FEET_PER_SECOND,
+        UnitOfSpeed.KILOMETERS_PER_HOUR,
+        UnitOfSpeed.KNOTS,
+        UnitOfSpeed.METERS_PER_SECOND,
+        UnitOfSpeed.MILES_PER_HOUR,
     }
 
 
@@ -263,16 +238,16 @@ class TemperatureConverter(BaseUnitConverter):
     """Utility to convert temperature values."""
 
     UNIT_CLASS = "temperature"
-    NORMALIZED_UNIT = TEMP_CELSIUS
+    NORMALIZED_UNIT = UnitOfTemperature.CELSIUS
     VALID_UNITS = {
-        TEMP_CELSIUS,
-        TEMP_FAHRENHEIT,
-        TEMP_KELVIN,
+        UnitOfTemperature.CELSIUS,
+        UnitOfTemperature.FAHRENHEIT,
+        UnitOfTemperature.KELVIN,
     }
     _UNIT_CONVERSION = {
-        TEMP_CELSIUS: 1.0,
-        TEMP_FAHRENHEIT: 1.8,
-        TEMP_KELVIN: 1.0,
+        UnitOfTemperature.CELSIUS: 1.0,
+        UnitOfTemperature.FAHRENHEIT: 1.8,
+        UnitOfTemperature.KELVIN: 1.0,
     }
 
     @classmethod
@@ -289,28 +264,28 @@ class TemperatureConverter(BaseUnitConverter):
         if from_unit == to_unit:
             return value
 
-        if from_unit == TEMP_CELSIUS:
-            if to_unit == TEMP_FAHRENHEIT:
+        if from_unit == UnitOfTemperature.CELSIUS:
+            if to_unit == UnitOfTemperature.FAHRENHEIT:
                 return cls._celsius_to_fahrenheit(value)
-            if to_unit == TEMP_KELVIN:
+            if to_unit == UnitOfTemperature.KELVIN:
                 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:
+        if from_unit == UnitOfTemperature.FAHRENHEIT:
+            if to_unit == UnitOfTemperature.CELSIUS:
                 return cls._fahrenheit_to_celsius(value)
-            if to_unit == TEMP_KELVIN:
+            if to_unit == UnitOfTemperature.KELVIN:
                 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:
+        if from_unit == UnitOfTemperature.KELVIN:
+            if to_unit == UnitOfTemperature.CELSIUS:
                 return cls._kelvin_to_celsius(value)
-            if to_unit == TEMP_FAHRENHEIT:
+            if to_unit == UnitOfTemperature.FAHRENHEIT:
                 return cls._celsius_to_fahrenheit(cls._kelvin_to_celsius(value))
             raise HomeAssistantError(
                 UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS)
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 42cf429650a..7e338f8f313 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -9,40 +9,18 @@ import voluptuous as vol
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
     LENGTH,
-    LENGTH_CENTIMETERS,
-    LENGTH_FEET,
-    LENGTH_INCHES,
-    LENGTH_KILOMETERS,
-    LENGTH_METERS,
-    LENGTH_MILES,
-    LENGTH_MILLIMETERS,
-    LENGTH_YARD,
     MASS,
-    MASS_GRAMS,
-    MASS_KILOGRAMS,
-    MASS_OUNCES,
-    MASS_POUNDS,
-    PRECIPITATION_INCHES,
-    PRECIPITATION_MILLIMETERS,
     PRESSURE,
-    PRESSURE_PA,
-    PRESSURE_PSI,
-    SPEED_FEET_PER_SECOND,
-    SPEED_KILOMETERS_PER_HOUR,
-    SPEED_METERS_PER_SECOND,
-    SPEED_MILES_PER_HOUR,
-    TEMP_CELSIUS,
-    TEMP_FAHRENHEIT,
     TEMPERATURE,
     UNIT_NOT_RECOGNIZED_TEMPLATE,
     VOLUME,
-    VOLUME_CUBIC_FEET,
-    VOLUME_CUBIC_METERS,
-    VOLUME_FLUID_OUNCE,
-    VOLUME_GALLONS,
-    VOLUME_LITERS,
-    VOLUME_MILLILITERS,
     WIND_SPEED,
+    UnitOfLength,
+    UnitOfMass,
+    UnitOfPressure,
+    UnitOfSpeed,
+    UnitOfTemperature,
+    UnitOfVolume,
 )
 from homeassistant.helpers.frame import report
 
@@ -63,7 +41,12 @@ _CONF_UNIT_SYSTEM_US_CUSTOMARY: Final = "us_customary"
 
 LENGTH_UNITS = DistanceConverter.VALID_UNITS
 
-MASS_UNITS: set[str] = {MASS_POUNDS, MASS_OUNCES, MASS_KILOGRAMS, MASS_GRAMS}
+MASS_UNITS: set[str] = {
+    UnitOfMass.POUNDS,
+    UnitOfMass.OUNCES,
+    UnitOfMass.KILOGRAMS,
+    UnitOfMass.GRAMS,
+}
 
 PRESSURE_UNITS = PressureConverter.VALID_UNITS
 
@@ -71,29 +54,26 @@ VOLUME_UNITS = VolumeConverter.VALID_UNITS
 
 WIND_SPEED_UNITS = SpeedConverter.VALID_UNITS
 
-TEMPERATURE_UNITS: set[str] = {TEMP_FAHRENHEIT, TEMP_CELSIUS}
+TEMPERATURE_UNITS: set[str] = {UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS}
 
 
 def _is_valid_unit(unit: str, unit_type: str) -> bool:
     """Check if the unit is valid for it's type."""
     if unit_type == LENGTH:
-        units = LENGTH_UNITS
-    elif unit_type == ACCUMULATED_PRECIPITATION:
-        units = LENGTH_UNITS
-    elif unit_type == WIND_SPEED:
-        units = WIND_SPEED_UNITS
-    elif unit_type == TEMPERATURE:
-        units = TEMPERATURE_UNITS
-    elif unit_type == MASS:
-        units = MASS_UNITS
-    elif unit_type == VOLUME:
-        units = VOLUME_UNITS
-    elif unit_type == PRESSURE:
-        units = PRESSURE_UNITS
-    else:
-        return False
-
-    return unit in units
+        return unit in LENGTH_UNITS
+    if unit_type == ACCUMULATED_PRECIPITATION:
+        return unit in LENGTH_UNITS
+    if unit_type == WIND_SPEED:
+        return unit in WIND_SPEED_UNITS
+    if unit_type == TEMPERATURE:
+        return unit in TEMPERATURE_UNITS
+    if unit_type == MASS:
+        return unit in MASS_UNITS
+    if unit_type == VOLUME:
+        return unit in VOLUME_UNITS
+    if unit_type == PRESSURE:
+        return unit in PRESSURE_UNITS
+    return False
 
 
 class UnitSystem:
@@ -267,62 +247,62 @@ validate_unit_system = vol.All(
 
 METRIC_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_METRIC,
-    accumulated_precipitation=PRECIPITATION_MILLIMETERS,
+    accumulated_precipitation=UnitOfLength.MILLIMETERS,
     conversions={
         # Convert non-metric distances
-        ("distance", LENGTH_FEET): LENGTH_METERS,
-        ("distance", LENGTH_INCHES): LENGTH_MILLIMETERS,
-        ("distance", LENGTH_MILES): LENGTH_KILOMETERS,
-        ("distance", LENGTH_YARD): LENGTH_METERS,
+        ("distance", UnitOfLength.FEET): UnitOfLength.METERS,
+        ("distance", UnitOfLength.INCHES): UnitOfLength.MILLIMETERS,
+        ("distance", UnitOfLength.MILES): UnitOfLength.KILOMETERS,
+        ("distance", UnitOfLength.YARDS): UnitOfLength.METERS,
         # Convert non-metric volumes of gas meters
-        ("gas", VOLUME_CUBIC_FEET): VOLUME_CUBIC_METERS,
+        ("gas", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
         # Convert non-metric speeds except knots to km/h
-        ("speed", SPEED_FEET_PER_SECOND): SPEED_KILOMETERS_PER_HOUR,
-        ("speed", SPEED_MILES_PER_HOUR): SPEED_KILOMETERS_PER_HOUR,
+        ("speed", UnitOfSpeed.FEET_PER_SECOND): UnitOfSpeed.KILOMETERS_PER_HOUR,
+        ("speed", UnitOfSpeed.MILES_PER_HOUR): UnitOfSpeed.KILOMETERS_PER_HOUR,
         # Convert non-metric volumes
-        ("volume", VOLUME_CUBIC_FEET): VOLUME_CUBIC_METERS,
-        ("volume", VOLUME_FLUID_OUNCE): VOLUME_MILLILITERS,
-        ("volume", VOLUME_GALLONS): VOLUME_LITERS,
+        ("volume", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
+        ("volume", UnitOfVolume.FLUID_OUNCES): UnitOfVolume.MILLILITERS,
+        ("volume", UnitOfVolume.GALLONS): UnitOfVolume.LITERS,
         # Convert non-metric volumes of water meters
-        ("water", VOLUME_CUBIC_FEET): VOLUME_CUBIC_METERS,
-        ("water", VOLUME_GALLONS): VOLUME_LITERS,
+        ("water", UnitOfVolume.CUBIC_FEET): UnitOfVolume.CUBIC_METERS,
+        ("water", UnitOfVolume.GALLONS): UnitOfVolume.LITERS,
     },
-    length=LENGTH_KILOMETERS,
-    mass=MASS_GRAMS,
-    pressure=PRESSURE_PA,
-    temperature=TEMP_CELSIUS,
-    volume=VOLUME_LITERS,
-    wind_speed=SPEED_METERS_PER_SECOND,
+    length=UnitOfLength.KILOMETERS,
+    mass=UnitOfMass.GRAMS,
+    pressure=UnitOfPressure.PA,
+    temperature=UnitOfTemperature.CELSIUS,
+    volume=UnitOfVolume.LITERS,
+    wind_speed=UnitOfSpeed.METERS_PER_SECOND,
 )
 
 US_CUSTOMARY_SYSTEM = UnitSystem(
     _CONF_UNIT_SYSTEM_US_CUSTOMARY,
-    accumulated_precipitation=PRECIPITATION_INCHES,
+    accumulated_precipitation=UnitOfLength.INCHES,
     conversions={
         # Convert non-USCS distances
-        ("distance", LENGTH_CENTIMETERS): LENGTH_INCHES,
-        ("distance", LENGTH_KILOMETERS): LENGTH_MILES,
-        ("distance", LENGTH_METERS): LENGTH_FEET,
-        ("distance", LENGTH_MILLIMETERS): LENGTH_INCHES,
+        ("distance", UnitOfLength.CENTIMETERS): UnitOfLength.INCHES,
+        ("distance", UnitOfLength.KILOMETERS): UnitOfLength.MILES,
+        ("distance", UnitOfLength.METERS): UnitOfLength.FEET,
+        ("distance", UnitOfLength.MILLIMETERS): UnitOfLength.INCHES,
         # Convert non-USCS volumes of gas meters
-        ("gas", VOLUME_CUBIC_METERS): VOLUME_CUBIC_FEET,
+        ("gas", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET,
         # Convert non-USCS speeds except knots to mph
-        ("speed", SPEED_METERS_PER_SECOND): SPEED_MILES_PER_HOUR,
-        ("speed", SPEED_KILOMETERS_PER_HOUR): SPEED_MILES_PER_HOUR,
+        ("speed", UnitOfSpeed.METERS_PER_SECOND): UnitOfSpeed.MILES_PER_HOUR,
+        ("speed", UnitOfSpeed.KILOMETERS_PER_HOUR): UnitOfSpeed.MILES_PER_HOUR,
         # Convert non-USCS volumes
-        ("volume", VOLUME_CUBIC_METERS): VOLUME_CUBIC_FEET,
-        ("volume", VOLUME_LITERS): VOLUME_GALLONS,
-        ("volume", VOLUME_MILLILITERS): VOLUME_FLUID_OUNCE,
+        ("volume", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET,
+        ("volume", UnitOfVolume.LITERS): UnitOfVolume.GALLONS,
+        ("volume", UnitOfVolume.MILLILITERS): UnitOfVolume.FLUID_OUNCES,
         # Convert non-USCS volumes of water meters
-        ("water", VOLUME_CUBIC_METERS): VOLUME_CUBIC_FEET,
-        ("water", VOLUME_LITERS): VOLUME_GALLONS,
+        ("water", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET,
+        ("water", UnitOfVolume.LITERS): UnitOfVolume.GALLONS,
     },
-    length=LENGTH_MILES,
-    mass=MASS_POUNDS,
-    pressure=PRESSURE_PSI,
-    temperature=TEMP_FAHRENHEIT,
-    volume=VOLUME_GALLONS,
-    wind_speed=SPEED_MILES_PER_HOUR,
+    length=UnitOfLength.MILES,
+    mass=UnitOfMass.POUNDS,
+    pressure=UnitOfPressure.PSI,
+    temperature=UnitOfTemperature.FAHRENHEIT,
+    volume=UnitOfVolume.GALLONS,
+    wind_speed=UnitOfSpeed.MILES_PER_HOUR,
 )
 
 IMPERIAL_SYSTEM = US_CUSTOMARY_SYSTEM
diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py
index 30468064dae..1b8a1928b1f 100644
--- a/tests/components/flipr/test_sensor.py
+++ b/tests/components/flipr/test_sensor.py
@@ -69,7 +69,7 @@ async def test_sensors(hass: HomeAssistant) -> None:
     state = hass.states.get("sensor.flipr_myfliprid_water_temp")
     assert state
     assert state.attributes.get(ATTR_ICON) is None
-    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is TEMP_CELSIUS
+    assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS
     assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
     assert state.state == "10.5"
 
diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py
index 1b43af09aec..b2b99d6f8c0 100644
--- a/tests/util/test_unit_conversion.py
+++ b/tests/util/test_unit_conversion.py
@@ -2,45 +2,14 @@
 import pytest
 
 from homeassistant.const import (
-    LENGTH_CENTIMETERS,
-    LENGTH_FEET,
-    LENGTH_INCHES,
-    LENGTH_KILOMETERS,
-    LENGTH_METERS,
-    LENGTH_MILES,
-    LENGTH_MILLIMETERS,
-    LENGTH_YARD,
-    MASS_GRAMS,
-    MASS_KILOGRAMS,
-    MASS_MICROGRAMS,
-    MASS_MILLIGRAMS,
-    MASS_OUNCES,
-    MASS_POUNDS,
-    POWER_KILO_WATT,
-    POWER_WATT,
-    PRESSURE_CBAR,
-    PRESSURE_HPA,
-    PRESSURE_INHG,
-    PRESSURE_KPA,
-    PRESSURE_MBAR,
-    PRESSURE_MMHG,
-    PRESSURE_PA,
-    PRESSURE_PSI,
-    SPEED_FEET_PER_SECOND,
-    SPEED_KILOMETERS_PER_HOUR,
-    SPEED_KNOTS,
-    SPEED_METERS_PER_SECOND,
-    SPEED_MILES_PER_HOUR,
-    TEMP_CELSIUS,
-    TEMP_FAHRENHEIT,
-    TEMP_KELVIN,
-    VOLUME_CUBIC_FEET,
-    VOLUME_CUBIC_METERS,
-    VOLUME_FLUID_OUNCE,
-    VOLUME_GALLONS,
-    VOLUME_LITERS,
-    VOLUME_MILLILITERS,
     UnitOfEnergy,
+    UnitOfLength,
+    UnitOfMass,
+    UnitOfPower,
+    UnitOfPressure,
+    UnitOfSpeed,
+    UnitOfTemperature,
+    UnitOfVolume,
     UnitOfVolumetricFlux,
 )
 from homeassistant.exceptions import HomeAssistantError
@@ -62,50 +31,50 @@ INVALID_SYMBOL = "bob"
 @pytest.mark.parametrize(
     "converter,valid_unit",
     [
-        (DistanceConverter, LENGTH_KILOMETERS),
-        (DistanceConverter, LENGTH_METERS),
-        (DistanceConverter, LENGTH_CENTIMETERS),
-        (DistanceConverter, LENGTH_MILLIMETERS),
-        (DistanceConverter, LENGTH_MILES),
-        (DistanceConverter, LENGTH_YARD),
-        (DistanceConverter, LENGTH_FEET),
-        (DistanceConverter, LENGTH_INCHES),
+        (DistanceConverter, UnitOfLength.KILOMETERS),
+        (DistanceConverter, UnitOfLength.METERS),
+        (DistanceConverter, UnitOfLength.CENTIMETERS),
+        (DistanceConverter, UnitOfLength.MILLIMETERS),
+        (DistanceConverter, UnitOfLength.MILES),
+        (DistanceConverter, UnitOfLength.YARDS),
+        (DistanceConverter, UnitOfLength.FEET),
+        (DistanceConverter, UnitOfLength.INCHES),
         (EnergyConverter, UnitOfEnergy.WATT_HOUR),
         (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
         (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR),
         (EnergyConverter, UnitOfEnergy.GIGA_JOULE),
-        (MassConverter, MASS_GRAMS),
-        (MassConverter, MASS_KILOGRAMS),
-        (MassConverter, MASS_MICROGRAMS),
-        (MassConverter, MASS_MILLIGRAMS),
-        (MassConverter, MASS_OUNCES),
-        (MassConverter, MASS_POUNDS),
-        (PowerConverter, POWER_WATT),
-        (PowerConverter, POWER_KILO_WATT),
-        (PressureConverter, PRESSURE_PA),
-        (PressureConverter, PRESSURE_HPA),
-        (PressureConverter, PRESSURE_MBAR),
-        (PressureConverter, PRESSURE_INHG),
-        (PressureConverter, PRESSURE_KPA),
-        (PressureConverter, PRESSURE_CBAR),
-        (PressureConverter, PRESSURE_MMHG),
-        (PressureConverter, PRESSURE_PSI),
+        (MassConverter, UnitOfMass.GRAMS),
+        (MassConverter, UnitOfMass.KILOGRAMS),
+        (MassConverter, UnitOfMass.MICROGRAMS),
+        (MassConverter, UnitOfMass.MILLIGRAMS),
+        (MassConverter, UnitOfMass.OUNCES),
+        (MassConverter, UnitOfMass.POUNDS),
+        (PowerConverter, UnitOfPower.WATT),
+        (PowerConverter, UnitOfPower.KILO_WATT),
+        (PressureConverter, UnitOfPressure.PA),
+        (PressureConverter, UnitOfPressure.HPA),
+        (PressureConverter, UnitOfPressure.MBAR),
+        (PressureConverter, UnitOfPressure.INHG),
+        (PressureConverter, UnitOfPressure.KPA),
+        (PressureConverter, UnitOfPressure.CBAR),
+        (PressureConverter, UnitOfPressure.MMHG),
+        (PressureConverter, UnitOfPressure.PSI),
         (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_DAY),
         (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_HOUR),
         (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY),
         (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR),
-        (SpeedConverter, SPEED_FEET_PER_SECOND),
-        (SpeedConverter, SPEED_KILOMETERS_PER_HOUR),
-        (SpeedConverter, SPEED_KNOTS),
-        (SpeedConverter, SPEED_METERS_PER_SECOND),
-        (SpeedConverter, SPEED_MILES_PER_HOUR),
-        (TemperatureConverter, TEMP_CELSIUS),
-        (TemperatureConverter, TEMP_FAHRENHEIT),
-        (TemperatureConverter, TEMP_KELVIN),
-        (VolumeConverter, VOLUME_LITERS),
-        (VolumeConverter, VOLUME_MILLILITERS),
-        (VolumeConverter, VOLUME_GALLONS),
-        (VolumeConverter, VOLUME_FLUID_OUNCE),
+        (SpeedConverter, UnitOfSpeed.FEET_PER_SECOND),
+        (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR),
+        (SpeedConverter, UnitOfSpeed.KNOTS),
+        (SpeedConverter, UnitOfSpeed.METERS_PER_SECOND),
+        (SpeedConverter, UnitOfSpeed.MILES_PER_HOUR),
+        (TemperatureConverter, UnitOfTemperature.CELSIUS),
+        (TemperatureConverter, UnitOfTemperature.FAHRENHEIT),
+        (TemperatureConverter, UnitOfTemperature.KELVIN),
+        (VolumeConverter, UnitOfVolume.LITERS),
+        (VolumeConverter, UnitOfVolume.MILLILITERS),
+        (VolumeConverter, UnitOfVolume.GALLONS),
+        (VolumeConverter, UnitOfVolume.FLUID_OUNCES),
     ],
 )
 def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None:
@@ -116,16 +85,16 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str)
 @pytest.mark.parametrize(
     "converter,valid_unit",
     [
-        (DistanceConverter, LENGTH_KILOMETERS),
+        (DistanceConverter, UnitOfLength.KILOMETERS),
         (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
-        (MassConverter, MASS_GRAMS),
-        (PowerConverter, POWER_WATT),
-        (PressureConverter, PRESSURE_PA),
-        (SpeedConverter, SPEED_KILOMETERS_PER_HOUR),
-        (TemperatureConverter, TEMP_CELSIUS),
-        (TemperatureConverter, TEMP_FAHRENHEIT),
-        (TemperatureConverter, TEMP_KELVIN),
-        (VolumeConverter, VOLUME_LITERS),
+        (MassConverter, UnitOfMass.GRAMS),
+        (PowerConverter, UnitOfPower.WATT),
+        (PressureConverter, UnitOfPressure.PA),
+        (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR),
+        (TemperatureConverter, UnitOfTemperature.CELSIUS),
+        (TemperatureConverter, UnitOfTemperature.FAHRENHEIT),
+        (TemperatureConverter, UnitOfTemperature.KELVIN),
+        (VolumeConverter, UnitOfVolume.LITERS),
     ],
 )
 def test_convert_invalid_unit(
@@ -142,14 +111,14 @@ def test_convert_invalid_unit(
 @pytest.mark.parametrize(
     "converter,from_unit,to_unit",
     [
-        (DistanceConverter, LENGTH_KILOMETERS, LENGTH_METERS),
+        (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS),
         (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR),
-        (MassConverter, MASS_GRAMS, MASS_KILOGRAMS),
-        (PowerConverter, POWER_WATT, POWER_KILO_WATT),
-        (PressureConverter, PRESSURE_HPA, PRESSURE_INHG),
-        (SpeedConverter, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR),
-        (TemperatureConverter, TEMP_CELSIUS, TEMP_FAHRENHEIT),
-        (VolumeConverter, VOLUME_GALLONS, VOLUME_LITERS),
+        (MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS),
+        (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT),
+        (PressureConverter, UnitOfPressure.HPA, UnitOfPressure.INHG),
+        (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR),
+        (TemperatureConverter, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT),
+        (VolumeConverter, UnitOfVolume.GALLONS, UnitOfVolume.LITERS),
     ],
 )
 def test_convert_nonnumeric_value(
@@ -163,18 +132,33 @@ def test_convert_nonnumeric_value(
 @pytest.mark.parametrize(
     "converter,from_unit,to_unit,expected",
     [
-        (DistanceConverter, LENGTH_KILOMETERS, LENGTH_METERS, 1 / 1000),
+        (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000),
         (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000),
-        (PowerConverter, POWER_WATT, POWER_KILO_WATT, 1000),
-        (PressureConverter, PRESSURE_HPA, PRESSURE_INHG, pytest.approx(33.86389)),
+        (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000),
+        (
+            PressureConverter,
+            UnitOfPressure.HPA,
+            UnitOfPressure.INHG,
+            pytest.approx(33.86389),
+        ),
         (
             SpeedConverter,
-            SPEED_KILOMETERS_PER_HOUR,
-            SPEED_MILES_PER_HOUR,
+            UnitOfSpeed.KILOMETERS_PER_HOUR,
+            UnitOfSpeed.MILES_PER_HOUR,
             pytest.approx(1.609343),
         ),
-        (TemperatureConverter, TEMP_CELSIUS, TEMP_FAHRENHEIT, 1 / 1.8),
-        (VolumeConverter, VOLUME_GALLONS, VOLUME_LITERS, pytest.approx(0.264172)),
+        (
+            TemperatureConverter,
+            UnitOfTemperature.CELSIUS,
+            UnitOfTemperature.FAHRENHEIT,
+            1 / 1.8,
+        ),
+        (
+            VolumeConverter,
+            UnitOfVolume.GALLONS,
+            UnitOfVolume.LITERS,
+            pytest.approx(0.264172),
+        ),
     ],
 )
 def test_get_unit_ratio(
@@ -187,62 +171,107 @@ def test_get_unit_ratio(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (5, LENGTH_MILES, pytest.approx(8.04672), LENGTH_KILOMETERS),
-        (5, LENGTH_MILES, pytest.approx(8046.72), LENGTH_METERS),
-        (5, LENGTH_MILES, pytest.approx(804672.0), LENGTH_CENTIMETERS),
-        (5, LENGTH_MILES, pytest.approx(8046720.0), LENGTH_MILLIMETERS),
-        (5, LENGTH_MILES, pytest.approx(8800.0), LENGTH_YARD),
-        (5, LENGTH_MILES, pytest.approx(26400.0008448), LENGTH_FEET),
-        (5, LENGTH_MILES, pytest.approx(316800.171072), LENGTH_INCHES),
-        (5, LENGTH_YARD, pytest.approx(0.0045720000000000005), LENGTH_KILOMETERS),
-        (5, LENGTH_YARD, pytest.approx(4.572), LENGTH_METERS),
-        (5, LENGTH_YARD, pytest.approx(457.2), LENGTH_CENTIMETERS),
-        (5, LENGTH_YARD, pytest.approx(4572), LENGTH_MILLIMETERS),
-        (5, LENGTH_YARD, pytest.approx(0.002840908212), LENGTH_MILES),
-        (5, LENGTH_YARD, pytest.approx(15.00000048), LENGTH_FEET),
-        (5, LENGTH_YARD, pytest.approx(180.0000972), LENGTH_INCHES),
-        (5000, LENGTH_FEET, pytest.approx(1.524), LENGTH_KILOMETERS),
-        (5000, LENGTH_FEET, pytest.approx(1524), LENGTH_METERS),
-        (5000, LENGTH_FEET, pytest.approx(152400.0), LENGTH_CENTIMETERS),
-        (5000, LENGTH_FEET, pytest.approx(1524000.0), LENGTH_MILLIMETERS),
-        (5000, LENGTH_FEET, pytest.approx(0.9469694040000001), LENGTH_MILES),
-        (5000, LENGTH_FEET, pytest.approx(1666.66667), LENGTH_YARD),
-        (5000, LENGTH_FEET, pytest.approx(60000.032400000004), LENGTH_INCHES),
-        (5000, LENGTH_INCHES, pytest.approx(0.127), LENGTH_KILOMETERS),
-        (5000, LENGTH_INCHES, pytest.approx(127.0), LENGTH_METERS),
-        (5000, LENGTH_INCHES, pytest.approx(12700.0), LENGTH_CENTIMETERS),
-        (5000, LENGTH_INCHES, pytest.approx(127000.0), LENGTH_MILLIMETERS),
-        (5000, LENGTH_INCHES, pytest.approx(0.078914117), LENGTH_MILES),
-        (5000, LENGTH_INCHES, pytest.approx(138.88889), LENGTH_YARD),
-        (5000, LENGTH_INCHES, pytest.approx(416.66668), LENGTH_FEET),
-        (5, LENGTH_KILOMETERS, pytest.approx(5000), LENGTH_METERS),
-        (5, LENGTH_KILOMETERS, pytest.approx(500000), LENGTH_CENTIMETERS),
-        (5, LENGTH_KILOMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS),
-        (5, LENGTH_KILOMETERS, pytest.approx(3.106855), LENGTH_MILES),
-        (5, LENGTH_KILOMETERS, pytest.approx(5468.066), LENGTH_YARD),
-        (5, LENGTH_KILOMETERS, pytest.approx(16404.2), LENGTH_FEET),
-        (5, LENGTH_KILOMETERS, pytest.approx(196850.5), LENGTH_INCHES),
-        (5000, LENGTH_METERS, pytest.approx(5), LENGTH_KILOMETERS),
-        (5000, LENGTH_METERS, pytest.approx(500000), LENGTH_CENTIMETERS),
-        (5000, LENGTH_METERS, pytest.approx(5000000), LENGTH_MILLIMETERS),
-        (5000, LENGTH_METERS, pytest.approx(3.106855), LENGTH_MILES),
-        (5000, LENGTH_METERS, pytest.approx(5468.066), LENGTH_YARD),
-        (5000, LENGTH_METERS, pytest.approx(16404.2), LENGTH_FEET),
-        (5000, LENGTH_METERS, pytest.approx(196850.5), LENGTH_INCHES),
-        (500000, LENGTH_CENTIMETERS, pytest.approx(5), LENGTH_KILOMETERS),
-        (500000, LENGTH_CENTIMETERS, pytest.approx(5000), LENGTH_METERS),
-        (500000, LENGTH_CENTIMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS),
-        (500000, LENGTH_CENTIMETERS, pytest.approx(3.106855), LENGTH_MILES),
-        (500000, LENGTH_CENTIMETERS, pytest.approx(5468.066), LENGTH_YARD),
-        (500000, LENGTH_CENTIMETERS, pytest.approx(16404.2), LENGTH_FEET),
-        (500000, LENGTH_CENTIMETERS, pytest.approx(196850.5), LENGTH_INCHES),
-        (5000000, LENGTH_MILLIMETERS, pytest.approx(5), LENGTH_KILOMETERS),
-        (5000000, LENGTH_MILLIMETERS, pytest.approx(5000), LENGTH_METERS),
-        (5000000, LENGTH_MILLIMETERS, pytest.approx(500000), LENGTH_CENTIMETERS),
-        (5000000, LENGTH_MILLIMETERS, pytest.approx(3.106855), LENGTH_MILES),
-        (5000000, LENGTH_MILLIMETERS, pytest.approx(5468.066), LENGTH_YARD),
-        (5000000, LENGTH_MILLIMETERS, pytest.approx(16404.2), LENGTH_FEET),
-        (5000000, LENGTH_MILLIMETERS, pytest.approx(196850.5), LENGTH_INCHES),
+        (5, UnitOfLength.MILES, pytest.approx(8.04672), UnitOfLength.KILOMETERS),
+        (5, UnitOfLength.MILES, pytest.approx(8046.72), UnitOfLength.METERS),
+        (5, UnitOfLength.MILES, pytest.approx(804672.0), UnitOfLength.CENTIMETERS),
+        (5, UnitOfLength.MILES, pytest.approx(8046720.0), UnitOfLength.MILLIMETERS),
+        (5, UnitOfLength.MILES, pytest.approx(8800.0), UnitOfLength.YARDS),
+        (5, UnitOfLength.MILES, pytest.approx(26400.0008448), UnitOfLength.FEET),
+        (5, UnitOfLength.MILES, pytest.approx(316800.171072), UnitOfLength.INCHES),
+        (
+            5,
+            UnitOfLength.YARDS,
+            pytest.approx(0.0045720000000000005),
+            UnitOfLength.KILOMETERS,
+        ),
+        (5, UnitOfLength.YARDS, pytest.approx(4.572), UnitOfLength.METERS),
+        (5, UnitOfLength.YARDS, pytest.approx(457.2), UnitOfLength.CENTIMETERS),
+        (5, UnitOfLength.YARDS, pytest.approx(4572), UnitOfLength.MILLIMETERS),
+        (5, UnitOfLength.YARDS, pytest.approx(0.002840908212), UnitOfLength.MILES),
+        (5, UnitOfLength.YARDS, pytest.approx(15.00000048), UnitOfLength.FEET),
+        (5, UnitOfLength.YARDS, pytest.approx(180.0000972), UnitOfLength.INCHES),
+        (5000, UnitOfLength.FEET, pytest.approx(1.524), UnitOfLength.KILOMETERS),
+        (5000, UnitOfLength.FEET, pytest.approx(1524), UnitOfLength.METERS),
+        (5000, UnitOfLength.FEET, pytest.approx(152400.0), UnitOfLength.CENTIMETERS),
+        (5000, UnitOfLength.FEET, pytest.approx(1524000.0), UnitOfLength.MILLIMETERS),
+        (
+            5000,
+            UnitOfLength.FEET,
+            pytest.approx(0.9469694040000001),
+            UnitOfLength.MILES,
+        ),
+        (5000, UnitOfLength.FEET, pytest.approx(1666.66667), UnitOfLength.YARDS),
+        (
+            5000,
+            UnitOfLength.FEET,
+            pytest.approx(60000.032400000004),
+            UnitOfLength.INCHES,
+        ),
+        (5000, UnitOfLength.INCHES, pytest.approx(0.127), UnitOfLength.KILOMETERS),
+        (5000, UnitOfLength.INCHES, pytest.approx(127.0), UnitOfLength.METERS),
+        (5000, UnitOfLength.INCHES, pytest.approx(12700.0), UnitOfLength.CENTIMETERS),
+        (5000, UnitOfLength.INCHES, pytest.approx(127000.0), UnitOfLength.MILLIMETERS),
+        (5000, UnitOfLength.INCHES, pytest.approx(0.078914117), UnitOfLength.MILES),
+        (5000, UnitOfLength.INCHES, pytest.approx(138.88889), UnitOfLength.YARDS),
+        (5000, UnitOfLength.INCHES, pytest.approx(416.66668), UnitOfLength.FEET),
+        (5, UnitOfLength.KILOMETERS, pytest.approx(5000), UnitOfLength.METERS),
+        (5, UnitOfLength.KILOMETERS, pytest.approx(500000), UnitOfLength.CENTIMETERS),
+        (5, UnitOfLength.KILOMETERS, pytest.approx(5000000), UnitOfLength.MILLIMETERS),
+        (5, UnitOfLength.KILOMETERS, pytest.approx(3.106855), UnitOfLength.MILES),
+        (5, UnitOfLength.KILOMETERS, pytest.approx(5468.066), UnitOfLength.YARDS),
+        (5, UnitOfLength.KILOMETERS, pytest.approx(16404.2), UnitOfLength.FEET),
+        (5, UnitOfLength.KILOMETERS, pytest.approx(196850.5), UnitOfLength.INCHES),
+        (5000, UnitOfLength.METERS, pytest.approx(5), UnitOfLength.KILOMETERS),
+        (5000, UnitOfLength.METERS, pytest.approx(500000), UnitOfLength.CENTIMETERS),
+        (5000, UnitOfLength.METERS, pytest.approx(5000000), UnitOfLength.MILLIMETERS),
+        (5000, UnitOfLength.METERS, pytest.approx(3.106855), UnitOfLength.MILES),
+        (5000, UnitOfLength.METERS, pytest.approx(5468.066), UnitOfLength.YARDS),
+        (5000, UnitOfLength.METERS, pytest.approx(16404.2), UnitOfLength.FEET),
+        (5000, UnitOfLength.METERS, pytest.approx(196850.5), UnitOfLength.INCHES),
+        (500000, UnitOfLength.CENTIMETERS, pytest.approx(5), UnitOfLength.KILOMETERS),
+        (500000, UnitOfLength.CENTIMETERS, pytest.approx(5000), UnitOfLength.METERS),
+        (
+            500000,
+            UnitOfLength.CENTIMETERS,
+            pytest.approx(5000000),
+            UnitOfLength.MILLIMETERS,
+        ),
+        (500000, UnitOfLength.CENTIMETERS, pytest.approx(3.106855), UnitOfLength.MILES),
+        (500000, UnitOfLength.CENTIMETERS, pytest.approx(5468.066), UnitOfLength.YARDS),
+        (500000, UnitOfLength.CENTIMETERS, pytest.approx(16404.2), UnitOfLength.FEET),
+        (
+            500000,
+            UnitOfLength.CENTIMETERS,
+            pytest.approx(196850.5),
+            UnitOfLength.INCHES,
+        ),
+        (5000000, UnitOfLength.MILLIMETERS, pytest.approx(5), UnitOfLength.KILOMETERS),
+        (5000000, UnitOfLength.MILLIMETERS, pytest.approx(5000), UnitOfLength.METERS),
+        (
+            5000000,
+            UnitOfLength.MILLIMETERS,
+            pytest.approx(500000),
+            UnitOfLength.CENTIMETERS,
+        ),
+        (
+            5000000,
+            UnitOfLength.MILLIMETERS,
+            pytest.approx(3.106855),
+            UnitOfLength.MILES,
+        ),
+        (
+            5000000,
+            UnitOfLength.MILLIMETERS,
+            pytest.approx(5468.066),
+            UnitOfLength.YARDS,
+        ),
+        (5000000, UnitOfLength.MILLIMETERS, pytest.approx(16404.2), UnitOfLength.FEET),
+        (
+            5000000,
+            UnitOfLength.MILLIMETERS,
+            pytest.approx(196850.5),
+            UnitOfLength.INCHES,
+        ),
     ],
 )
 def test_distance_convert(
@@ -281,36 +310,41 @@ def test_energy_convert(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (10, MASS_KILOGRAMS, 10000, MASS_GRAMS),
-        (10, MASS_KILOGRAMS, 10000000, MASS_MILLIGRAMS),
-        (10, MASS_KILOGRAMS, 10000000000, MASS_MICROGRAMS),
-        (10, MASS_KILOGRAMS, pytest.approx(352.73961), MASS_OUNCES),
-        (10, MASS_KILOGRAMS, pytest.approx(22.046226), MASS_POUNDS),
-        (10, MASS_GRAMS, 0.01, MASS_KILOGRAMS),
-        (10, MASS_GRAMS, 10000, MASS_MILLIGRAMS),
-        (10, MASS_GRAMS, 10000000, MASS_MICROGRAMS),
-        (10, MASS_GRAMS, pytest.approx(0.35273961), MASS_OUNCES),
-        (10, MASS_GRAMS, pytest.approx(0.022046226), MASS_POUNDS),
-        (10, MASS_MILLIGRAMS, 0.00001, MASS_KILOGRAMS),
-        (10, MASS_MILLIGRAMS, 0.01, MASS_GRAMS),
-        (10, MASS_MILLIGRAMS, 10000, MASS_MICROGRAMS),
-        (10, MASS_MILLIGRAMS, pytest.approx(0.00035273961), MASS_OUNCES),
-        (10, MASS_MILLIGRAMS, pytest.approx(0.000022046226), MASS_POUNDS),
-        (10000, MASS_MICROGRAMS, 0.00001, MASS_KILOGRAMS),
-        (10000, MASS_MICROGRAMS, 0.01, MASS_GRAMS),
-        (10000, MASS_MICROGRAMS, 10, MASS_MILLIGRAMS),
-        (10000, MASS_MICROGRAMS, pytest.approx(0.00035273961), MASS_OUNCES),
-        (10000, MASS_MICROGRAMS, pytest.approx(0.000022046226), MASS_POUNDS),
-        (1, MASS_POUNDS, 0.45359237, MASS_KILOGRAMS),
-        (1, MASS_POUNDS, 453.59237, MASS_GRAMS),
-        (1, MASS_POUNDS, 453592.37, MASS_MILLIGRAMS),
-        (1, MASS_POUNDS, 453592370, MASS_MICROGRAMS),
-        (1, MASS_POUNDS, 16, MASS_OUNCES),
-        (16, MASS_OUNCES, 0.45359237, MASS_KILOGRAMS),
-        (16, MASS_OUNCES, 453.59237, MASS_GRAMS),
-        (16, MASS_OUNCES, 453592.37, MASS_MILLIGRAMS),
-        (16, MASS_OUNCES, 453592370, MASS_MICROGRAMS),
-        (16, MASS_OUNCES, 1, MASS_POUNDS),
+        (10, UnitOfMass.KILOGRAMS, 10000, UnitOfMass.GRAMS),
+        (10, UnitOfMass.KILOGRAMS, 10000000, UnitOfMass.MILLIGRAMS),
+        (10, UnitOfMass.KILOGRAMS, 10000000000, UnitOfMass.MICROGRAMS),
+        (10, UnitOfMass.KILOGRAMS, pytest.approx(352.73961), UnitOfMass.OUNCES),
+        (10, UnitOfMass.KILOGRAMS, pytest.approx(22.046226), UnitOfMass.POUNDS),
+        (10, UnitOfMass.GRAMS, 0.01, UnitOfMass.KILOGRAMS),
+        (10, UnitOfMass.GRAMS, 10000, UnitOfMass.MILLIGRAMS),
+        (10, UnitOfMass.GRAMS, 10000000, UnitOfMass.MICROGRAMS),
+        (10, UnitOfMass.GRAMS, pytest.approx(0.35273961), UnitOfMass.OUNCES),
+        (10, UnitOfMass.GRAMS, pytest.approx(0.022046226), UnitOfMass.POUNDS),
+        (10, UnitOfMass.MILLIGRAMS, 0.00001, UnitOfMass.KILOGRAMS),
+        (10, UnitOfMass.MILLIGRAMS, 0.01, UnitOfMass.GRAMS),
+        (10, UnitOfMass.MILLIGRAMS, 10000, UnitOfMass.MICROGRAMS),
+        (10, UnitOfMass.MILLIGRAMS, pytest.approx(0.00035273961), UnitOfMass.OUNCES),
+        (10, UnitOfMass.MILLIGRAMS, pytest.approx(0.000022046226), UnitOfMass.POUNDS),
+        (10000, UnitOfMass.MICROGRAMS, 0.00001, UnitOfMass.KILOGRAMS),
+        (10000, UnitOfMass.MICROGRAMS, 0.01, UnitOfMass.GRAMS),
+        (10000, UnitOfMass.MICROGRAMS, 10, UnitOfMass.MILLIGRAMS),
+        (10000, UnitOfMass.MICROGRAMS, pytest.approx(0.00035273961), UnitOfMass.OUNCES),
+        (
+            10000,
+            UnitOfMass.MICROGRAMS,
+            pytest.approx(0.000022046226),
+            UnitOfMass.POUNDS,
+        ),
+        (1, UnitOfMass.POUNDS, 0.45359237, UnitOfMass.KILOGRAMS),
+        (1, UnitOfMass.POUNDS, 453.59237, UnitOfMass.GRAMS),
+        (1, UnitOfMass.POUNDS, 453592.37, UnitOfMass.MILLIGRAMS),
+        (1, UnitOfMass.POUNDS, 453592370, UnitOfMass.MICROGRAMS),
+        (1, UnitOfMass.POUNDS, 16, UnitOfMass.OUNCES),
+        (16, UnitOfMass.OUNCES, 0.45359237, UnitOfMass.KILOGRAMS),
+        (16, UnitOfMass.OUNCES, 453.59237, UnitOfMass.GRAMS),
+        (16, UnitOfMass.OUNCES, 453592.37, UnitOfMass.MILLIGRAMS),
+        (16, UnitOfMass.OUNCES, 453592370, UnitOfMass.MICROGRAMS),
+        (16, UnitOfMass.OUNCES, 1, UnitOfMass.POUNDS),
     ],
 )
 def test_mass_convert(
@@ -326,8 +360,8 @@ def test_mass_convert(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (10, POWER_KILO_WATT, 10000, POWER_WATT),
-        (10, POWER_WATT, 0.01, POWER_KILO_WATT),
+        (10, UnitOfPower.KILO_WATT, 10000, UnitOfPower.WATT),
+        (10, UnitOfPower.WATT, 0.01, UnitOfPower.KILO_WATT),
     ],
 )
 def test_power_convert(
@@ -343,32 +377,32 @@ def test_power_convert(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (1000, PRESSURE_HPA, pytest.approx(14.5037743897), PRESSURE_PSI),
-        (1000, PRESSURE_HPA, pytest.approx(29.5299801647), PRESSURE_INHG),
-        (1000, PRESSURE_HPA, pytest.approx(100000), PRESSURE_PA),
-        (1000, PRESSURE_HPA, pytest.approx(100), PRESSURE_KPA),
-        (1000, PRESSURE_HPA, pytest.approx(1000), PRESSURE_MBAR),
-        (1000, PRESSURE_HPA, pytest.approx(100), PRESSURE_CBAR),
-        (100, PRESSURE_KPA, pytest.approx(14.5037743897), PRESSURE_PSI),
-        (100, PRESSURE_KPA, pytest.approx(29.5299801647), PRESSURE_INHG),
-        (100, PRESSURE_KPA, pytest.approx(100000), PRESSURE_PA),
-        (100, PRESSURE_KPA, pytest.approx(1000), PRESSURE_HPA),
-        (100, PRESSURE_KPA, pytest.approx(1000), PRESSURE_MBAR),
-        (100, PRESSURE_KPA, pytest.approx(100), PRESSURE_CBAR),
-        (30, PRESSURE_INHG, pytest.approx(14.7346266155), PRESSURE_PSI),
-        (30, PRESSURE_INHG, pytest.approx(101.59167), PRESSURE_KPA),
-        (30, PRESSURE_INHG, pytest.approx(1015.9167), PRESSURE_HPA),
-        (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), 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),
+        (1000, UnitOfPressure.HPA, pytest.approx(14.5037743897), UnitOfPressure.PSI),
+        (1000, UnitOfPressure.HPA, pytest.approx(29.5299801647), UnitOfPressure.INHG),
+        (1000, UnitOfPressure.HPA, pytest.approx(100000), UnitOfPressure.PA),
+        (1000, UnitOfPressure.HPA, pytest.approx(100), UnitOfPressure.KPA),
+        (1000, UnitOfPressure.HPA, pytest.approx(1000), UnitOfPressure.MBAR),
+        (1000, UnitOfPressure.HPA, pytest.approx(100), UnitOfPressure.CBAR),
+        (100, UnitOfPressure.KPA, pytest.approx(14.5037743897), UnitOfPressure.PSI),
+        (100, UnitOfPressure.KPA, pytest.approx(29.5299801647), UnitOfPressure.INHG),
+        (100, UnitOfPressure.KPA, pytest.approx(100000), UnitOfPressure.PA),
+        (100, UnitOfPressure.KPA, pytest.approx(1000), UnitOfPressure.HPA),
+        (100, UnitOfPressure.KPA, pytest.approx(1000), UnitOfPressure.MBAR),
+        (100, UnitOfPressure.KPA, pytest.approx(100), UnitOfPressure.CBAR),
+        (30, UnitOfPressure.INHG, pytest.approx(14.7346266155), UnitOfPressure.PSI),
+        (30, UnitOfPressure.INHG, pytest.approx(101.59167), UnitOfPressure.KPA),
+        (30, UnitOfPressure.INHG, pytest.approx(1015.9167), UnitOfPressure.HPA),
+        (30, UnitOfPressure.INHG, pytest.approx(101591.67), UnitOfPressure.PA),
+        (30, UnitOfPressure.INHG, pytest.approx(1015.9167), UnitOfPressure.MBAR),
+        (30, UnitOfPressure.INHG, pytest.approx(101.59167), UnitOfPressure.CBAR),
+        (30, UnitOfPressure.INHG, pytest.approx(762), UnitOfPressure.MMHG),
+        (30, UnitOfPressure.MMHG, pytest.approx(0.580103), UnitOfPressure.PSI),
+        (30, UnitOfPressure.MMHG, pytest.approx(3.99967), UnitOfPressure.KPA),
+        (30, UnitOfPressure.MMHG, pytest.approx(39.9967), UnitOfPressure.HPA),
+        (30, UnitOfPressure.MMHG, pytest.approx(3999.67), UnitOfPressure.PA),
+        (30, UnitOfPressure.MMHG, pytest.approx(39.9967), UnitOfPressure.MBAR),
+        (30, UnitOfPressure.MMHG, pytest.approx(3.99967), UnitOfPressure.CBAR),
+        (30, UnitOfPressure.MMHG, pytest.approx(1.181102), UnitOfPressure.INHG),
     ],
 )
 def test_pressure_convert(
@@ -385,9 +419,14 @@ def test_pressure_convert(
     "value,from_unit,expected,to_unit",
     [
         # 5 km/h / 1.609 km/mi = 3.10686 mi/h
-        (5, SPEED_KILOMETERS_PER_HOUR, pytest.approx(3.106856), SPEED_MILES_PER_HOUR),
+        (
+            5,
+            UnitOfSpeed.KILOMETERS_PER_HOUR,
+            pytest.approx(3.106856),
+            UnitOfSpeed.MILES_PER_HOUR,
+        ),
         # 5 mi/h * 1.609 km/mi = 8.04672 km/h
-        (5, SPEED_MILES_PER_HOUR, 8.04672, SPEED_KILOMETERS_PER_HOUR),
+        (5, UnitOfSpeed.MILES_PER_HOUR, 8.04672, UnitOfSpeed.KILOMETERS_PER_HOUR),
         # 5 in/day * 25.4 mm/in = 127 mm/day
         (
             5,
@@ -419,7 +458,7 @@ def test_pressure_convert(
         # 5 m/s * 39.3701 in/m * 3600 s/hr = 708661
         (
             5,
-            SPEED_METERS_PER_SECOND,
+            UnitOfSpeed.METERS_PER_SECOND,
             pytest.approx(708661.42),
             UnitOfVolumetricFlux.INCHES_PER_HOUR,
         ),
@@ -428,12 +467,17 @@ def test_pressure_convert(
             5000,
             UnitOfVolumetricFlux.INCHES_PER_HOUR,
             pytest.approx(0.0352778),
-            SPEED_METERS_PER_SECOND,
+            UnitOfSpeed.METERS_PER_SECOND,
         ),
         # 5 kt * 1852 m/nmi / 3600 s/h = 2.5722 m/s
-        (5, SPEED_KNOTS, pytest.approx(2.57222), SPEED_METERS_PER_SECOND),
+        (5, UnitOfSpeed.KNOTS, pytest.approx(2.57222), UnitOfSpeed.METERS_PER_SECOND),
         # 5 ft/s * 0.3048 m/ft = 1.524 m/s
-        (5, SPEED_FEET_PER_SECOND, pytest.approx(1.524), SPEED_METERS_PER_SECOND),
+        (
+            5,
+            UnitOfSpeed.FEET_PER_SECOND,
+            pytest.approx(1.524),
+            UnitOfSpeed.METERS_PER_SECOND,
+        ),
     ],
 )
 def test_speed_convert(
@@ -449,12 +493,32 @@ def test_speed_convert(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (100, TEMP_CELSIUS, 212, TEMP_FAHRENHEIT),
-        (100, TEMP_CELSIUS, 373.15, TEMP_KELVIN),
-        (100, TEMP_FAHRENHEIT, pytest.approx(37.77777777777778), TEMP_CELSIUS),
-        (100, TEMP_FAHRENHEIT, pytest.approx(310.92777777777775), TEMP_KELVIN),
-        (100, TEMP_KELVIN, pytest.approx(-173.15), TEMP_CELSIUS),
-        (100, TEMP_KELVIN, pytest.approx(-279.66999999999996), TEMP_FAHRENHEIT),
+        (100, UnitOfTemperature.CELSIUS, 212, UnitOfTemperature.FAHRENHEIT),
+        (100, UnitOfTemperature.CELSIUS, 373.15, UnitOfTemperature.KELVIN),
+        (
+            100,
+            UnitOfTemperature.FAHRENHEIT,
+            pytest.approx(37.77777777777778),
+            UnitOfTemperature.CELSIUS,
+        ),
+        (
+            100,
+            UnitOfTemperature.FAHRENHEIT,
+            pytest.approx(310.92777777777775),
+            UnitOfTemperature.KELVIN,
+        ),
+        (
+            100,
+            UnitOfTemperature.KELVIN,
+            pytest.approx(-173.15),
+            UnitOfTemperature.CELSIUS,
+        ),
+        (
+            100,
+            UnitOfTemperature.KELVIN,
+            pytest.approx(-279.66999999999996),
+            UnitOfTemperature.FAHRENHEIT,
+        ),
     ],
 )
 def test_temperature_convert(
@@ -467,12 +531,22 @@ def test_temperature_convert(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (100, TEMP_CELSIUS, 180, TEMP_FAHRENHEIT),
-        (100, TEMP_CELSIUS, 100, TEMP_KELVIN),
-        (100, TEMP_FAHRENHEIT, pytest.approx(55.55555555555556), TEMP_CELSIUS),
-        (100, TEMP_FAHRENHEIT, pytest.approx(55.55555555555556), TEMP_KELVIN),
-        (100, TEMP_KELVIN, 100, TEMP_CELSIUS),
-        (100, TEMP_KELVIN, 180, TEMP_FAHRENHEIT),
+        (100, UnitOfTemperature.CELSIUS, 180, UnitOfTemperature.FAHRENHEIT),
+        (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN),
+        (
+            100,
+            UnitOfTemperature.FAHRENHEIT,
+            pytest.approx(55.55555555555556),
+            UnitOfTemperature.CELSIUS,
+        ),
+        (
+            100,
+            UnitOfTemperature.FAHRENHEIT,
+            pytest.approx(55.55555555555556),
+            UnitOfTemperature.KELVIN,
+        ),
+        (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS),
+        (100, UnitOfTemperature.KELVIN, 180, UnitOfTemperature.FAHRENHEIT),
     ],
 )
 def test_temperature_convert_with_interval(
@@ -485,40 +559,110 @@ def test_temperature_convert_with_interval(
 @pytest.mark.parametrize(
     "value,from_unit,expected,to_unit",
     [
-        (5, VOLUME_LITERS, pytest.approx(1.32086), VOLUME_GALLONS),
-        (5, VOLUME_GALLONS, pytest.approx(18.92706), VOLUME_LITERS),
-        (5, VOLUME_CUBIC_METERS, pytest.approx(176.5733335), VOLUME_CUBIC_FEET),
-        (500, VOLUME_CUBIC_FEET, pytest.approx(14.1584233), VOLUME_CUBIC_METERS),
-        (500, VOLUME_CUBIC_FEET, pytest.approx(14.1584233), VOLUME_CUBIC_METERS),
-        (500, VOLUME_CUBIC_FEET, pytest.approx(478753.2467), VOLUME_FLUID_OUNCE),
-        (500, VOLUME_CUBIC_FEET, pytest.approx(3740.25974), VOLUME_GALLONS),
-        (500, VOLUME_CUBIC_FEET, pytest.approx(14158.42329599), VOLUME_LITERS),
-        (500, VOLUME_CUBIC_FEET, pytest.approx(14158423.29599), VOLUME_MILLILITERS),
-        (500, VOLUME_CUBIC_METERS, 500, VOLUME_CUBIC_METERS),
-        (500, VOLUME_CUBIC_METERS, pytest.approx(16907011.35), VOLUME_FLUID_OUNCE),
-        (500, VOLUME_CUBIC_METERS, pytest.approx(132086.02617), VOLUME_GALLONS),
-        (500, VOLUME_CUBIC_METERS, 500000, VOLUME_LITERS),
-        (500, VOLUME_CUBIC_METERS, 500000000, VOLUME_MILLILITERS),
-        (500, VOLUME_FLUID_OUNCE, pytest.approx(0.52218967), VOLUME_CUBIC_FEET),
-        (500, VOLUME_FLUID_OUNCE, pytest.approx(0.014786764), VOLUME_CUBIC_METERS),
-        (500, VOLUME_FLUID_OUNCE, 3.90625, VOLUME_GALLONS),
-        (500, VOLUME_FLUID_OUNCE, pytest.approx(14.786764), VOLUME_LITERS),
-        (500, VOLUME_FLUID_OUNCE, pytest.approx(14786.764), VOLUME_MILLILITERS),
-        (500, VOLUME_GALLONS, pytest.approx(66.84027), VOLUME_CUBIC_FEET),
-        (500, VOLUME_GALLONS, pytest.approx(1.892706), VOLUME_CUBIC_METERS),
-        (500, VOLUME_GALLONS, 64000, VOLUME_FLUID_OUNCE),
-        (500, VOLUME_GALLONS, pytest.approx(1892.70589), VOLUME_LITERS),
-        (500, VOLUME_GALLONS, pytest.approx(1892705.89), VOLUME_MILLILITERS),
-        (500, VOLUME_LITERS, pytest.approx(17.65733), VOLUME_CUBIC_FEET),
-        (500, VOLUME_LITERS, 0.5, VOLUME_CUBIC_METERS),
-        (500, VOLUME_LITERS, pytest.approx(16907.011), VOLUME_FLUID_OUNCE),
-        (500, VOLUME_LITERS, pytest.approx(132.086), VOLUME_GALLONS),
-        (500, VOLUME_LITERS, 500000, VOLUME_MILLILITERS),
-        (500, VOLUME_MILLILITERS, pytest.approx(0.01765733), VOLUME_CUBIC_FEET),
-        (500, VOLUME_MILLILITERS, 0.0005, VOLUME_CUBIC_METERS),
-        (500, VOLUME_MILLILITERS, pytest.approx(16.907), VOLUME_FLUID_OUNCE),
-        (500, VOLUME_MILLILITERS, pytest.approx(0.132086), VOLUME_GALLONS),
-        (500, VOLUME_MILLILITERS, 0.5, VOLUME_LITERS),
+        (5, UnitOfVolume.LITERS, pytest.approx(1.32086), UnitOfVolume.GALLONS),
+        (5, UnitOfVolume.GALLONS, pytest.approx(18.92706), UnitOfVolume.LITERS),
+        (
+            5,
+            UnitOfVolume.CUBIC_METERS,
+            pytest.approx(176.5733335),
+            UnitOfVolume.CUBIC_FEET,
+        ),
+        (
+            500,
+            UnitOfVolume.CUBIC_FEET,
+            pytest.approx(14.1584233),
+            UnitOfVolume.CUBIC_METERS,
+        ),
+        (
+            500,
+            UnitOfVolume.CUBIC_FEET,
+            pytest.approx(14.1584233),
+            UnitOfVolume.CUBIC_METERS,
+        ),
+        (
+            500,
+            UnitOfVolume.CUBIC_FEET,
+            pytest.approx(478753.2467),
+            UnitOfVolume.FLUID_OUNCES,
+        ),
+        (500, UnitOfVolume.CUBIC_FEET, pytest.approx(3740.25974), UnitOfVolume.GALLONS),
+        (
+            500,
+            UnitOfVolume.CUBIC_FEET,
+            pytest.approx(14158.42329599),
+            UnitOfVolume.LITERS,
+        ),
+        (
+            500,
+            UnitOfVolume.CUBIC_FEET,
+            pytest.approx(14158423.29599),
+            UnitOfVolume.MILLILITERS,
+        ),
+        (500, UnitOfVolume.CUBIC_METERS, 500, UnitOfVolume.CUBIC_METERS),
+        (
+            500,
+            UnitOfVolume.CUBIC_METERS,
+            pytest.approx(16907011.35),
+            UnitOfVolume.FLUID_OUNCES,
+        ),
+        (
+            500,
+            UnitOfVolume.CUBIC_METERS,
+            pytest.approx(132086.02617),
+            UnitOfVolume.GALLONS,
+        ),
+        (500, UnitOfVolume.CUBIC_METERS, 500000, UnitOfVolume.LITERS),
+        (500, UnitOfVolume.CUBIC_METERS, 500000000, UnitOfVolume.MILLILITERS),
+        (
+            500,
+            UnitOfVolume.FLUID_OUNCES,
+            pytest.approx(0.52218967),
+            UnitOfVolume.CUBIC_FEET,
+        ),
+        (
+            500,
+            UnitOfVolume.FLUID_OUNCES,
+            pytest.approx(0.014786764),
+            UnitOfVolume.CUBIC_METERS,
+        ),
+        (500, UnitOfVolume.FLUID_OUNCES, 3.90625, UnitOfVolume.GALLONS),
+        (500, UnitOfVolume.FLUID_OUNCES, pytest.approx(14.786764), UnitOfVolume.LITERS),
+        (
+            500,
+            UnitOfVolume.FLUID_OUNCES,
+            pytest.approx(14786.764),
+            UnitOfVolume.MILLILITERS,
+        ),
+        (500, UnitOfVolume.GALLONS, pytest.approx(66.84027), UnitOfVolume.CUBIC_FEET),
+        (500, UnitOfVolume.GALLONS, pytest.approx(1.892706), UnitOfVolume.CUBIC_METERS),
+        (500, UnitOfVolume.GALLONS, 64000, UnitOfVolume.FLUID_OUNCES),
+        (500, UnitOfVolume.GALLONS, pytest.approx(1892.70589), UnitOfVolume.LITERS),
+        (
+            500,
+            UnitOfVolume.GALLONS,
+            pytest.approx(1892705.89),
+            UnitOfVolume.MILLILITERS,
+        ),
+        (500, UnitOfVolume.LITERS, pytest.approx(17.65733), UnitOfVolume.CUBIC_FEET),
+        (500, UnitOfVolume.LITERS, 0.5, UnitOfVolume.CUBIC_METERS),
+        (500, UnitOfVolume.LITERS, pytest.approx(16907.011), UnitOfVolume.FLUID_OUNCES),
+        (500, UnitOfVolume.LITERS, pytest.approx(132.086), UnitOfVolume.GALLONS),
+        (500, UnitOfVolume.LITERS, 500000, UnitOfVolume.MILLILITERS),
+        (
+            500,
+            UnitOfVolume.MILLILITERS,
+            pytest.approx(0.01765733),
+            UnitOfVolume.CUBIC_FEET,
+        ),
+        (500, UnitOfVolume.MILLILITERS, 0.0005, UnitOfVolume.CUBIC_METERS),
+        (
+            500,
+            UnitOfVolume.MILLILITERS,
+            pytest.approx(16.907),
+            UnitOfVolume.FLUID_OUNCES,
+        ),
+        (500, UnitOfVolume.MILLILITERS, pytest.approx(0.132086), UnitOfVolume.GALLONS),
+        (500, UnitOfVolume.MILLILITERS, 0.5, UnitOfVolume.LITERS),
     ],
 )
 def test_volume_convert(
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index db396738503..03385196cab 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -7,33 +7,17 @@ from homeassistant.components.sensor import SensorDeviceClass
 from homeassistant.const import (
     ACCUMULATED_PRECIPITATION,
     LENGTH,
-    LENGTH_CENTIMETERS,
-    LENGTH_FEET,
-    LENGTH_INCHES,
-    LENGTH_KILOMETERS,
-    LENGTH_METERS,
-    LENGTH_MILES,
-    LENGTH_MILLIMETERS,
-    LENGTH_YARD,
     MASS,
-    MASS_GRAMS,
     PRESSURE,
-    PRESSURE_PA,
-    SPEED_FEET_PER_SECOND,
-    SPEED_KILOMETERS_PER_HOUR,
-    SPEED_KNOTS,
-    SPEED_METERS_PER_SECOND,
-    SPEED_MILES_PER_HOUR,
-    TEMP_CELSIUS,
     TEMPERATURE,
     VOLUME,
-    VOLUME_CUBIC_FEET,
-    VOLUME_CUBIC_METERS,
-    VOLUME_FLUID_OUNCE,
-    VOLUME_GALLONS,
-    VOLUME_LITERS,
-    VOLUME_MILLILITERS,
     WIND_SPEED,
+    UnitOfLength,
+    UnitOfMass,
+    UnitOfPressure,
+    UnitOfSpeed,
+    UnitOfTemperature,
+    UnitOfVolume,
 )
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.util.unit_system import (
@@ -56,79 +40,79 @@ def test_invalid_units():
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            accumulated_precipitation=UnitOfLength.MILLIMETERS,
             conversions={},
-            length=LENGTH_METERS,
-            mass=MASS_GRAMS,
-            pressure=PRESSURE_PA,
+            length=UnitOfLength.METERS,
+            mass=UnitOfMass.GRAMS,
+            pressure=UnitOfPressure.PA,
             temperature=INVALID_UNIT,
-            volume=VOLUME_LITERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
+            volume=UnitOfVolume.LITERS,
+            wind_speed=UnitOfSpeed.METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            accumulated_precipitation=UnitOfLength.MILLIMETERS,
             conversions={},
             length=INVALID_UNIT,
-            mass=MASS_GRAMS,
-            pressure=PRESSURE_PA,
-            temperature=TEMP_CELSIUS,
-            volume=VOLUME_LITERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
+            mass=UnitOfMass.GRAMS,
+            pressure=UnitOfPressure.PA,
+            temperature=UnitOfTemperature.CELSIUS,
+            volume=UnitOfVolume.LITERS,
+            wind_speed=UnitOfSpeed.METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            accumulated_precipitation=UnitOfLength.MILLIMETERS,
             conversions={},
-            length=LENGTH_METERS,
-            mass=MASS_GRAMS,
-            pressure=PRESSURE_PA,
-            temperature=TEMP_CELSIUS,
-            volume=VOLUME_LITERS,
+            length=UnitOfLength.METERS,
+            mass=UnitOfMass.GRAMS,
+            pressure=UnitOfPressure.PA,
+            temperature=UnitOfTemperature.CELSIUS,
+            volume=UnitOfVolume.LITERS,
             wind_speed=INVALID_UNIT,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            accumulated_precipitation=UnitOfLength.MILLIMETERS,
             conversions={},
-            length=LENGTH_METERS,
-            mass=MASS_GRAMS,
-            pressure=PRESSURE_PA,
-            temperature=TEMP_CELSIUS,
+            length=UnitOfLength.METERS,
+            mass=UnitOfMass.GRAMS,
+            pressure=UnitOfPressure.PA,
+            temperature=UnitOfTemperature.CELSIUS,
             volume=INVALID_UNIT,
-            wind_speed=SPEED_METERS_PER_SECOND,
+            wind_speed=UnitOfSpeed.METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            accumulated_precipitation=UnitOfLength.MILLIMETERS,
             conversions={},
-            length=LENGTH_METERS,
+            length=UnitOfLength.METERS,
             mass=INVALID_UNIT,
-            pressure=PRESSURE_PA,
-            temperature=TEMP_CELSIUS,
-            volume=VOLUME_LITERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
+            pressure=UnitOfPressure.PA,
+            temperature=UnitOfTemperature.CELSIUS,
+            volume=UnitOfVolume.LITERS,
+            wind_speed=UnitOfSpeed.METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
         UnitSystem(
             SYSTEM_NAME,
-            accumulated_precipitation=LENGTH_MILLIMETERS,
+            accumulated_precipitation=UnitOfLength.MILLIMETERS,
             conversions={},
-            length=LENGTH_METERS,
-            mass=MASS_GRAMS,
+            length=UnitOfLength.METERS,
+            mass=UnitOfMass.GRAMS,
             pressure=INVALID_UNIT,
-            temperature=TEMP_CELSIUS,
-            volume=VOLUME_LITERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
+            temperature=UnitOfTemperature.CELSIUS,
+            volume=UnitOfVolume.LITERS,
+            wind_speed=UnitOfSpeed.METERS_PER_SECOND,
         )
 
     with pytest.raises(ValueError):
@@ -136,41 +120,41 @@ def test_invalid_units():
             SYSTEM_NAME,
             accumulated_precipitation=INVALID_UNIT,
             conversions={},
-            length=LENGTH_METERS,
-            mass=MASS_GRAMS,
-            pressure=PRESSURE_PA,
-            temperature=TEMP_CELSIUS,
-            volume=VOLUME_LITERS,
-            wind_speed=SPEED_METERS_PER_SECOND,
+            length=UnitOfLength.METERS,
+            mass=UnitOfMass.GRAMS,
+            pressure=UnitOfPressure.PA,
+            temperature=UnitOfTemperature.CELSIUS,
+            volume=UnitOfVolume.LITERS,
+            wind_speed=UnitOfSpeed.METERS_PER_SECOND,
         )
 
 
 def test_invalid_value():
     """Test no conversion happens if value is non-numeric."""
     with pytest.raises(TypeError):
-        METRIC_SYSTEM.length("25a", LENGTH_KILOMETERS)
+        METRIC_SYSTEM.length("25a", UnitOfLength.KILOMETERS)
     with pytest.raises(TypeError):
-        METRIC_SYSTEM.temperature("50K", TEMP_CELSIUS)
+        METRIC_SYSTEM.temperature("50K", UnitOfTemperature.CELSIUS)
     with pytest.raises(TypeError):
-        METRIC_SYSTEM.wind_speed("50km/h", SPEED_METERS_PER_SECOND)
+        METRIC_SYSTEM.wind_speed("50km/h", UnitOfSpeed.METERS_PER_SECOND)
     with pytest.raises(TypeError):
-        METRIC_SYSTEM.volume("50L", VOLUME_LITERS)
+        METRIC_SYSTEM.volume("50L", UnitOfVolume.LITERS)
     with pytest.raises(TypeError):
-        METRIC_SYSTEM.pressure("50Pa", PRESSURE_PA)
+        METRIC_SYSTEM.pressure("50Pa", UnitOfPressure.PA)
     with pytest.raises(TypeError):
-        METRIC_SYSTEM.accumulated_precipitation("50mm", LENGTH_MILLIMETERS)
+        METRIC_SYSTEM.accumulated_precipitation("50mm", UnitOfLength.MILLIMETERS)
 
 
 def test_as_dict():
     """Test that the as_dict() method returns the expected dictionary."""
     expected = {
-        LENGTH: LENGTH_KILOMETERS,
-        WIND_SPEED: SPEED_METERS_PER_SECOND,
-        TEMPERATURE: TEMP_CELSIUS,
-        VOLUME: VOLUME_LITERS,
-        MASS: MASS_GRAMS,
-        PRESSURE: PRESSURE_PA,
-        ACCUMULATED_PRECIPITATION: LENGTH_MILLIMETERS,
+        LENGTH: UnitOfLength.KILOMETERS,
+        WIND_SPEED: UnitOfSpeed.METERS_PER_SECOND,
+        TEMPERATURE: UnitOfTemperature.CELSIUS,
+        VOLUME: UnitOfVolume.LITERS,
+        MASS: UnitOfMass.GRAMS,
+        PRESSURE: UnitOfPressure.PA,
+        ACCUMULATED_PRECIPITATION: UnitOfLength.MILLIMETERS,
     }
 
     assert expected == METRIC_SYSTEM.as_dict()
@@ -318,13 +302,13 @@ def test_accumulated_precipitation_to_imperial():
 
 def test_properties():
     """Test the unit properties are returned as expected."""
-    assert METRIC_SYSTEM.length_unit == LENGTH_KILOMETERS
-    assert METRIC_SYSTEM.wind_speed_unit == SPEED_METERS_PER_SECOND
-    assert METRIC_SYSTEM.temperature_unit == TEMP_CELSIUS
-    assert METRIC_SYSTEM.mass_unit == MASS_GRAMS
-    assert METRIC_SYSTEM.volume_unit == VOLUME_LITERS
-    assert METRIC_SYSTEM.pressure_unit == PRESSURE_PA
-    assert METRIC_SYSTEM.accumulated_precipitation_unit == LENGTH_MILLIMETERS
+    assert METRIC_SYSTEM.length_unit == UnitOfLength.KILOMETERS
+    assert METRIC_SYSTEM.wind_speed_unit == UnitOfSpeed.METERS_PER_SECOND
+    assert METRIC_SYSTEM.temperature_unit == UnitOfTemperature.CELSIUS
+    assert METRIC_SYSTEM.mass_unit == UnitOfMass.GRAMS
+    assert METRIC_SYSTEM.volume_unit == UnitOfVolume.LITERS
+    assert METRIC_SYSTEM.pressure_unit == UnitOfPressure.PA
+    assert METRIC_SYSTEM.accumulated_precipitation_unit == UnitOfLength.MILLIMETERS
 
 
 @pytest.mark.parametrize(
@@ -397,36 +381,44 @@ def test_get_unit_system_invalid(key: str) -> None:
     "device_class, original_unit, state_unit",
     (
         # Test distance conversion
-        (SensorDeviceClass.DISTANCE, LENGTH_FEET, LENGTH_METERS),
-        (SensorDeviceClass.DISTANCE, LENGTH_INCHES, LENGTH_MILLIMETERS),
-        (SensorDeviceClass.DISTANCE, LENGTH_MILES, LENGTH_KILOMETERS),
-        (SensorDeviceClass.DISTANCE, LENGTH_YARD, LENGTH_METERS),
-        (SensorDeviceClass.DISTANCE, LENGTH_KILOMETERS, None),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.FEET, UnitOfLength.METERS),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.INCHES, UnitOfLength.MILLIMETERS),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.MILES, UnitOfLength.KILOMETERS),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.YARDS, UnitOfLength.METERS),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.KILOMETERS, None),
         (SensorDeviceClass.DISTANCE, "very_long", None),
         # Test gas meter conversion
-        (SensorDeviceClass.GAS, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
-        (SensorDeviceClass.GAS, VOLUME_CUBIC_METERS, None),
+        (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS),
+        (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, None),
         (SensorDeviceClass.GAS, "very_much", None),
         # Test speed conversion
-        (SensorDeviceClass.SPEED, SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR),
-        (SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR),
-        (SensorDeviceClass.SPEED, SPEED_KILOMETERS_PER_HOUR, None),
-        (SensorDeviceClass.SPEED, SPEED_KNOTS, None),
-        (SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, None),
+        (
+            SensorDeviceClass.SPEED,
+            UnitOfSpeed.FEET_PER_SECOND,
+            UnitOfSpeed.KILOMETERS_PER_HOUR,
+        ),
+        (
+            SensorDeviceClass.SPEED,
+            UnitOfSpeed.MILES_PER_HOUR,
+            UnitOfSpeed.KILOMETERS_PER_HOUR,
+        ),
+        (SensorDeviceClass.SPEED, UnitOfSpeed.KILOMETERS_PER_HOUR, None),
+        (SensorDeviceClass.SPEED, UnitOfSpeed.KNOTS, None),
+        (SensorDeviceClass.SPEED, UnitOfSpeed.METERS_PER_SECOND, None),
         (SensorDeviceClass.SPEED, "very_fast", None),
         # Test volume conversion
-        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
-        (SensorDeviceClass.VOLUME, VOLUME_FLUID_OUNCE, VOLUME_MILLILITERS),
-        (SensorDeviceClass.VOLUME, VOLUME_GALLONS, VOLUME_LITERS),
-        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_METERS, None),
-        (SensorDeviceClass.VOLUME, VOLUME_LITERS, None),
-        (SensorDeviceClass.VOLUME, VOLUME_MILLILITERS, None),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.FLUID_OUNCES, UnitOfVolume.MILLILITERS),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.GALLONS, UnitOfVolume.LITERS),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.CUBIC_METERS, None),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.LITERS, None),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.MILLILITERS, None),
         (SensorDeviceClass.VOLUME, "very_much", None),
         # Test water meter conversion
-        (SensorDeviceClass.WATER, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS),
-        (SensorDeviceClass.WATER, VOLUME_GALLONS, VOLUME_LITERS),
-        (SensorDeviceClass.WATER, VOLUME_CUBIC_METERS, None),
-        (SensorDeviceClass.WATER, VOLUME_LITERS, None),
+        (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS),
+        (SensorDeviceClass.WATER, UnitOfVolume.GALLONS, UnitOfVolume.LITERS),
+        (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_METERS, None),
+        (SensorDeviceClass.WATER, UnitOfVolume.LITERS, None),
         (SensorDeviceClass.WATER, "very_much", None),
     ),
 )
@@ -444,36 +436,44 @@ def test_get_metric_converted_unit_(
     "device_class, original_unit, state_unit",
     (
         # Test distance conversion
-        (SensorDeviceClass.DISTANCE, LENGTH_CENTIMETERS, LENGTH_INCHES),
-        (SensorDeviceClass.DISTANCE, LENGTH_KILOMETERS, LENGTH_MILES),
-        (SensorDeviceClass.DISTANCE, LENGTH_METERS, LENGTH_FEET),
-        (SensorDeviceClass.DISTANCE, LENGTH_MILLIMETERS, LENGTH_INCHES),
-        (SensorDeviceClass.DISTANCE, LENGTH_MILES, None),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.CENTIMETERS, UnitOfLength.INCHES),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.KILOMETERS, UnitOfLength.MILES),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.METERS, UnitOfLength.FEET),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.MILLIMETERS, UnitOfLength.INCHES),
+        (SensorDeviceClass.DISTANCE, UnitOfLength.MILES, None),
         (SensorDeviceClass.DISTANCE, "very_long", None),
         # Test gas meter conversion
-        (SensorDeviceClass.GAS, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
-        (SensorDeviceClass.GAS, VOLUME_CUBIC_FEET, None),
+        (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
+        (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, None),
         (SensorDeviceClass.GAS, "very_much", None),
         # Test speed conversion
-        (SensorDeviceClass.SPEED, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR),
-        (SensorDeviceClass.SPEED, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR),
-        (SensorDeviceClass.SPEED, SPEED_FEET_PER_SECOND, None),
-        (SensorDeviceClass.SPEED, SPEED_KNOTS, None),
-        (SensorDeviceClass.SPEED, SPEED_MILES_PER_HOUR, None),
+        (
+            SensorDeviceClass.SPEED,
+            UnitOfSpeed.METERS_PER_SECOND,
+            UnitOfSpeed.MILES_PER_HOUR,
+        ),
+        (
+            SensorDeviceClass.SPEED,
+            UnitOfSpeed.KILOMETERS_PER_HOUR,
+            UnitOfSpeed.MILES_PER_HOUR,
+        ),
+        (SensorDeviceClass.SPEED, UnitOfSpeed.FEET_PER_SECOND, None),
+        (SensorDeviceClass.SPEED, UnitOfSpeed.KNOTS, None),
+        (SensorDeviceClass.SPEED, UnitOfSpeed.MILES_PER_HOUR, None),
         (SensorDeviceClass.SPEED, "very_fast", None),
         # Test volume conversion
-        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
-        (SensorDeviceClass.VOLUME, VOLUME_LITERS, VOLUME_GALLONS),
-        (SensorDeviceClass.VOLUME, VOLUME_MILLILITERS, VOLUME_FLUID_OUNCE),
-        (SensorDeviceClass.VOLUME, VOLUME_CUBIC_FEET, None),
-        (SensorDeviceClass.VOLUME, VOLUME_FLUID_OUNCE, None),
-        (SensorDeviceClass.VOLUME, VOLUME_GALLONS, None),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.LITERS, UnitOfVolume.GALLONS),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.MILLILITERS, UnitOfVolume.FLUID_OUNCES),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.CUBIC_FEET, None),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.FLUID_OUNCES, None),
+        (SensorDeviceClass.VOLUME, UnitOfVolume.GALLONS, None),
         (SensorDeviceClass.VOLUME, "very_much", None),
         # Test water meter conversion
-        (SensorDeviceClass.WATER, VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
-        (SensorDeviceClass.WATER, VOLUME_LITERS, VOLUME_GALLONS),
-        (SensorDeviceClass.WATER, VOLUME_CUBIC_FEET, None),
-        (SensorDeviceClass.WATER, VOLUME_GALLONS, None),
+        (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
+        (SensorDeviceClass.WATER, UnitOfVolume.LITERS, UnitOfVolume.GALLONS),
+        (SensorDeviceClass.WATER, UnitOfVolume.CUBIC_FEET, None),
+        (SensorDeviceClass.WATER, UnitOfVolume.GALLONS, None),
         (SensorDeviceClass.WATER, "very_much", None),
     ),
 )
-- 
GitLab


From 0321c8bdc5cdf70346ed3c06152f9434e78fb7be Mon Sep 17 00:00:00 2001
From: Joakim Plate <elupus@ecce.se>
Date: Wed, 26 Oct 2022 21:04:11 +0200
Subject: [PATCH 870/985] Allow device class for switch to be set in knx
 (#81039)

---
 homeassistant/components/knx/schema.py | 4 ++++
 homeassistant/components/knx/switch.py | 2 ++
 2 files changed, 6 insertions(+)

diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py
index 55602d6153a..c1615b7e8e2 100644
--- a/homeassistant/components/knx/schema.py
+++ b/homeassistant/components/knx/schema.py
@@ -22,6 +22,9 @@ from homeassistant.components.cover import (
 )
 from homeassistant.components.number import NumberMode
 from homeassistant.components.sensor import CONF_STATE_CLASS, STATE_CLASSES_SCHEMA
+from homeassistant.components.switch import (
+    DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA,
+)
 from homeassistant.const import (
     CONF_DEVICE_CLASS,
     CONF_ENTITY_CATEGORY,
@@ -873,6 +876,7 @@ class SwitchSchema(KNXPlatformSchema):
             vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
             vol.Required(KNX_ADDRESS): ga_list_validator,
             vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
+            vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
             vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
         }
     )
diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py
index 9f4eb6fc632..81f8de815c9 100644
--- a/homeassistant/components/knx/switch.py
+++ b/homeassistant/components/knx/switch.py
@@ -9,6 +9,7 @@ from xknx.devices import Switch as XknxSwitch
 from homeassistant import config_entries
 from homeassistant.components.switch import SwitchEntity
 from homeassistant.const import (
+    CONF_DEVICE_CLASS,
     CONF_ENTITY_CATEGORY,
     CONF_NAME,
     STATE_ON,
@@ -56,6 +57,7 @@ class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity):
             )
         )
         self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
+        self._attr_device_class = config.get(CONF_DEVICE_CLASS)
         self._attr_unique_id = str(self._device.switch.group_address)
 
     async def async_added_to_hass(self) -> None:
-- 
GitLab


From a6034411809e6b06954a58aca5134eef5bd4c9a0 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
Date: Wed, 26 Oct 2022 21:04:41 +0200
Subject: [PATCH 871/985] Add support for water usage Flume (#81041)

---
 homeassistant/components/flume/sensor.py | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py
index 703744c248e..5b8dbde69e4 100644
--- a/homeassistant/components/flume/sensor.py
+++ b/homeassistant/components/flume/sensor.py
@@ -7,6 +7,7 @@ from homeassistant.components.sensor import (
     SensorDeviceClass,
     SensorEntity,
     SensorEntityDescription,
+    SensorStateClass,
 )
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import VOLUME_GALLONS
@@ -40,34 +41,40 @@ FLUME_QUERIES_SENSOR: tuple[SensorEntityDescription, ...] = (
         key="month_to_date",
         name="Current Month",
         native_unit_of_measurement=VOLUME_GALLONS,
-        device_class=SensorDeviceClass.VOLUME,
+        device_class=SensorDeviceClass.WATER,
+        state_class=SensorStateClass.TOTAL_INCREASING,
     ),
     SensorEntityDescription(
         key="week_to_date",
         name="Current Week",
         native_unit_of_measurement=VOLUME_GALLONS,
-        device_class=SensorDeviceClass.VOLUME,
+        device_class=SensorDeviceClass.WATER,
+        state_class=SensorStateClass.TOTAL_INCREASING,
     ),
     SensorEntityDescription(
         key="today",
         name="Current Day",
         native_unit_of_measurement=VOLUME_GALLONS,
-        device_class=SensorDeviceClass.VOLUME,
+        device_class=SensorDeviceClass.WATER,
+        state_class=SensorStateClass.TOTAL_INCREASING,
     ),
     SensorEntityDescription(
         key="last_60_min",
         name="60 Minutes",
         native_unit_of_measurement=f"{VOLUME_GALLONS}/h",
+        state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
         key="last_24_hrs",
         name="24 Hours",
         native_unit_of_measurement=f"{VOLUME_GALLONS}/d",
+        state_class=SensorStateClass.MEASUREMENT,
     ),
     SensorEntityDescription(
         key="last_30_days",
         name="30 Days",
         native_unit_of_measurement=f"{VOLUME_GALLONS}/mo",
+        state_class=SensorStateClass.MEASUREMENT,
     ),
 )
 
-- 
GitLab


From a4310d2085a554547b3e97b4ecee90c9b5dad959 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 26 Oct 2022 21:11:28 +0200
Subject: [PATCH 872/985] Allow integrations to drop custom unit conversion
 (#81005)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
---
 homeassistant/components/sensor/__init__.py | 60 ++++++++++++++++++---
 tests/components/sensor/test_init.py        | 54 +++++++++++++++++++
 2 files changed, 108 insertions(+), 6 deletions(-)

diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index e18bfbb178d..6ba88defc83 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -1,6 +1,7 @@
 """Component to interface with various sensors that can be monitored."""
 from __future__ import annotations
 
+import asyncio
 from collections.abc import Mapping
 from contextlib import suppress
 from dataclasses import dataclass
@@ -56,6 +57,7 @@ from homeassistant.helpers.config_validation import (  # noqa: F401
 )
 from homeassistant.helpers.entity import Entity, EntityDescription
 from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity_platform import EntityPlatform
 from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
 from homeassistant.helpers.typing import ConfigType, StateType
 from homeassistant.util import dt as dt_util
@@ -453,6 +455,47 @@ class SensorEntity(Entity):
     _last_reset_reported = False
     _sensor_option_unit_of_measurement: str | None = None
 
+    @callback
+    def add_to_platform_start(
+        self,
+        hass: HomeAssistant,
+        platform: EntityPlatform,
+        parallel_updates: asyncio.Semaphore | None,
+    ) -> None:
+        """Start adding an entity to a platform."""
+        super().add_to_platform_start(hass, platform, parallel_updates)
+
+        if self.unique_id is None:
+            return
+        registry = er.async_get(self.hass)
+        if not (
+            entity_id := registry.async_get_entity_id(
+                platform.domain, platform.platform_name, self.unique_id
+            )
+        ):
+            return
+        registry_entry = registry.async_get(entity_id)
+        assert registry_entry
+
+        # Store unit override according to automatic unit conversion rules if:
+        # - no unit override is stored in the entity registry
+        # - units have changed
+        # - the unit stored in the registry matches automatic unit conversion rules
+        # This allows integrations to drop custom unit conversion and rely on automatic
+        # conversion.
+        registry_unit = registry_entry.unit_of_measurement
+        if (
+            DOMAIN not in registry_entry.options
+            and f"{DOMAIN}.private" not in registry_entry.options
+            and self.unit_of_measurement != registry_unit
+            and (suggested_unit := self._get_initial_suggested_unit()) == registry_unit
+        ):
+            registry.async_update_entity_options(
+                entity_id,
+                f"{DOMAIN}.private",
+                {"suggested_unit_of_measurement": suggested_unit},
+            )
+
     async def async_internal_added_to_hass(self) -> None:
         """Call when the sensor entity is added to hass."""
         await super().async_internal_added_to_hass()
@@ -495,12 +538,8 @@ class SensorEntity(Entity):
 
         return None
 
-    def get_initial_entity_options(self) -> er.EntityOptionsType | None:
-        """Return initial entity options.
-
-        These will be stored in the entity registry the first time the entity is seen,
-        and then never updated.
-        """
+    def _get_initial_suggested_unit(self) -> str | None:
+        """Return initial suggested unit of measurement."""
         # Unit suggested by the integration
         suggested_unit_of_measurement = self.suggested_unit_of_measurement
 
@@ -510,6 +549,15 @@ class SensorEntity(Entity):
                 self.device_class, self.native_unit_of_measurement
             )
 
+        return suggested_unit_of_measurement
+
+    def get_initial_entity_options(self) -> er.EntityOptionsType | None:
+        """Return initial entity options.
+
+        These will be stored in the entity registry the first time the entity is seen,
+        and then never updated.
+        """
+        suggested_unit_of_measurement = self._get_initial_suggested_unit()
         if suggested_unit_of_measurement is None:
             return None
 
diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py
index 9bfa6fc46ba..e168a1c2271 100644
--- a/tests/components/sensor/test_init.py
+++ b/tests/components/sensor/test_init.py
@@ -873,3 +873,57 @@ async def test_unit_conversion_priority_suggested_unit_change(
     state = hass.states.get(entity1.entity_id)
     assert float(state.state) == approx(float(original_value))
     assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit
+
+
+@pytest.mark.parametrize(
+    "unit_system, native_unit, original_unit, native_value, original_value, device_class",
+    [
+        # Distance
+        (
+            US_CUSTOMARY_SYSTEM,
+            LENGTH_KILOMETERS,
+            LENGTH_MILES,
+            1000,
+            621,
+            SensorDeviceClass.DISTANCE,
+        ),
+    ],
+)
+async def test_unit_conversion_priority_legacy_conversion_removed(
+    hass,
+    enable_custom_integrations,
+    unit_system,
+    native_unit,
+    original_unit,
+    native_value,
+    original_value,
+    device_class,
+):
+    """Test priority of unit conversion."""
+
+    hass.config.units = unit_system
+
+    entity_registry = er.async_get(hass)
+    platform = getattr(hass.components, "test.sensor")
+    platform.init(empty=True)
+
+    # Pre-register entities
+    entity_registry.async_get_or_create(
+        "sensor", "test", "very_unique", unit_of_measurement=original_unit
+    )
+
+    platform.ENTITIES["0"] = platform.MockSensor(
+        name="Test",
+        device_class=device_class,
+        native_unit_of_measurement=native_unit,
+        native_value=str(native_value),
+        unique_id="very_unique",
+    )
+    entity0 = platform.ENTITIES["0"]
+
+    assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+    await hass.async_block_till_done()
+
+    state = hass.states.get(entity0.entity_id)
+    assert float(state.state) == approx(float(original_value))
+    assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == original_unit
-- 
GitLab


From 95fc641949d59504d386399f9f9a1a6c1080ed6b Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 26 Oct 2022 21:20:52 +0200
Subject: [PATCH 873/985] Add support to the energy integration for tracking
 water usage (#80888)

---
 homeassistant/components/energy/data.py     |  35 ++++-
 homeassistant/components/energy/sensor.py   |  22 ++-
 homeassistant/components/energy/validate.py |  67 +++++++++
 tests/components/energy/test_sensor.py      |  51 +++++++
 tests/components/energy/test_validate.py    | 159 ++++++++++++++++++++
 5 files changed, 330 insertions(+), 4 deletions(-)

diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py
index bc7903203c4..339c0c638e2 100644
--- a/homeassistant/components/energy/data.py
+++ b/homeassistant/components/energy/data.py
@@ -87,13 +87,13 @@ class BatterySourceType(TypedDict):
 
 
 class GasSourceType(TypedDict):
-    """Dictionary holding the source of gas storage."""
+    """Dictionary holding the source of gas consumption."""
 
     type: Literal["gas"]
 
     stat_energy_from: str
 
-    # statistic_id of costs ($) incurred from the energy meter
+    # statistic_id of costs ($) incurred from the gas meter
     # If set to None and entity_energy_price or number_energy_price are configured,
     # an EnergyCostSensor will be automatically created
     stat_cost: str | None
@@ -103,7 +103,26 @@ class GasSourceType(TypedDict):
     number_energy_price: float | None  # Price for energy ($/m³)
 
 
-SourceType = Union[GridSourceType, SolarSourceType, BatterySourceType, GasSourceType]
+class WaterSourceType(TypedDict):
+    """Dictionary holding the source of water consumption."""
+
+    type: Literal["water"]
+
+    stat_energy_from: str
+
+    # statistic_id of costs ($) incurred from the water meter
+    # If set to None and entity_energy_price or number_energy_price are configured,
+    # an EnergyCostSensor will be automatically created
+    stat_cost: str | None
+
+    # Used to generate costs if stat_cost is set to None
+    entity_energy_price: str | None  # entity_id of an entity providing price ($/m³)
+    number_energy_price: float | None  # Price for energy ($/m³)
+
+
+SourceType = Union[
+    GridSourceType, SolarSourceType, BatterySourceType, GasSourceType, WaterSourceType
+]
 
 
 class DeviceConsumption(TypedDict):
@@ -221,6 +240,15 @@ GAS_SOURCE_SCHEMA = vol.Schema(
         vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
     }
 )
+WATER_SOURCE_SCHEMA = vol.Schema(
+    {
+        vol.Required("type"): "water",
+        vol.Required("stat_energy_from"): str,
+        vol.Optional("stat_cost"): vol.Any(str, None),
+        vol.Optional("entity_energy_price"): vol.Any(str, None),
+        vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
+    }
+)
 
 
 def check_type_limits(value: list[SourceType]) -> list[SourceType]:
@@ -243,6 +271,7 @@ ENERGY_SOURCE_SCHEMA = vol.All(
                     "solar": SOLAR_SOURCE_SCHEMA,
                     "battery": BATTERY_SOURCE_SCHEMA,
                     "gas": GAS_SOURCE_SCHEMA,
+                    "water": WATER_SOURCE_SCHEMA,
                 },
             )
         ]
diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py
index 540642a89da..c97b67287d1 100644
--- a/homeassistant/components/energy/sensor.py
+++ b/homeassistant/components/energy/sensor.py
@@ -19,6 +19,8 @@ from homeassistant.const import (
     ATTR_UNIT_OF_MEASUREMENT,
     VOLUME_CUBIC_FEET,
     VOLUME_CUBIC_METERS,
+    VOLUME_GALLONS,
+    VOLUME_LITERS,
     UnitOfEnergy,
 )
 from homeassistant.core import (
@@ -49,6 +51,12 @@ VALID_ENERGY_UNITS = [
     UnitOfEnergy.GIGA_JOULE,
 ]
 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__)
 
 
@@ -67,7 +75,7 @@ async def async_setup_platform(
 class SourceAdapter:
     """Adapter to allow sources and their flows to be used as sensors."""
 
-    source_type: Literal["grid", "gas"]
+    source_type: Literal["grid", "gas", "water"]
     flow_type: Literal["flow_from", "flow_to", None]
     stat_energy_key: Literal["stat_energy_from", "stat_energy_to"]
     total_money_key: Literal["stat_cost", "stat_compensation"]
@@ -100,6 +108,14 @@ SOURCE_ADAPTERS: Final = (
         "Cost",
         "cost",
     ),
+    SourceAdapter(
+        "water",
+        None,
+        "stat_energy_from",
+        "stat_cost",
+        "Cost",
+        "cost",
+    ),
 )
 
 
@@ -316,6 +332,10 @@ class EnergyCostSensor(SensorEntity):
             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:
diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py
index f31eb53fb37..cf4ff3ef63e 100644
--- a/homeassistant/components/energy/validate.py
+++ b/homeassistant/components/energy/validate.py
@@ -13,6 +13,8 @@ from homeassistant.const import (
     STATE_UNKNOWN,
     VOLUME_CUBIC_FEET,
     VOLUME_CUBIC_METERS,
+    VOLUME_GALLONS,
+    VOLUME_LITERS,
     UnitOfEnergy,
 )
 from homeassistant.core import HomeAssistant, callback, valid_entity_id
@@ -52,6 +54,20 @@ GAS_PRICE_UNITS = tuple(
 )
 GAS_UNIT_ERROR = "entity_unexpected_unit_gas"
 GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price"
+WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,)
+WATER_USAGE_UNITS = {
+    sensor.SensorDeviceClass.WATER: (
+        VOLUME_CUBIC_METERS,
+        VOLUME_CUBIC_FEET,
+        VOLUME_GALLONS,
+        VOLUME_LITERS,
+    ),
+}
+WATER_PRICE_UNITS = tuple(
+    f"/{unit}" for units in WATER_USAGE_UNITS.values() for unit in units
+)
+WATER_UNIT_ERROR = "entity_unexpected_unit_water"
+WATER_PRICE_UNIT_ERROR = "entity_unexpected_unit_water_price"
 
 
 @dataclasses.dataclass
@@ -437,6 +453,57 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
                     )
                 )
 
+        elif source["type"] == "water":
+            wanted_statistics_metadata.add(source["stat_energy_from"])
+            validate_calls.append(
+                functools.partial(
+                    _async_validate_usage_stat,
+                    hass,
+                    statistics_metadata,
+                    source["stat_energy_from"],
+                    WATER_USAGE_DEVICE_CLASSES,
+                    WATER_USAGE_UNITS,
+                    WATER_UNIT_ERROR,
+                    source_result,
+                )
+            )
+
+            if (stat_cost := source.get("stat_cost")) is not None:
+                wanted_statistics_metadata.add(stat_cost)
+                validate_calls.append(
+                    functools.partial(
+                        _async_validate_cost_stat,
+                        hass,
+                        statistics_metadata,
+                        stat_cost,
+                        source_result,
+                    )
+                )
+            elif source.get("entity_energy_price") is not None:
+                validate_calls.append(
+                    functools.partial(
+                        _async_validate_price_entity,
+                        hass,
+                        source["entity_energy_price"],
+                        source_result,
+                        WATER_PRICE_UNITS,
+                        WATER_PRICE_UNIT_ERROR,
+                    )
+                )
+
+            if (
+                source.get("entity_energy_price") is not None
+                or source.get("number_energy_price") is not None
+            ):
+                validate_calls.append(
+                    functools.partial(
+                        _async_validate_auto_generated_cost_entity,
+                        hass,
+                        source["stat_energy_from"],
+                        source_result,
+                    )
+                )
+
         elif source["type"] == "solar":
             wanted_statistics_metadata.add(source["stat_energy_from"])
             validate_calls.append(
diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py
index 3b954e8d62b..14a04ea74c6 100644
--- a/tests/components/energy/test_sensor.py
+++ b/tests/components/energy/test_sensor.py
@@ -933,6 +933,57 @@ async def test_cost_sensor_handle_gas_kwh(
     assert state.state == "50.0"
 
 
+@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS))
+async def test_cost_sensor_handle_water(
+    setup_integration, hass, hass_storage, unit
+) -> None:
+    """Test water cost price from sensor entity."""
+    energy_attributes = {
+        ATTR_UNIT_OF_MEASUREMENT: unit,
+        ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
+    }
+    energy_data = data.EnergyManager.default_preferences()
+    energy_data["energy_sources"].append(
+        {
+            "type": "water",
+            "stat_energy_from": "sensor.water_consumption",
+            "stat_cost": None,
+            "entity_energy_price": None,
+            "number_energy_price": 0.5,
+        }
+    )
+
+    hass_storage[data.STORAGE_KEY] = {
+        "version": 1,
+        "data": energy_data,
+    }
+
+    now = dt_util.utcnow()
+
+    hass.states.async_set(
+        "sensor.water_consumption",
+        100,
+        energy_attributes,
+    )
+
+    with patch("homeassistant.util.dt.utcnow", return_value=now):
+        await setup_integration(hass)
+
+    state = hass.states.get("sensor.water_consumption_cost")
+    assert state.state == "0.0"
+
+    # water use bumped to 200 ft³/m³
+    hass.states.async_set(
+        "sensor.water_consumption",
+        200,
+        energy_attributes,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("sensor.water_consumption_cost")
+    assert state.state == "50.0"
+
+
 @pytest.mark.parametrize("state_class", [None])
 async def test_cost_sensor_wrong_state_class(
     setup_integration, hass, hass_storage, caplog, state_class
diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py
index c8aa4299484..f1e626c24d5 100644
--- a/tests/components/energy/test_validate.py
+++ b/tests/components/energy/test_validate.py
@@ -946,3 +946,162 @@ async def test_validation_grid_no_costs_tracking(
         "energy_sources": [[]],
         "device_consumption": [],
     }
+
+
+async def test_validation_water(
+    hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
+):
+    """Test validating water with sensors for energy and cost/compensation."""
+    mock_is_entity_recorded["sensor.water_cost_1"] = False
+    mock_is_entity_recorded["sensor.water_compensation_1"] = False
+    await mock_energy_manager.async_update(
+        {
+            "energy_sources": [
+                {
+                    "type": "water",
+                    "stat_energy_from": "sensor.water_consumption_1",
+                    "stat_cost": "sensor.water_cost_1",
+                },
+                {
+                    "type": "water",
+                    "stat_energy_from": "sensor.water_consumption_2",
+                    "stat_cost": "sensor.water_cost_2",
+                },
+                {
+                    "type": "water",
+                    "stat_energy_from": "sensor.water_consumption_3",
+                    "stat_cost": "sensor.water_cost_2",
+                },
+                {
+                    "type": "water",
+                    "stat_energy_from": "sensor.water_consumption_4",
+                    "entity_energy_price": "sensor.water_price_1",
+                },
+                {
+                    "type": "water",
+                    "stat_energy_from": "sensor.water_consumption_3",
+                    "entity_energy_price": "sensor.water_price_2",
+                },
+            ]
+        }
+    )
+    await hass.async_block_till_done()
+    hass.states.async_set(
+        "sensor.water_consumption_1",
+        "10.10",
+        {
+            "device_class": "water",
+            "unit_of_measurement": "beers",
+            "state_class": "total_increasing",
+        },
+    )
+    hass.states.async_set(
+        "sensor.water_consumption_2",
+        "10.10",
+        {
+            "device_class": "water",
+            "unit_of_measurement": "ft³",
+            "state_class": "total_increasing",
+        },
+    )
+    hass.states.async_set(
+        "sensor.water_consumption_3",
+        "10.10",
+        {
+            "device_class": "water",
+            "unit_of_measurement": "m³",
+            "state_class": "total_increasing",
+        },
+    )
+    hass.states.async_set(
+        "sensor.water_consumption_4",
+        "10.10",
+        {"unit_of_measurement": "beers", "state_class": "total_increasing"},
+    )
+    hass.states.async_set(
+        "sensor.water_cost_2",
+        "10.10",
+        {"unit_of_measurement": "EUR/kWh", "state_class": "total_increasing"},
+    )
+    hass.states.async_set(
+        "sensor.water_price_1",
+        "10.10",
+        {"unit_of_measurement": "EUR/m³", "state_class": "total_increasing"},
+    )
+    hass.states.async_set(
+        "sensor.water_price_2",
+        "10.10",
+        {"unit_of_measurement": "EUR/invalid", "state_class": "total_increasing"},
+    )
+
+    assert (await validate.async_validate(hass)).as_dict() == {
+        "energy_sources": [
+            [
+                {
+                    "type": "entity_unexpected_unit_water",
+                    "identifier": "sensor.water_consumption_1",
+                    "value": "beers",
+                },
+                {
+                    "type": "recorder_untracked",
+                    "identifier": "sensor.water_cost_1",
+                    "value": None,
+                },
+                {
+                    "type": "entity_not_defined",
+                    "identifier": "sensor.water_cost_1",
+                    "value": None,
+                },
+            ],
+            [],
+            [],
+            [
+                {
+                    "type": "entity_unexpected_device_class",
+                    "identifier": "sensor.water_consumption_4",
+                    "value": None,
+                },
+            ],
+            [
+                {
+                    "type": "entity_unexpected_unit_water_price",
+                    "identifier": "sensor.water_price_2",
+                    "value": "EUR/invalid",
+                },
+            ],
+        ],
+        "device_consumption": [],
+    }
+
+
+async def test_validation_water_no_costs_tracking(
+    hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
+):
+    """Test validating water with sensors without cost tracking."""
+    await mock_energy_manager.async_update(
+        {
+            "energy_sources": [
+                {
+                    "type": "water",
+                    "stat_energy_from": "sensor.water_consumption_1",
+                    "stat_cost": None,
+                    "entity_energy_price": None,
+                    "number_energy_price": None,
+                },
+            ]
+        }
+    )
+    hass.states.async_set(
+        "sensor.water_consumption_1",
+        "10.10",
+        {
+            "device_class": "water",
+            "unit_of_measurement": "m³",
+            "state_class": "total_increasing",
+        },
+    )
+
+    assert (await validate.async_validate(hass)).as_dict() == {
+        "energy_sources": [[]],
+        "device_consumption": [],
+    }
-- 
GitLab


From 00f72f8b2a3594bdd809bccfb8a1a67ab2e4effb Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
Date: Wed, 26 Oct 2022 15:32:55 -0400
Subject: [PATCH 874/985] Bump frontend to 20221026.0 (#81042)

---
 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 0deb091b4c4..390b0ccfc18 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==20221021.0"],
+  "requirements": ["home-assistant-frontend==20221026.0"],
   "dependencies": [
     "api",
     "auth",
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index b66832b4737..f26e78bdd13 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -21,7 +21,7 @@ dbus-fast==1.47.0
 fnvhash==0.1.0
 hass-nabucasa==0.56.0
 home-assistant-bluetooth==1.6.0
-home-assistant-frontend==20221021.0
+home-assistant-frontend==20221026.0
 httpx==0.23.0
 ifaddr==0.1.7
 jinja2==3.1.2
diff --git a/requirements_all.txt b/requirements_all.txt
index a056ce0a3d0..1948c3ecedd 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==20221021.0
+home-assistant-frontend==20221026.0
 
 # homeassistant.components.home_connect
 homeconnect==0.7.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 965d184dc19..13c359f787e 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==20221021.0
+home-assistant-frontend==20221026.0
 
 # homeassistant.components.home_connect
 homeconnect==0.7.2
-- 
GitLab


From d50795af2b861e28e717f0479ad6e800b7030620 Mon Sep 17 00:00:00 2001
From: Steven Looman <steven.looman@gmail.com>
Date: Wed, 26 Oct 2022 21:34:44 +0200
Subject: [PATCH 875/985] Move upnp derived sensors to library, be more robust
 about failing getting some data (#79955)

---
 homeassistant/components/upnp/__init__.py     | 127 ++----------------
 .../components/upnp/binary_sensor.py          |  20 ++-
 homeassistant/components/upnp/config_flow.py  |   1 -
 homeassistant/components/upnp/const.py        |   5 +-
 homeassistant/components/upnp/coordinator.py  |  50 +++++++
 homeassistant/components/upnp/device.py       |  99 +++++---------
 homeassistant/components/upnp/entity.py       |  54 ++++++++
 homeassistant/components/upnp/manifest.json   |   3 +-
 homeassistant/components/upnp/sensor.py       | 113 +++++-----------
 homeassistant/generated/integrations.json     |   2 +-
 tests/components/upnp/conftest.py             |  28 ++--
 tests/components/upnp/test_binary_sensor.py   |  26 +++-
 tests/components/upnp/test_sensor.py          |  90 ++++---------
 13 files changed, 274 insertions(+), 344 deletions(-)
 create mode 100644 homeassistant/components/upnp/coordinator.py
 create mode 100644 homeassistant/components/upnp/entity.py

diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py
index 95531450e5a..0d4c39e6d3d 100644
--- a/homeassistant/components/upnp/__init__.py
+++ b/homeassistant/components/upnp/__init__.py
@@ -1,28 +1,17 @@
-"""Open ports in your router for Home Assistant and provide statistics."""
+"""UPnP/IGD integration."""
 from __future__ import annotations
 
 import asyncio
-from collections.abc import Mapping
-from dataclasses import dataclass
 from datetime import timedelta
-from typing import Any
 
-from async_upnp_client.exceptions import UpnpCommunicationError, UpnpConnectionError
+from async_upnp_client.exceptions import UpnpConnectionError
 
 from homeassistant.components import ssdp
-from homeassistant.components.binary_sensor import BinarySensorEntityDescription
-from homeassistant.components.sensor import SensorEntityDescription
 from homeassistant.config_entries import ConfigEntry
 from homeassistant.const import Platform
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.helpers import config_validation as cv, device_registry as dr
-from homeassistant.helpers.entity import DeviceInfo
-from homeassistant.helpers.update_coordinator import (
-    CoordinatorEntity,
-    DataUpdateCoordinator,
-    UpdateFailed,
-)
+from homeassistant.helpers import config_validation, device_registry
 
 from .const import (
     CONFIG_ENTRY_HOST,
@@ -36,14 +25,15 @@ from .const import (
     IDENTIFIER_SERIAL_NUMBER,
     LOGGER,
 )
-from .device import Device, async_create_device
+from .coordinator import UpnpDataUpdateCoordinator
+from .device import async_create_device
 
 NOTIFICATION_ID = "upnp_notification"
 NOTIFICATION_TITLE = "UPnP/IGD Setup"
 
 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
 
-CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
+CONFIG_SCHEMA = config_validation.removed(DOMAIN, raise_if_present=False)
 
 
 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -126,12 +116,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     if device.serial_number:
         identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number))
 
-    connections = {(dr.CONNECTION_UPNP, device.udn)}
+    connections = {(device_registry.CONNECTION_UPNP, device.udn)}
     if device_mac_address:
-        connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address))
+        connections.add((device_registry.CONNECTION_NETWORK_MAC, device_mac_address))
 
-    device_registry = dr.async_get(hass)
-    device_entry = device_registry.async_get_device(
+    dev_registry = device_registry.async_get(hass)
+    device_entry = dev_registry.async_get_device(
         identifiers=identifiers, connections=connections
     )
     if device_entry:
@@ -142,7 +132,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         )
     if not device_entry:
         # No device found, create new device entry.
-        device_entry = device_registry.async_get_or_create(
+        device_entry = dev_registry.async_get_or_create(
             config_entry_id=entry.entry_id,
             connections=connections,
             identifiers=identifiers,
@@ -155,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         )
     else:
         # Update identifier.
-        device_entry = device_registry.async_update_device(
+        device_entry = dev_registry.async_update_device(
             device_entry.id,
             new_identifiers=identifiers,
         )
@@ -191,96 +181,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
         del hass.data[DOMAIN][entry.entry_id]
 
     return unload_ok
-
-
-@dataclass
-class UpnpBinarySensorEntityDescription(BinarySensorEntityDescription):
-    """A class that describes UPnP entities."""
-
-    format: str = "s"
-    unique_id: str | None = None
-
-
-@dataclass
-class UpnpSensorEntityDescription(SensorEntityDescription):
-    """A class that describes a sensor UPnP entities."""
-
-    format: str = "s"
-    unique_id: str | None = None
-
-
-class UpnpDataUpdateCoordinator(DataUpdateCoordinator):
-    """Define an object to update data from UPNP device."""
-
-    def __init__(
-        self,
-        hass: HomeAssistant,
-        device: Device,
-        device_entry: dr.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:
-            update_values = await asyncio.gather(
-                self.device.async_get_traffic_data(),
-                self.device.async_get_status(),
-            )
-        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
-
-        return {
-            **update_values[0],
-            **update_values[1],
-        }
-
-
-class UpnpEntity(CoordinatorEntity[UpnpDataUpdateCoordinator]):
-    """Base class for UPnP/IGD entities."""
-
-    entity_description: UpnpSensorEntityDescription | UpnpBinarySensorEntityDescription
-
-    def __init__(
-        self,
-        coordinator: UpnpDataUpdateCoordinator,
-        entity_description: UpnpSensorEntityDescription
-        | UpnpBinarySensorEntityDescription,
-    ) -> 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/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py
index 7da7f187882..7419cc84ea2 100644
--- a/homeassistant/components/upnp/binary_sensor.py
+++ b/homeassistant/components/upnp/binary_sensor.py
@@ -1,19 +1,31 @@
 """Support for UPnP/IGD 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 . import UpnpBinarySensorEntityDescription, UpnpDataUpdateCoordinator, UpnpEntity
+from . import UpnpDataUpdateCoordinator
 from .const import DOMAIN, LOGGER, WAN_STATUS
+from .entity import UpnpEntity, UpnpEntityDescription
+
+
+@dataclass
+class UpnpBinarySensorEntityDescription(
+    UpnpEntityDescription, BinarySensorEntityDescription
+):
+    """A class that describes binary sensor UPnP entities."""
+
 
-BINARYSENSOR_ENTITY_DESCRIPTIONS: tuple[UpnpBinarySensorEntityDescription, ...] = (
+SENSOR_DESCRIPTIONS: tuple[UpnpBinarySensorEntityDescription, ...] = (
     UpnpBinarySensorEntityDescription(
         key=WAN_STATUS,
         name="wan status",
@@ -29,14 +41,14 @@ 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 = [
         UpnpStatusBinarySensor(
             coordinator=coordinator,
             entity_description=entity_description,
         )
-        for entity_description in BINARYSENSOR_ENTITY_DESCRIPTIONS
+        for entity_description in SENSOR_DESCRIPTIONS
         if coordinator.data.get(entity_description.key) is not None
     ]
     LOGGER.debug("Adding binary_sensor entities: %s", entities)
diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py
index 3386cf40711..6b488398461 100644
--- a/homeassistant/components/upnp/config_flow.py
+++ b/homeassistant/components/upnp/config_flow.py
@@ -78,7 +78,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
     # Paths:
     # - ssdp(discovery_info) --> 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.
-
-        Traffic data consists of:
-        - total bytes sent
-        - total bytes received
-        - total packets sent
-        - total packats received
-
-        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(),
-        )
+    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
 
-        return {
-            TIMESTAMP: utcnow(),
-            BYTES_RECEIVED: values[0],
-            BYTES_SENT: values[1],
-            PACKETS_RECEIVED: values[2],
-            PACKETS_SENT: values[3],
-        }
+        def get_value(value: Any) -> Any:
+            if value is None or isinstance(value, Exception):
+                return None
 
-    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 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"
-- 
GitLab


From d107d8df7896a80e2220531b0cdd02fa0fd687ec Mon Sep 17 00:00:00 2001
From: Klaas Neirinck <kneirinck@users.noreply.github.com>
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)
-- 
GitLab


From 2a6f2f431d628e18a5b9d21a58111bcee321547b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From cf0f79294bef3c138ecdf1d12a2c86e028399c6f Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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"
-- 
GitLab


From 11cc7e156626991353ff78efd21378626c570ecb Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
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()
-- 
GitLab


From b5615823bababfc17fa6b92ef95ce173f502e602 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From ad29bd55a45e7d04bc5f6f02f912ed0ccd7252c0 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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
-- 
GitLab


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
-- 
GitLab


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
 
 
-- 
GitLab


From 61d064ffd567b944fc2629e8c3e4d5d14304c1a7 Mon Sep 17 00:00:00 2001
From: Avi Miller <me@dje.li>
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
-- 
GitLab


From eec1015789839e4713d5901be3f7b91cfdc1cca2 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From a50fd6a259742caab5ab739ef9458cd0f351c5ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Huryn?= <michalhuryn@gmail.com>
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
-- 
GitLab


From cbd5e919cbddbb6afd5ac4582ed15fd6fcff1958 Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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()
 
-- 
GitLab


From 43164b5751e6eac914fab81431552263e96e49b9 Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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."""
-- 
GitLab


From 8751eaaf3ee94011e5c961a96063024f58a39e8a Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 4927f4206aa534479f95a8a403f14c2286cc3e97 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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()
-- 
GitLab


From 233ad2b90b38e6b169dcbd1002e5ef1eeb5b3e36 Mon Sep 17 00:00:00 2001
From: Matthias Alphart <farmio@alphart.net>
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):
-- 
GitLab


From 6d973a1793f2edf718e18bd18cb018b95630829b Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 1c8a7fe8e8d1e57f47c9b50ab482212424fd8808 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
-- 
GitLab


From aeecc93ad653cb7504bed01ff1074cb14b3b4721 Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
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 <kevin.stillhammer@gmail.com>

* Apply PR feedback

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>

Signed-off-by: Kevin Stillhammer <kevin.stillhammer@gmail.com>
---
 .../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",
+    }
-- 
GitLab


From 1ef9e9e19aa3a8577f6467c3a85c04a70e4b0cc5 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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"))
-- 
GitLab


From 3f55d037f813775155d1237b00e414596ce2085f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 9de89c97a4be8c918f6c3f7d8ae155e7dfd9e2b1 Mon Sep 17 00:00:00 2001
From: Thibaut <thibaut@etienne.pw>
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
-- 
GitLab


From 2bfd4e79d23e5d1b26f555b61e3de9216d85a382 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 4dc2d885cfb38ba40504ba38f6eb0879c0bf32fe Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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": "<enum 'DeviceState'>",
+                    "repr": "<DeviceState.ON: ('01', 'on')>",
+                },
+                "device_type": {
+                    "__type": "<enum 'DeviceType'>",
+                    "repr": "<DeviceType.V4: ('Switcher V4', '0317', "
+                    "1, <DeviceCategory.WATER_HEATER: 1>)>",
+                },
+                "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,
+        },
+    }
-- 
GitLab


From 089bbe839157d34e324fe035ecee1eb0ddc684f2 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 09fc492d80c204259c2764a50d95beb148b0d843 Mon Sep 17 00:00:00 2001
From: Robert Hillis <tkdrob4390@yahoo.com>
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
-- 
GitLab


From 230993b7c0ae01947f50adc147a46e757ec66ec5 Mon Sep 17 00:00:00 2001
From: muppet3000 <c.m.straffon@gmail.com>
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
-- 
GitLab


From f5fe3ec50e74ee4edd4e6163c0228c0bb324e667 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


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
-- 
GitLab


From 6000cc087be63441ef04f882fb76ecd61c3d3afc Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 6036443d4a9858462c0b75043d83f486df155171 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
-- 
GitLab


From 6f3b7d009d4577a480d33d52c89f2f5dd52f92a9 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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,
+        },
+    }
-- 
GitLab


From 1b7524a79e80dec127a4993b7a1842bf7993d5f6 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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 <nick@koston.org>
---
 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(
-- 
GitLab


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 <nick@koston.org>
---
 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
-- 
GitLab


From 3323bf4ae9062481bfeeda6d3ba2ce971dc58dca Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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)
-- 
GitLab


From 2dd8797f671e86d63f5d0d3ff311cf7f408bea28 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 43b1dd54d577428527f6eac06e6ce102c29183b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Klomp?= <rene@klomp.ws>
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
-- 
GitLab


From 62635c2a96d30a926010b05ef9474b223e08faa7 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From bf04f94e0535b49fed9d6981f2d2482eca65a4eb Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 16fe7df19e2c295f682860a089a7118473fe291b Mon Sep 17 00:00:00 2001
From: Menco Bolt <macmenco@users.noreply.github.com>
Date: Sat, 29 Oct 2022 20:25:46 +0200
Subject: [PATCH 916/985] Today's Consumption is  INCREASING (#81204)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
---
 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(
-- 
GitLab


From d0a0285dd9b90f75a84dccd0f4d0df62772d3a26 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 7e740b7c9d7cfa8b546fcf556c720e625fa4b30a Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 96cdb2975566cfb6da59cada48553f990d2fe62f Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
-- 
GitLab


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
-- 
GitLab


From 8d3ed60986bcff4f89aa6db77460bea5c3ef93c4 Mon Sep 17 00:00:00 2001
From: Guido Schmitz <Shutgun@users.noreply.github.com>
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(
-- 
GitLab


From be138adb2336418d49423dacdbf098edae68e4ec Mon Sep 17 00:00:00 2001
From: Kevin Stillhammer <kevin.stillhammer@gmail.com>
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"
-- 
GitLab


From 24b3d218153fe6c692384609dedc34342237a47f Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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 <paulus@home-assistant.io>
---
 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)
-- 
GitLab


From a6bb7a083201664ab3d6514265305f2199aa533e Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 11bdddc1dc983e75b453f07851c11b05e90c7576 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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
 
-- 
GitLab


From 90a36894896fbfd5db4023871d4d2e623b7149c2 Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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()
-- 
GitLab


From 9d88c953147ef24877b0aff6a2aae9a2eb649a81 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 5f81f968ee3761f3fcc28cb95c27111e225a0b36 Mon Sep 17 00:00:00 2001
From: Jc2k <john.carr@unrouted.co.uk>
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",
                 ),
-- 
GitLab


From 0af69a1014e28ed582a65a1b6faae98d7680422d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From c36260dd17e2ac4e64362d796076e35b79260401 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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:
-- 
GitLab


From 5e3fb6ee9fe038a0ad29dd8e5d5d9119de363708 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 94f92e7f8aa702efaed7679959abcdeaa6ce3dde Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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,
-- 
GitLab


From 8bafb56f0422ce5d8ec8e56278526e8e4e973c50 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From e26149d0c34653ce21c879a38428f4d5940d8bf8 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 9fac632dcd064f6f895ef9a5cc3f34b3fbb5cfaa Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From eccf61a546a4654c20544b371d2856ea1cdfa58b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 81dde5cfdf6c1fc5ef5ccc82b01abb05b9b94251 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 1f70941f6daea91af735749e07be6f3c9e519aee Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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:
-- 
GitLab


From 3cf63ec88ed0dd7d37318083293d59da3dc8dc51 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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,
         )
-- 
GitLab


From 13562d271e664668c7372c8e082e8b2132a64222 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 8f843b3046ee9d381fd7cf904af50cf7f51aca81 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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)

-- 
GitLab


From 8eef55ed60d3ac945e51a282f0d02a22759f8a52 Mon Sep 17 00:00:00 2001
From: Chris Talkington <chris@talkingtontech.com>
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
-- 
GitLab


From 4fbbb7ba6dfd7bdecd662e47977bb7abe92f25ac Mon Sep 17 00:00:00 2001
From: Tobias Sauerwein <cgtobi@users.noreply.github.com>
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           | 236 +++---
 .../homestatus_91763b24c43d3e344f424e8b.json  | 696 ++----------------
 .../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, 383 insertions(+), 880 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"
-          },
-          {
-            "id": "12:34:56:03:1b:e4",
-            "type": "NAModule2",
-            "name": "Garden",
-            "setup_date": 1543579864,
-            "bridge": "12:34:56:37:11:ca"
+            "bridge": "12:34:56:80:bb:26"
           },
           {
-            "id": "12:34:56:05:51:20",
+            "id": "12:34:56:80:c1:ea",
             "type": "NAModule3",
             "name": "Rain",
             "setup_date": 1591770206,
-            "bridge": "12:34:56:37:11:ca"
+            "bridge": "12:34:56:80:bb:26"
           },
           {
-            "id": "12:34:56:07:bb:3e",
+            "id": "12:34:56:80:44:92",
             "type": "NAModule4",
             "name": "Bedroom",
             "setup_date": 1484997703,
-            "bridge": "12:34:56:37:11:ca"
+            "bridge": "12:34:56:80:bb:26"
           },
           {
-            "id": "12:34:56:26:68:92",
-            "type": "NHC",
-            "name": "Indoor",
-            "setup_date": 1571342643
+            "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:26:cc:01",
-            "type": "BNS",
-            "name": "Child",
-            "setup_date": 1571634243
+            "id": "12:34:56:03:1b:e4",
+            "type": "NAModule2",
+            "name": "Garden",
+            "setup_date": 1543579864,
+            "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,629 +258,43 @@
           "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"
-        },
-        {
-          "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
+          "boiler_status": true,
+          "cooler_status": false
         },
         {
-          "id": "12:34:56:80:00:12:ac:f2",
-          "type": "NLP",
-          "on": true,
-          "offload": false,
-          "firmware_revision": 62,
-          "last_seen": 1644569425,
+          "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: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"
-        },
+        }
+      ],
+      "rooms": [
         {
-          "id": "12:34:56:00:00:a1:4c:da",
-          "type": "NLPC",
-          "firmware_revision": 62,
-          "last_seen": 1646511241,
-          "power": 476,
+          "id": "2746182631",
           "reachable": true,
-          "bridge": "12:34:56:80:60:40"
+          "therm_measured_temperature": 19.8,
+          "therm_setpoint_temperature": 12,
+          "therm_setpoint_mode": "away",
+          "therm_setpoint_start_time": 1559229567,
+          "therm_setpoint_end_time": 0
         },
         {
-          "id": "12:34:56:00:01:01:01:a1",
-          "brightness": 100,
-          "firmware_revision": 52,
-          "last_seen": 1604940167,
-          "on": false,
-          "power": 0,
+          "id": "2940411577",
           "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"
-        }
-      ],
-      "rooms": [
-        {
-          "id": "2746182631",
-          "reachable": true,
-          "therm_measured_temperature": 19.8,
-          "therm_setpoint_temperature": 12,
-          "therm_setpoint_mode": "schedule",
-          "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 <Hall> 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",
-- 
GitLab


From f3a96ce14b3d1cba01c06b267b47cda93c3cfc7e Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 0a476baf16014f26cd67ad9fbbdf166bb3d3d7b8 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
-- 
GitLab


From 3ddcc637da42bd5c19c41441261fdfffe4ee794e Mon Sep 17 00:00:00 2001
From: Mike Degatano <michael.degatano@gmail.com>
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,
-- 
GitLab


From 7046f5f19e3238c0b3111b9712aad4245ea9c759 Mon Sep 17 00:00:00 2001
From: TheJulianJES <TheJulianJES@users.noreply.github.com>
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()
             )
-- 
GitLab


From 9b4f2df8f34355099c8b44cc041922e7d57b6016 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From d7e76fdf3a5e89617d79508bd3418459197c4c6b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 19a5c87da6869244e4b66a7efe544976dee45955 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 356953c8bc898770b22fcaa0522e3a83eeece68a Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From 882ad31a99f2f0e306e7b902c28837d72028d7b6 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 599c23c1d72ad460546ab6707e045b20d0ffd720 Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 941512641b1f538f6ed9333a3184fe9839f6205d Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 0ac0e9c0d5ecac6a8a64cd10e34daff13ae1db53 Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
-- 
GitLab


From dfe399e370b432ba5936e629d3d40f1227849a21 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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",
-- 
GitLab


From 8965a1322cd74fc3d77d38eefc134f81db1a95d9 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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)
-- 
GitLab


From 9b87f7f6f9409e1117c255c011f7480daf17d9cd Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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,
-- 
GitLab


From 4684101a853baeb8bb7624136748fe094c1cb980 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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 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)
+
+            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 isinstance(installed_version, str) and installed_version != "":
-                self._attr_installed_version = installed_version
+            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_installed_version_received)
+        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"
-- 
GitLab


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."""
 
-- 
GitLab


From 1cc85f77e30e7e87e17fb2e78229c71146ea6820 Mon Sep 17 00:00:00 2001
From: Ron Klinkien <ron@cyberjunky.nl>
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]
-- 
GitLab


From f9493bc313d987302bb27664bdccab49aeb798bc Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
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
-- 
GitLab


From 473490aee773c0ba1c3089e614bf6747bd945a08 Mon Sep 17 00:00:00 2001
From: Shay Levy <levyshay1@gmail.com>
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
-- 
GitLab


From c4bb225060085fb0e2732647b13ab4bffb9e523e Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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
-- 
GitLab


From a2d432dfd65e7404de4e868c1b75f0c608cbbfa2 Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
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
-- 
GitLab


From f265c160d17ff435796f35c769570beac8b13969 Mon Sep 17 00:00:00 2001
From: Maciej Bieniek <bieniu@users.noreply.github.com>
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
-- 
GitLab


From d0ddbb5f5807b798434d5441a4dc693583c4424e Mon Sep 17 00:00:00 2001
From: "David F. Mulcahey" <david.mulcahey@me.com>
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()
             ),
-- 
GitLab


From 9dff7ab6b96b8c5f78441e7e6865ba57d421ceff Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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]
-- 
GitLab


From 8c63a9ce5e29de71e3dd3db5816c8a48e8bc4064 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
+    )
-- 
GitLab


From 1efec8323abf6ef1e3896c65ebac76cbd24a3613 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From e8f93d9c7f943482bb10692fa255093c36747faa Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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"
-- 
GitLab


From b9132e78b48bc9e0356c3784ae6d357e25cd0db3 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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",
+            "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)",
         ),
-        (ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"),
         (
-            exceptions.HomeAssistantError(),
+            exceptions.HomeAssistantError,
             websocket_api.ERR_UNKNOWN_ERROR,
             "Unknown error",
+            "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)",
         ),
-    ):
-        send_messages.clear()
+    ],
+)
+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
-- 
GitLab


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
-- 
GitLab


From 0dbf0504ffc3b502ab151b32a8e788a930cf2a81 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From a5f209b219bea2b9c20cede33b7515fd3cc7733e Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 3aca3763741443aca8ff4b6a534b4b5b17d7e47d Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <balloob@gmail.com>
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 <git@frenck.dev>
---
 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
-
-            if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
-                f"/{UnitOfEnergy.MEGA_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.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)
-
-        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
+        energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
 
-        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])
-- 
GitLab


From 9f54e332ec68a348a00cb82c0759b1cd3b64aa25 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From f6c094b017cdb4a8e8f0c164adfad63ee7df5552 Mon Sep 17 00:00:00 2001
From: Mike Degatano <michael.degatano@gmail.com>
Date: Tue, 1 Nov 2022 21:29:11 -0400
Subject: [PATCH 978/985] Improve supervisor repairs (#81387)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
---
 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"]
-- 
GitLab


From a6e745b6879baa2765d6750b72471bb3fe1f7e68 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston" <nick@koston.org>
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
-- 
GitLab


From 06d22d8249fbbfa91ef354d69637d5c6bebfde36 Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From 3409dea28c4e5ba0726a6503301cbd9b99acba6d Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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"
-- 
GitLab


From 970fd9bdba5433ade6a92b8027663c8b27a5a5d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= <github@dahoiv.net>
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
-- 
GitLab


From 28832e1c2e2b6473fd8dccebbb745256f860b923 Mon Sep 17 00:00:00 2001
From: Allen Porter <allen@thebends.org>
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
-- 
GitLab


From 1ea0d0e47f2a3b749f6158eb4b9f732e0b5c372c Mon Sep 17 00:00:00 2001
From: Bram Kragten <mail@bramkragten.nl>
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
-- 
GitLab


From f14a84211f417d3e2d02b7e07e250f9acb716a86 Mon Sep 17 00:00:00 2001
From: Franck Nijhof <git@frenck.dev>
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"
-- 
GitLab