From 7115e6304431292e0291d485b1a71710e7c2f3cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Aug 2022 14:11:29 -0400 Subject: [PATCH 001/231] Bumped version to 2022.9.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 750b014e0da..6c530231139 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -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 d68cac82923..9d5b168c559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0.dev0" +version = "2022.9.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8b8db998df0e341b8bc4ff6261fe8fd2fe1f45b8 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 31 Aug 2022 21:16:00 +0200 Subject: [PATCH 002/231] Catch unknown user exception in Overkiz integration (#76693) --- homeassistant/components/overkiz/config_flow.py | 3 +++ homeassistant/components/overkiz/strings.json | 3 ++- homeassistant/components/overkiz/translations/en.json | 3 ++- tests/components/overkiz/test_config_flow.py | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 2808c309938..d3ab9722fca 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -12,6 +12,7 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyAttemptsBannedException, TooManyRequestsException, + UnknownUserException, ) from pyoverkiz.models import obfuscate_id import voluptuous as vol @@ -83,6 +84,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "server_in_maintenance" except TooManyAttemptsBannedException: errors["base"] = "too_many_attempts" + except UnknownUserException: + errors["base"] = "unknown_user" except Exception as exception: # pylint: disable=broad-except errors["base"] = "unknown" LOGGER.exception(exception) diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 9c64311a73e..ecc0329eb2a 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -18,7 +18,8 @@ "server_in_maintenance": "Server is down for maintenance", "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": "[%key:common::config_flow::error::unknown%]", + "unknown_user": "Unknown user. Somfy Protect accounts are 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 9e24a9d3cb3..9c8ad538695 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server is down for maintenance", "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": "Unexpected error", + "unknown_user": "Unknown user. Somfy Protect accounts are 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 0542f4dc9fc..940da7b39c2 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -9,6 +9,7 @@ from pyoverkiz.exceptions import ( MaintenanceException, TooManyAttemptsBannedException, TooManyRequestsException, + UnknownUserException, ) import pytest @@ -88,6 +89,7 @@ async def test_form(hass: HomeAssistant) -> None: (ClientError, "cannot_connect"), (MaintenanceException, "server_in_maintenance"), (TooManyAttemptsBannedException, "too_many_attempts"), + (UnknownUserException, "unknown_user"), (Exception, "unknown"), ], ) From 37acd3e3f234ec30f53bb311bef26eb3d5538d2c Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 1 Sep 2022 05:42:23 +0300 Subject: [PATCH 003/231] Suppress 404 in Bravia TV (#77288) --- homeassistant/components/braviatv/coordinator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 2744911007d..bdacddcdb2f 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -7,7 +7,7 @@ from functools import wraps import logging from typing import Any, Final, TypeVar -from pybravia import BraviaTV, BraviaTVError +from pybravia import BraviaTV, BraviaTVError, BraviaTVNotFound from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player.const import ( @@ -79,6 +79,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.connected = False # Assume that the TV is in Play mode self.playing = True + self.skipped_updates = 0 super().__init__( hass, @@ -113,6 +114,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): power_status = await self.client.get_power_status() self.is_on = power_status == "active" + self.skipped_updates = 0 if self.is_on is False: return @@ -121,6 +123,13 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): await self.async_update_sources() await self.async_update_volume() await self.async_update_playing() + except BraviaTVNotFound as err: + if self.skipped_updates < 10: + self.connected = False + self.skipped_updates += 1 + _LOGGER.debug("Update skipped, Bravia API service is reloading") + return + raise UpdateFailed("Error communicating with device") from err except BraviaTVError as err: self.is_on = False self.connected = False From 7d90f6ccea04fc5326d43f74abf29496df0e2268 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 31 Aug 2022 16:35:58 -0400 Subject: [PATCH 004/231] Bump version of pyunifiprotect to 4.2.0 (#77618) --- 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 a01706337e0..5958da8f00e 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.1.9", "unifi-discovery==1.1.6"], + "requirements": ["pyunifiprotect==4.2.0", "unifi-discovery==1.1.6"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index ffbff8840a0..fcb36668f7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2037,7 +2037,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.9 +pyunifiprotect==4.2.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc79492c7a2..26882369a6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1400,7 +1400,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.1.9 +pyunifiprotect==4.2.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From de35e84543d1c2446a3dd4fad3939db2eedbbd37 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 1 Sep 2022 01:18:34 +0200 Subject: [PATCH 005/231] Update xknx to 1.0.2 (#77627) --- 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 bb5599939db..c0aa6c3941c 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.0.1"], + "requirements": ["xknx==1.0.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index fcb36668f7f..24ec7ecc560 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ xboxapi==2.0.1 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.1 +xknx==1.0.2 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26882369a6f..002df1e17a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1729,7 +1729,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==1.0.1 +xknx==1.0.2 # homeassistant.components.bluesound # homeassistant.components.fritz From 129f7176342eee5ea25872423536181caae65d39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 02:43:18 +0000 Subject: [PATCH 006/231] Bump bleak to 0.16.0 (#77629) Co-authored-by: Justin Vanderhooft --- 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 5cd83a2d51a..981b7854e36 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["usb"], "quality_scale": "internal", "requirements": [ - "bleak==0.15.1", + "bleak==0.16.0", "bluetooth-adapters==0.3.2", "bluetooth-auto-recovery==0.3.0" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f40589bf64..a456e9ec965 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.8.0 bcrypt==3.1.7 -bleak==0.15.1 +bleak==0.16.0 bluetooth-adapters==0.3.2 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index 24ec7ecc560..1e77606ec48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.15.1 +bleak==0.16.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 002df1e17a2..a013c67cfc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak==0.15.1 +bleak==0.16.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 From a080256f88847f609fbd60693becf9c51d41c30b Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Wed, 31 Aug 2022 20:23:45 -0400 Subject: [PATCH 007/231] Bump melnor-bluetooth to 0.0.15 (#77631) --- homeassistant/components/melnor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index 37ac40cb3aa..a59758f705b 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/melnor", "iot_class": "local_polling", "name": "Melnor Bluetooth", - "requirements": ["melnor-bluetooth==0.0.13"] + "requirements": ["melnor-bluetooth==0.0.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e77606ec48..50306cf623b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1037,7 +1037,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.13 +melnor-bluetooth==0.0.15 # homeassistant.components.message_bird messagebird==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a013c67cfc2..a0356065e94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ mcstatus==6.0.0 meater-python==0.0.8 # homeassistant.components.melnor -melnor-bluetooth==0.0.13 +melnor-bluetooth==0.0.15 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From 5f4411113a5b9196ea6dbb71c60fb95cb82357a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 31 Aug 2022 22:57:12 -0400 Subject: [PATCH 008/231] Bumped version to 2022.9.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 6c530231139..8d5242123e5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -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 9d5b168c559..a2e1c4063a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b0" +version = "2022.9.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From aa57594d21dbc15b0e714c27f9d9b3d9a57cab2f Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 1 Sep 2022 03:40:13 +0200 Subject: [PATCH 009/231] Required config_flow values for here_travel_time (#75026) --- .../here_travel_time/config_flow.py | 46 ++++++++++++++----- .../components/here_travel_time/const.py | 2 + 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index e8a05796b66..b4756c82922 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -11,6 +11,8 @@ from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, CONF_ENTITY_NAMESPACE, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, @@ -30,6 +32,8 @@ from .const import ( CONF_ARRIVAL_TIME, CONF_DEPARTURE, CONF_DEPARTURE_TIME, + CONF_DESTINATION, + CONF_ORIGIN, CONF_ROUTE_MODE, CONF_TRAFFIC_MODE, DEFAULT_NAME, @@ -187,13 +191,25 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Configure origin by using gps coordinates.""" if user_input is not None: - self._config[CONF_ORIGIN_LATITUDE] = user_input["origin"]["latitude"] - self._config[CONF_ORIGIN_LONGITUDE] = user_input["origin"]["longitude"] + self._config[CONF_ORIGIN_LATITUDE] = user_input[CONF_ORIGIN][CONF_LATITUDE] + self._config[CONF_ORIGIN_LONGITUDE] = user_input[CONF_ORIGIN][ + CONF_LONGITUDE + ] return self.async_show_menu( step_id="destination_menu", menu_options=["destination_coordinates", "destination_entity"], ) - schema = vol.Schema({"origin": selector({LocationSelector.selector_type: {}})}) + schema = vol.Schema( + { + vol.Required( + CONF_ORIGIN, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } + ) return self.async_show_form(step_id="origin_coordinates", data_schema=schema) async def async_step_origin_entity( @@ -206,9 +222,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="destination_menu", menu_options=["destination_coordinates", "destination_entity"], ) - schema = vol.Schema( - {CONF_ORIGIN_ENTITY_ID: selector({EntitySelector.selector_type: {}})} - ) + schema = vol.Schema({vol.Required(CONF_ORIGIN_ENTITY_ID): EntitySelector()}) return self.async_show_form(step_id="origin_entity", data_schema=schema) async def async_step_destination_coordinates( @@ -217,11 +231,11 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Configure destination by using gps coordinates.""" if user_input is not None: - self._config[CONF_DESTINATION_LATITUDE] = user_input["destination"][ - "latitude" + self._config[CONF_DESTINATION_LATITUDE] = user_input[CONF_DESTINATION][ + CONF_LATITUDE ] - self._config[CONF_DESTINATION_LONGITUDE] = user_input["destination"][ - "longitude" + self._config[CONF_DESTINATION_LONGITUDE] = user_input[CONF_DESTINATION][ + CONF_LONGITUDE ] return self.async_create_entry( title=self._config[CONF_NAME], @@ -229,7 +243,15 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=default_options(self.hass), ) schema = vol.Schema( - {"destination": selector({LocationSelector.selector_type: {}})} + { + vol.Required( + CONF_DESTINATION, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } ) return self.async_show_form( step_id="destination_coordinates", data_schema=schema @@ -250,7 +272,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=default_options(self.hass), ) schema = vol.Schema( - {CONF_DESTINATION_ENTITY_ID: selector({EntitySelector.selector_type: {}})} + {vol.Required(CONF_DESTINATION_ENTITY_ID): EntitySelector()} ) return self.async_show_form(step_id="destination_entity", data_schema=schema) diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index b3768b2d69d..4e9b8beaf12 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -9,9 +9,11 @@ DOMAIN = "here_travel_time" DEFAULT_SCAN_INTERVAL = 300 +CONF_DESTINATION = "destination" CONF_DESTINATION_LATITUDE = "destination_latitude" CONF_DESTINATION_LONGITUDE = "destination_longitude" CONF_DESTINATION_ENTITY_ID = "destination_entity_id" +CONF_ORIGIN = "origin" CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" From b3830d0f17f3e2dc92007b0837190bb00e95c55f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 1 Sep 2022 16:46:43 +0800 Subject: [PATCH 010/231] Fix basic browse_media support in forked-daapd (#77595) --- .../components/forked_daapd/const.py | 11 ++++++++ .../components/forked_daapd/media_player.py | 25 ++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index d711ae1b35b..f0d915ce3e5 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,6 +1,17 @@ """Const for forked-daapd.""" from homeassistant.components.media_player import MediaPlayerEntityFeature +CAN_PLAY_TYPE = { + "audio/mp4", + "audio/aac", + "audio/mpeg", + "audio/flac", + "audio/ogg", + "audio/x-ms-wma", + "audio/aiff", + "audio/wav", +} + CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 6c1a772fb4d..953461c1019 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -1,4 +1,6 @@ """This library brings support for forked_daapd to Home Assistant.""" +from __future__ import annotations + import asyncio from collections import defaultdict import logging @@ -8,7 +10,7 @@ from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI from homeassistant.components import media_source -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) @@ -35,6 +37,7 @@ from homeassistant.util.dt import utcnow from .const import ( CALLBACK_TIMEOUT, + CAN_PLAY_TYPE, CONF_LIBRESPOT_JAVA_PORT, CONF_MAX_PLAYLISTS, CONF_TTS_PAUSE_TIME, @@ -769,6 +772,18 @@ class ForkedDaapdMaster(MediaPlayerEntity): )() _LOGGER.warning("No pipe control available for %s", pipe_name) + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, + ) + class ForkedDaapdUpdater: """Manage updates for the forked-daapd device.""" @@ -885,11 +900,3 @@ class ForkedDaapdUpdater: self._api, outputs_to_add, ) - - async def async_browse_media(self, media_content_type=None, media_content_id=None): - """Implement the websocket media browsing helper.""" - return await media_source.async_browse_media( - self.hass, - media_content_id, - content_filter=lambda item: item.media_content_type.startswith("audio/"), - ) From d6b2f0ff7637bfc9c1e423be56670395e02fc31a Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 1 Sep 2022 12:02:46 -0600 Subject: [PATCH 011/231] Code quality improvements for litterrobot integration (#77605) --- .../components/litterrobot/button.py | 52 +++++++++---------- .../components/litterrobot/entity.py | 50 ++++++++++++------ .../components/litterrobot/select.py | 20 ++++--- .../components/litterrobot/sensor.py | 32 +++++------- .../components/litterrobot/switch.py | 25 ++++----- .../components/litterrobot/vacuum.py | 19 +++---- tests/components/litterrobot/test_vacuum.py | 19 +++++++ 7 files changed, 123 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 74c659fd474..81d9c65927e 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -1,21 +1,25 @@ """Support for Litter-Robot button.""" from __future__ import annotations -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import Callable, Coroutine from dataclasses import dataclass import itertools from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot3 -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + DOMAIN as PLATFORM, + ButtonEntity, + ButtonEntityDescription, +) 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 .entity import LitterRobotEntity, _RobotT +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -26,21 +30,24 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities: Iterable[LitterRobotButtonEntity] = itertools.chain( - ( - LitterRobotButtonEntity( - robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON - ) - for robot in hub.litter_robots() - if isinstance(robot, LitterRobot3) - ), - ( - LitterRobotButtonEntity( - robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON - ) - for robot in hub.feeder_robots() - ), + entities: list[LitterRobotButtonEntity] = list( + itertools.chain( + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON + ) + for robot in hub.litter_robots() + if isinstance(robot, LitterRobot3) + ), + ( + LitterRobotButtonEntity( + robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON + ) + for robot in hub.feeder_robots() + ), + ) ) + async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) @@ -76,17 +83,6 @@ class LitterRobotButtonEntity(LitterRobotEntity[_RobotT], ButtonEntity): entity_description: RobotButtonEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotButtonEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot button entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - async def async_press(self) -> None: """Press the button.""" await self.entity_description.press_fn(self.robot) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 8471e007ce9..9716793f70e 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,7 +1,7 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Iterable from datetime import time import logging from typing import Any, Generic, TypeVar @@ -10,8 +10,9 @@ from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException from typing_extensions import ParamSpec -from homeassistant.core import CALLBACK_TYPE, callback -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -36,18 +37,18 @@ class LitterRobotEntity( _attr_has_entity_name = True - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot - self.entity_type = entity_type self.hub = hub - self._attr_name = entity_type.capitalize() - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self.robot.serial}-{self.entity_type}" + self.entity_description = description + self._attr_unique_id = f"{self.robot.serial}-{description.key}" + # The following can be removed in 2022.12 after adjusting names in entities appropriately + if description.name is not None: + self._attr_name = description.name.capitalize() @property def device_info(self) -> DeviceInfo: @@ -65,9 +66,11 @@ class LitterRobotEntity( class LitterRobotControlEntity(LitterRobotEntity[_RobotT]): """A Litter-Robot entity that can control the unit.""" - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, entity_type=entity_type, hub=hub) + super().__init__(robot=robot, hub=hub, description=description) self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( @@ -134,9 +137,11 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): _attr_entity_category = EntityCategory.CONFIG - def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription + ) -> None: """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, entity_type=entity_type, hub=hub) + super().__init__(robot=robot, hub=hub, description=description) self._assumed_state: bool | None = None async def perform_action_and_assume_state( @@ -146,3 +151,18 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): if await self.perform_action_and_refresh(action, assumed_state): self._assumed_state = assumed_state self.async_write_ha_state() + + +def async_update_unique_id( + hass: HomeAssistant, domain: str, entities: Iterable[LitterRobotEntity[_RobotT]] +) -> None: + """Update unique ID to be based on entity description key instead of name. + + Introduced with release 2022.9. + """ + ent_reg = er.async_get(hass) + for entity in entities: + old_unique_id = f"{entity.robot.serial}-{entity.entity_description.name}" + if entity_id := ent_reg.async_get_entity_id(domain, DOMAIN, old_unique_id): + new_unique_id = f"{entity.robot.serial}-{entity.entity_description.key}" + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index a18cd3b46b5..9ec784db8f2 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -8,13 +8,18 @@ from typing import Any, Generic, TypeVar from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.components.select import ( + DOMAIN as PLATFORM, + SelectEntity, + SelectEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT +from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub _CastTypeT = TypeVar("_CastTypeT", int, float) @@ -40,9 +45,10 @@ class RobotSelectEntityDescription( LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( - key="clean_cycle_wait_time_minutes", + key="cycle_delay", name="Clean Cycle Wait Time Minutes", icon="mdi:timer-outline", + unit_of_measurement=TIME_MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, options_fn=lambda robot: robot.VALID_WAIT_TIMES, select_fn=lambda robot, option: (robot.set_wait_time, int(option)), @@ -65,7 +71,7 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot selects using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + entities: list[LitterRobotSelect] = list( itertools.chain( ( LitterRobotSelect(robot=robot, hub=hub, description=LITTER_ROBOT_SELECT) @@ -77,6 +83,8 @@ async def async_setup_entry( ), ) ) + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) class LitterRobotSelect( @@ -93,9 +101,7 @@ class LitterRobotSelect( description: RobotSelectEntityDescription[_RobotT, _CastTypeT], ) -> None: """Initialize a Litter-Robot select entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description + super().__init__(robot, hub, description) options = self.entity_description.options_fn(self.robot) self._attr_options = list(map(str, options)) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 90bdfcbda73..c904335d23f 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -9,6 +9,7 @@ from typing import Any, Generic, Union, cast from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -20,7 +21,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -48,17 +49,6 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): entity_description: RobotSensorEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotSensorEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot sensor entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - @property def native_value(self) -> float | datetime | str | None: """Return the state.""" @@ -79,32 +69,32 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { LitterRobot: [ RobotSensorEntityDescription[LitterRobot]( - name="Waste Drawer", key="waste_drawer_level", + name="Waste Drawer", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), ), RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode Start Time", key="sleep_mode_start_time", + name="Sleep Mode Start Time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( - name="Sleep Mode End Time", key="sleep_mode_end_time", + name="Sleep Mode End Time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( - name="Last Seen", key="last_seen", + name="Last Seen", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, ), RobotSensorEntityDescription[LitterRobot]( - name="Status Code", key="status_code", + name="Status Code", device_class="litterrobot__status_code", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -119,8 +109,8 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { ], FeederRobot: [ RobotSensorEntityDescription[FeederRobot]( - name="Food level", key="food_level", + name="Food level", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), ) @@ -135,10 +125,12 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ LitterRobotSensorEntity(robot=robot, hub=hub, description=description) for robot in hub.account.robots for robot_type, entity_descriptions in ROBOT_SENSOR_MAP.items() if isinstance(robot, robot_type) for description in entity_descriptions - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 2f54ede38b8..779ee699b41 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -7,13 +7,17 @@ from typing import Any, Generic, Union from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.components.switch import ( + DOMAIN as PLATFORM, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT +from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -51,17 +55,6 @@ class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): entity_description: RobotSwitchEntityDescription[_RobotT] - def __init__( - self, - robot: _RobotT, - hub: LitterRobotHub, - description: RobotSwitchEntityDescription[_RobotT], - ) -> None: - """Initialize a Litter-Robot switch entity.""" - assert description.name - super().__init__(robot, description.name, hub) - self.entity_description = description - @property def is_on(self) -> bool | None: """Return true if switch is on.""" @@ -93,9 +86,11 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ RobotSwitchEntity(robot=robot, hub=hub, description=description) for description in ROBOT_SWITCHES for robot in hub.account.robots if isinstance(robot, (LitterRobot, FeederRobot)) - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 9a4b825045f..27cd3e6758a 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,7 +1,6 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations -import logging from typing import Any from pylitterbot import LitterRobot @@ -9,11 +8,13 @@ from pylitterbot.enums import LitterBoxStatus import voluptuous as vol from homeassistant.components.vacuum import ( + DOMAIN as PLATFORM, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_PAUSED, StateVacuumEntity, + StateVacuumEntityDescription, VacuumEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -23,13 +24,9 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotControlEntity +from .entity import LitterRobotControlEntity, async_update_unique_id from .hub import LitterRobotHub -_LOGGER = logging.getLogger(__name__) - -TYPE_LITTER_BOX = "Litter Box" - SERVICE_SET_SLEEP_MODE = "set_sleep_mode" LITTER_BOX_STATUS_STATE_MAP = { @@ -44,6 +41,8 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.OFF: STATE_OFF, } +LITTER_BOX_ENTITY = StateVacuumEntityDescription("litter_box", name="Litter Box") + async def async_setup_entry( hass: HomeAssistant, @@ -53,10 +52,12 @@ async def async_setup_entry( """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) + entities = [ + LitterRobotCleaner(robot=robot, hub=hub, description=LITTER_BOX_ENTITY) for robot in hub.litter_robots() - ) + ] + async_update_unique_id(hass, PLATFORM, entities) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 02667bb8310..eb9a4c8c60b 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -21,6 +21,7 @@ from homeassistant.components.vacuum import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er from homeassistant.util.dt import utcnow from .common import VACUUM_ENTITY_ID @@ -28,6 +29,9 @@ from .conftest import setup_integration from tests.common import async_fire_time_changed +VACUUM_UNIQUE_ID_OLD = "LR3C012345-Litter Box" +VACUUM_UNIQUE_ID_NEW = "LR3C012345-litter_box" + COMPONENT_SERVICE_DOMAIN = { SERVICE_SET_SLEEP_MODE: DOMAIN, } @@ -35,6 +39,18 @@ COMPONENT_SERVICE_DOMAIN = { async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" + ent_reg = er.async_get(hass) + + # Create entity entry to migrate to new unique ID + ent_reg.async_get_or_create( + PLATFORM_DOMAIN, + DOMAIN, + VACUUM_UNIQUE_ID_OLD, + suggested_object_id=VACUUM_ENTITY_ID.replace(PLATFORM_DOMAIN, ""), + ) + ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) @@ -43,6 +59,9 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert vacuum.state == STATE_DOCKED assert vacuum.attributes["is_sleeping"] is False + ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_NEW + async def test_vacuum_status_when_sleeping( hass: HomeAssistant, mock_account_with_sleeping_robot: MagicMock From 8c697b188106309e949b215e82003b44d54f4dd2 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 1 Sep 2022 21:02:09 +0300 Subject: [PATCH 012/231] Increase sleep in Risco setup (#77619) --- homeassistant/components/risco/__init__.py | 4 ++++ homeassistant/components/risco/config_flow.py | 5 ----- homeassistant/components/risco/const.py | 2 -- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/risco/test_alarm_control_panel.py | 3 +++ tests/components/risco/test_config_flow.py | 6 ------ 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index e95b3016139..179ddd5cad6 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -154,6 +154,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: + if is_local(entry): + local_data: LocalData = hass.data[DOMAIN][entry.entry_id] + await local_data.system.disconnect() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 1befe626347..5e1cdb75b5a 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Risco integration.""" from __future__ import annotations -import asyncio from collections.abc import Mapping import logging @@ -32,7 +31,6 @@ from .const import ( DEFAULT_OPTIONS, DOMAIN, RISCO_STATES, - SLEEP_INTERVAL, TYPE_LOCAL, ) @@ -150,9 +148,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info["title"]) self._abort_if_unique_id_configured() - # Risco can hang if we don't wait before creating a new connection - await asyncio.sleep(SLEEP_INTERVAL) - return self.async_create_entry( title=info["title"], data={**user_input, **{CONF_TYPE: TYPE_LOCAL}} ) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index f4ac170d3c7..9f0e71701c6 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -46,5 +46,3 @@ DEFAULT_OPTIONS = { CONF_RISCO_STATES_TO_HA: DEFAULT_RISCO_STATES_TO_HA, CONF_HA_STATES_TO_RISCO: DEFAULT_HA_STATES_TO_RISCO, } - -SLEEP_INTERVAL = 1 diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 38035e22c62..9703b5775bc 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.3"], + "requirements": ["pyrisco==0.5.4"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 50306cf623b..502851c9b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1814,7 +1814,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.3 +pyrisco==0.5.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0356065e94..5efad4639d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1270,7 +1270,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.3 +pyrisco==0.5.4 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 0014e712ab1..1625e78ece6 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -479,6 +479,9 @@ async def test_local_setup(hass, two_part_local_alarm, setup_risco_local): device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1_local")}) assert device is not None assert device.manufacturer == "Risco" + with patch("homeassistant.components.risco.RiscoLocal.disconnect") as mock_close: + await hass.config_entries.async_unload(setup_risco_local.entry_id) + mock_close.assert_awaited_once() async def _check_local_state( diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index a39a724d7b9..396aad8015d 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -72,9 +72,6 @@ async def test_cloud_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close, patch( - "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", - 0, - ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -168,9 +165,6 @@ async def test_local_form(hass): ), patch( "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" ) as mock_close, patch( - "homeassistant.components.risco.config_flow.SLEEP_INTERVAL", - 0, - ), patch( "homeassistant.components.risco.async_setup_entry", return_value=True, ) as mock_setup_entry: From 68a01562ecfa335ed872ff5d45bc803bb0d04eb1 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:52:06 +0200 Subject: [PATCH 013/231] Add and remove Snapcast client/group callbacks properly (#77624) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/snapcast/media_player.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 703cb41a38f..0e6524c8504 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -125,10 +125,17 @@ class SnapcastGroupDevice(MediaPlayerEntity): def __init__(self, group, uid_part): """Initialize the Snapcast group device.""" - group.set_callback(self.schedule_update_ha_state) self._group = group self._uid = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}" + async def async_added_to_hass(self) -> None: + """Subscribe to group events.""" + self._group.set_callback(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect group object when removed.""" + self._group.set_callback(None) + @property def state(self): """Return the state of the player.""" @@ -213,10 +220,17 @@ class SnapcastClientDevice(MediaPlayerEntity): def __init__(self, client, uid_part): """Initialize the Snapcast client device.""" - client.set_callback(self.schedule_update_ha_state) self._client = client self._uid = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}" + async def async_added_to_hass(self) -> None: + """Subscribe to client events.""" + self._client.set_callback(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect client object when removed.""" + self._client.set_callback(None) + @property def unique_id(self): """ From 073ca240f136970611db41e6e4c7013d7c008825 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 1 Sep 2022 10:19:21 +0200 Subject: [PATCH 014/231] Required option_flow values for here_travel_time (#77651) --- .../here_travel_time/config_flow.py | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index b4756c82922..d42e6d6bf3e 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -24,7 +24,6 @@ from homeassistant.helpers.selector import ( EntitySelector, LocationSelector, TimeSelector, - selector, ) from .const import ( @@ -361,28 +360,30 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): menu_options=["departure_time", "no_time"], ) - options = { - vol.Optional( - CONF_TRAFFIC_MODE, - default=self.config_entry.options.get( - CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED - ), - ): vol.In(TRAFFIC_MODES), - vol.Optional( - CONF_ROUTE_MODE, - default=self.config_entry.options.get( - CONF_ROUTE_MODE, ROUTE_MODE_FASTEST - ), - ): vol.In(ROUTE_MODES), - vol.Optional( - CONF_UNIT_SYSTEM, - default=self.config_entry.options.get( - CONF_UNIT_SYSTEM, self.hass.config.units.name - ), - ): vol.In(UNITS), - } + schema = vol.Schema( + { + vol.Optional( + CONF_TRAFFIC_MODE, + default=self.config_entry.options.get( + CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED + ), + ): vol.In(TRAFFIC_MODES), + vol.Optional( + CONF_ROUTE_MODE, + default=self.config_entry.options.get( + CONF_ROUTE_MODE, ROUTE_MODE_FASTEST + ), + ): vol.In(ROUTE_MODES), + vol.Optional( + CONF_UNIT_SYSTEM, + default=self.config_entry.options.get( + CONF_UNIT_SYSTEM, self.hass.config.units.name + ), + ): vol.In(UNITS), + } + ) - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + return self.async_show_form(step_id="init", data_schema=schema) async def async_step_no_time( self, user_input: dict[str, Any] | None = None @@ -398,12 +399,12 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME] return self.async_create_entry(title="", data=self._config) - options = {"arrival_time": selector({TimeSelector.selector_type: {}})} - - return self.async_show_form( - step_id="arrival_time", data_schema=vol.Schema(options) + schema = vol.Schema( + {vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()} ) + return self.async_show_form(step_id="arrival_time", data_schema=schema) + async def async_step_departure_time( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -412,8 +413,8 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME] return self.async_create_entry(title="", data=self._config) - options = {"departure_time": selector({TimeSelector.selector_type: {}})} - - return self.async_show_form( - step_id="departure_time", data_schema=vol.Schema(options) + schema = vol.Schema( + {vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()} ) + + return self.async_show_form(step_id="departure_time", data_schema=schema) From 37e425db30bd807100cd7d4cb267dcca0477bb9d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Sep 2022 17:45:19 +0200 Subject: [PATCH 015/231] Clean up user overridden device class in entity registry (#77662) --- homeassistant/helpers/entity_registry.py | 14 ++++- tests/helpers/test_entity_registry.py | 72 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 23e9cc5f752..d495d196440 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -30,6 +30,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import ( Event, @@ -62,7 +63,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 7 +STORAGE_VERSION_MINOR = 8 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -970,10 +971,19 @@ async def _async_migrate( entity["hidden_by"] = None if old_major_version == 1 and old_minor_version < 7: - # Version 1.6 adds has_entity_name + # Version 1.7 adds has_entity_name for entity in data["entities"]: entity["has_entity_name"] = False + if old_major_version == 1 and old_minor_version < 8: + # Cleanup after frontend bug which incorrectly updated device_class + # Fixed by frontend PR #13551 + for entity in data["entities"]: + domain = split_entity_id(entity["entity_id"])[0] + if domain in [Platform.BINARY_SENSOR, Platform.COVER]: + continue + entity["device_class"] = None + if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 9c2592eace0..e4c371a0198 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -528,6 +528,78 @@ async def test_migration_1_1(hass, hass_storage): assert entry.original_device_class == "best_class" +@pytest.mark.parametrize("load_registries", [False]) +async def test_migration_1_7(hass, hass_storage): + """Test migration from version 1.7. + + This tests cleanup after frontend bug which incorrectly updated device_class + """ + entity_dict = { + "area_id": None, + "capabilities": {}, + "config_entry_id": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": "12345", + "name": None, + "options": None, + "original_icon": None, + "original_name": None, + "platform": "super_platform", + "supported_features": 0, + "unique_id": "very_unique", + "unit_of_measurement": None, + } + + hass_storage[er.STORAGE_KEY] = { + "version": 1, + "minor_version": 7, + "data": { + "entities": [ + { + **entity_dict, + "device_class": "original_class_by_integration", + "entity_id": "test.entity", + "original_device_class": "new_class_by_integration", + }, + { + **entity_dict, + "device_class": "class_by_user", + "entity_id": "binary_sensor.entity", + "original_device_class": "class_by_integration", + }, + { + **entity_dict, + "device_class": "class_by_user", + "entity_id": "cover.entity", + "original_device_class": "class_by_integration", + }, + ] + }, + } + + await er.async_load(hass) + registry = er.async_get(hass) + + entry = registry.async_get_or_create("test", "super_platform", "very_unique") + assert entry.device_class is None + assert entry.original_device_class == "new_class_by_integration" + + entry = registry.async_get_or_create( + "binary_sensor", "super_platform", "very_unique" + ) + assert entry.device_class == "class_by_user" + assert entry.original_device_class == "class_by_integration" + + entry = registry.async_get_or_create("cover", "super_platform", "very_unique") + assert entry.device_class == "class_by_user" + assert entry.original_device_class == "class_by_integration" + + @pytest.mark.parametrize("load_registries", [False]) async def test_loading_invalid_entity_id(hass, hass_storage): """Test we skip entities with invalid entity IDs.""" From 377791d6e7401b512b9110d1c2e1e4e1eccdce4a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 1 Sep 2022 17:51:27 +0200 Subject: [PATCH 016/231] Include entity registry id in entity registry WS API (#77668) --- homeassistant/components/config/entity_registry.py | 1 + tests/components/config/test_entity_registry.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 6d022aa2d14..cbfd092bc0c 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -237,6 +237,7 @@ def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: "entity_id": entry.entity_id, "hidden_by": entry.hidden_by, "icon": entry.icon, + "id": entry.id, "name": entry.name, "original_name": entry.original_name, "platform": entry.platform, diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 11a2adb5646..30153195eec 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,4 +1,6 @@ """Test entity_registry API.""" +from unittest.mock import ANY + import pytest from homeassistant.components.config import entity_registry @@ -67,6 +69,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", @@ -81,6 +84,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": None, "original_name": None, "platform": "test_platform", @@ -117,6 +121,7 @@ async def test_list_entities(hass, client): "has_entity_name": False, "hidden_by": None, "icon": None, + "id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", @@ -159,6 +164,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.name", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": "Hello World", "options": {}, @@ -189,6 +195,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.no_name", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": None, "options": {}, @@ -252,6 +259,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {}, @@ -324,6 +332,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {}, @@ -361,6 +370,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "id": ANY, "has_entity_name": False, "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, @@ -409,6 +419,7 @@ async def test_update_entity_require_restart(hass, client): "entity_category": None, "entity_id": entity_id, "icon": None, + "id": ANY, "hidden_by": None, "has_entity_name": False, "name": None, @@ -515,6 +526,7 @@ async def test_update_entity_no_changes(hass, client): "entity_id": "test_domain.world", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": "name of entity", "options": {}, @@ -601,6 +613,7 @@ async def test_update_entity_id(hass, client): "entity_id": "test_domain.planet", "hidden_by": None, "icon": None, + "id": ANY, "has_entity_name": False, "name": None, "options": {}, From ee0e12ac460bab29d557968f700835a0ffb4ef18 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 1 Sep 2022 17:57:49 +0100 Subject: [PATCH 017/231] Fix async_all_discovered_devices(False) to return connectable and unconnectable devices (#77670) --- homeassistant/components/bluetooth/manager.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d2b59469bd9..d274939c610 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -221,10 +221,14 @@ class BluetoothManager: @hass_callback def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]: """Return all of discovered devices from all the scanners including duplicates.""" - return itertools.chain.from_iterable( - scanner.discovered_devices - for scanner in self._get_scanners_by_type(connectable) + yield from itertools.chain.from_iterable( + scanner.discovered_devices for scanner in self._get_scanners_by_type(True) ) + if not connectable: + yield from itertools.chain.from_iterable( + scanner.discovered_devices + for scanner in self._get_scanners_by_type(False) + ) @hass_callback def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]: From 0cdbb295bc617d5f333fd846ada4dcbc0077f2ce Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 2 Sep 2022 00:08:14 +0200 Subject: [PATCH 018/231] bump pynetgear to 0.10.8 (#77672) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 69a21e5aace..92b3065147c 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.7"], + "requirements": ["pynetgear==0.10.8"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 502851c9b83..a7b8fcc8e0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1710,7 +1710,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.7 +pynetgear==0.10.8 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5efad4639d7..a733b8debea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1199,7 +1199,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.7 +pynetgear==0.10.8 # homeassistant.components.nina pynina==0.1.8 From c9d4924deadf715a093ebb1e19c1082381715ce6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 19:00:14 +0000 Subject: [PATCH 019/231] Bump pySwitchbot to 0.18.22 (#77673) --- 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 cda3f958f5c..9fb73a62dd6 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.18.21"], + "requirements": ["PySwitchbot==0.18.22"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a7b8fcc8e0e..2b17b73d29c 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.18.21 +PySwitchbot==0.18.22 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a733b8debea..b99703f4f07 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.18.21 +PySwitchbot==0.18.22 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From dc2c0a159f8c78d380cdba552d9b06edd517487b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 Sep 2022 18:10:20 +0000 Subject: [PATCH 020/231] Ensure unique id is set for esphome when setup via user flow (#77677) --- homeassistant/components/esphome/__init__.py | 4 +++ .../components/esphome/config_flow.py | 2 ++ tests/components/esphome/test_config_flow.py | 30 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2a885ed90ec..07b6d3071f6 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -333,6 +333,10 @@ async def async_setup_entry( # noqa: C901 if entry_data.device_info is not None and entry_data.device_info.name: cli.expected_name = entry_data.device_info.name reconnect_logic.name = entry_data.device_info.name + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=entry_data.device_info.name + ) await reconnect_logic.start() entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 9fd12634e43..ea64fb7fb7f 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -331,6 +331,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): await cli.disconnect(force=True) self._name = self._device_info.name + await self.async_set_unique_id(self._name, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) return None diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 43e2f916082..a4d1f416868 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -81,6 +81,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): CONF_NOISE_PSK: "", } assert result["title"] == "test" + assert result["result"].unique_id == "test" assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 @@ -91,6 +92,35 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): assert mock_client.noise_psk is None +async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): + """Test setup up the same name updates the host.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="test", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data=None, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) + + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == "127.0.0.1" + + async def test_user_resolve_error(hass, mock_client, mock_zeroconf): """Test user step with IP resolve error.""" From 329c6920650e0d257d4d57c520e6f3d8213f92bd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 15:00:50 -0400 Subject: [PATCH 021/231] Pin Pandas 1.4.3 (#77679) --- 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 a456e9ec965..84edd18206c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -126,3 +126,6 @@ pubnub!=6.4.0 # Package's __init__.pyi stub has invalid syntax and breaks mypy # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 + +# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 +pandas==1.4.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3709d4cff08..d0eb830f088 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -139,6 +139,9 @@ pubnub!=6.4.0 # Package's __init__.pyi stub has invalid syntax and breaks mypy # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 + +# Pandas 1.4.4 has issues with wheels om armhf + Py3.10 +pandas==1.4.3 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From f4273a098da8df7b0d024ff18d7b54d92b96f4f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Sep 2022 01:22:12 +0000 Subject: [PATCH 022/231] Bump bluetooth-adapters to 0.3.3 (#77683) --- 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 981b7854e36..9bc4c50a1e4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.2", + "bluetooth-adapters==0.3.3", "bluetooth-auto-recovery==0.3.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 84edd18206c..56a6e5efd05 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2b17b73d29c..5a69abaf5d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b99703f4f07..85d67928246 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.2 +bluetooth-adapters==0.3.3 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 From 1f9c5ff3699f47b77ebf3e2395b6a1a189c7ccb2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 21:24:30 -0400 Subject: [PATCH 023/231] Bump frontend to 20220901.0 (#77689) --- 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 1bf8962d615..ebaa83f8d46 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==20220831.0"], + "requirements": ["home-assistant-frontend==20220901.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56a6e5efd05..1fe755a9321 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5a69abaf5d0..46a997631f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85d67928246..a46bf30baae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220831.0 +home-assistant-frontend==20220901.0 # homeassistant.components.home_connect homeconnect==0.7.2 From a10a16ab21b26cb17276dfff537eb50b2deec66c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Sep 2022 21:25:12 -0400 Subject: [PATCH 024/231] Bumped version to 2022.9.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 8d5242123e5..b3ca5fd9fe3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -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 a2e1c4063a9..993fcd99839 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b1" +version = "2022.9.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 36c1b9a4191197420374d0dd034f769bffe1a3ad Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 1 Sep 2022 04:49:36 -0400 Subject: [PATCH 025/231] Fix timezone edge cases for Unifi Protect media source (#77636) * Fixes timezone edge cases for Unifi Protect media source * linting --- .../components/unifiprotect/media_source.py | 19 ++- .../unifiprotect/test_media_source.py | 133 ++++++++++++++++-- 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 58b14ab9b3b..4910c18cf5f 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -101,12 +101,12 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: @callback -def _get_start_end(hass: HomeAssistant, start: datetime) -> tuple[datetime, datetime]: +def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]: start = dt_util.as_local(start) end = dt_util.now() - start = start.replace(day=1, hour=1, minute=0, second=0, microsecond=0) - end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + start = start.replace(day=1, hour=0, minute=0, second=1, microsecond=0) + end = end.replace(day=1, hour=0, minute=0, second=2, microsecond=0) return start, end @@ -571,9 +571,16 @@ class ProtectMediaSource(MediaSource): if not build_children: return source - month = start.month + if data.api.bootstrap.recording_start is not None: + recording_start = data.api.bootstrap.recording_start.date() + start = max(recording_start, start) + + recording_end = dt_util.now().date() + end = start.replace(month=start.month + 1) - timedelta(days=1) + end = min(recording_end, end) + children = [self._build_days(data, camera_id, event_type, start, is_all=True)] - while start.month == month: + while start <= end: children.append( self._build_days(data, camera_id, event_type, start, is_all=False) ) @@ -702,7 +709,7 @@ class ProtectMediaSource(MediaSource): self._build_recent(data, camera_id, event_type, 30), ] - start, end = _get_start_end(self.hass, data.api.bootstrap.recording_start) + start, end = _get_month_start_end(data.api.bootstrap.recording_start) while end > start: children.append(self._build_month(data, camera_id, event_type, end.date())) end = (end - timedelta(days=1)).replace(day=1) diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index bb3bc8aa345..74a007e0ba0 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -4,7 +4,9 @@ from datetime import datetime, timedelta from ipaddress import IPv4Address from unittest.mock import AsyncMock, Mock, patch +from freezegun import freeze_time import pytest +import pytz from pyunifiprotect.data import ( Bootstrap, Camera, @@ -28,6 +30,7 @@ from homeassistant.components.unifiprotect.media_source import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .conftest import MockUFPFixture from .utils import init_entry @@ -430,13 +433,52 @@ async def test_browse_media_event_type( assert browse.children[3].identifier == "test_id:browse:all:smart" +ONE_MONTH_SIMPLE = ( + datetime( + year=2022, + month=9, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_SIMPLE = ( + datetime( + year=2022, + month=8, + day=31, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_SIMPLE, TWO_MONTH_SIMPLE], +) +@freeze_time("2022-09-15 03:00:00-07:00") async def test_browse_media_time( - hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, ): """Test browsing time selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + end = datetime.fromisoformat("2022-09-15 03:00:00-07:00") + end_local = dt_util.as_local(end) + + 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) @@ -449,17 +491,89 @@ async def test_browse_media_time( assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" assert browse.identifier == base_id - assert len(browse.children) == 4 + assert len(browse.children) == 3 + months assert browse.children[0].title == "Last 24 Hours" assert browse.children[0].identifier == f"{base_id}:recent:1" assert browse.children[1].title == "Last 7 Days" assert browse.children[1].identifier == f"{base_id}:recent:7" assert browse.children[2].title == "Last 30 Days" assert browse.children[2].identifier == f"{base_id}:recent:30" - assert browse.children[3].title == f"{fixed_now.strftime('%B %Y')}" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" assert ( browse.children[3].identifier - == f"{base_id}:range:{fixed_now.year}:{fixed_now.month}" + == f"{base_id}:range:{end_local.year}:{end_local.month}" + ) + + +ONE_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=8, + day=1, + hour=3, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 1, +) +TWO_MONTH_TIMEZONE = ( + datetime( + year=2022, + month=7, + day=31, + hour=21, + minute=0, + second=0, + microsecond=0, + tzinfo=pytz.timezone("US/Pacific"), + ), + 2, +) + + +@pytest.mark.parametrize( + "start,months", + [ONE_MONTH_TIMEZONE, TWO_MONTH_TIMEZONE], +) +@freeze_time("2022-08-31 21:00:00-07:00") +async def test_browse_media_time_timezone( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + start: datetime, + months: int, +): + """Test browsing time selector level media.""" + + end = datetime.fromisoformat("2022-08-31 21:00:00-07:00") + end_local = dt_util.as_local(end) + + 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" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" + assert browse.identifier == base_id + assert len(browse.children) == 3 + months + assert browse.children[0].title == "Last 24 Hours" + assert browse.children[0].identifier == f"{base_id}:recent:1" + assert browse.children[1].title == "Last 7 Days" + assert browse.children[1].identifier == f"{base_id}:recent:7" + assert browse.children[2].title == "Last 30 Days" + assert browse.children[2].identifier == f"{base_id}:recent:30" + assert browse.children[3].title == f"{end_local.strftime('%B %Y')}" + assert ( + browse.children[3].identifier + == f"{base_id}:range:{end_local.year}:{end_local.month}" ) @@ -599,13 +713,14 @@ async def test_browse_media_eventthumb( assert browse.media_class == MEDIA_CLASS_IMAGE +@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 ): """Test browsing day selector level media.""" - last_month = fixed_now.replace(day=1) - timedelta(days=1) - ufp.api.bootstrap._recording_start = last_month + start = datetime.fromisoformat("2022-09-03 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) @@ -623,7 +738,7 @@ async def test_browse_media_day( == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" ) assert browse.identifier == base_id - assert len(browse.children) in (29, 30, 31, 32) + assert len(browse.children) == 14 assert browse.children[0].title == "Whole Month" assert browse.children[0].identifier == f"{base_id}:all" From 9652c0c3267c578aa9aca00ac50304e372043f37 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 2 Sep 2022 14:18:10 -0600 Subject: [PATCH 026/231] Adjust litterrobot platform loading/unloading (#77682) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/litterrobot/__init__.py | 57 +++++++------------ tests/components/litterrobot/conftest.py | 4 +- tests/components/litterrobot/test_vacuum.py | 9 ++- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index d302989fc01..742e9dcb9c7 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,7 +1,7 @@ """The Litter-Robot integration.""" from __future__ import annotations -from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4 +from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -10,65 +10,48 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = [ - Platform.BUTTON, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, -] - PLATFORMS_BY_TYPE = { - LitterRobot: ( - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - LitterRobot3: ( - Platform.BUTTON, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - LitterRobot4: ( - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - Platform.VACUUM, - ), - FeederRobot: ( - Platform.BUTTON, + Robot: ( Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ), + LitterRobot: (Platform.VACUUM,), + LitterRobot3: (Platform.BUTTON,), + FeederRobot: (Platform.BUTTON,), } +def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]: + """Get platforms for robots.""" + return { + platform + for robot in robots + for robot_type, platforms in PLATFORMS_BY_TYPE.items() + if isinstance(robot, robot_type) + for platform in platforms + } + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) await hub.login(load_robots=True) - platforms: set[str] = set() - for robot in hub.account.robots: - platforms.update(PLATFORMS_BY_TYPE[type(robot)]) - if platforms: + if platforms := get_platforms_for_robots(hub.account.robots): 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.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] await hub.account.disconnect() + platforms = get_platforms_for_robots(hub.account.robots) + unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index d5d29e12988..e5d5e730b61 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -99,8 +99,8 @@ async def setup_integration( with patch( "homeassistant.components.litterrobot.hub.Account", return_value=mock_account ), patch( - "homeassistant.components.litterrobot.PLATFORMS", - [platform_domain] if platform_domain else [], + "homeassistant.components.litterrobot.PLATFORMS_BY_TYPE", + {Robot: (platform_domain,)} if platform_domain else {}, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index eb9a4c8c60b..08aa8b2399b 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -52,6 +52,7 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + assert len(ent_reg.entities) == 1 assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) vacuum = hass.states.get(VACUUM_ENTITY_ID) @@ -78,10 +79,16 @@ async def test_no_robots( hass: HomeAssistant, mock_account_with_no_robots: MagicMock ) -> None: """Tests the vacuum entity was set up.""" - await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) + entry = await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) assert not hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) + ent_reg = er.async_get(hass) + assert len(ent_reg.entities) == 0 + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + async def test_vacuum_with_error( hass: HomeAssistant, mock_account_with_error: MagicMock From 6fff63332524eb75b6693950fab5e07b6b0fc6c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 Sep 2022 20:17:35 +0000 Subject: [PATCH 027/231] Bump bluetooth-adapters to 3.3.4 (#77705) --- 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 9bc4c50a1e4..b98312040f0 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.3", + "bluetooth-adapters==0.3.4", "bluetooth-auto-recovery==0.3.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1fe755a9321..8cfb71c5db0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 bluetooth-auto-recovery==0.3.0 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 46a997631f2..265d7b277e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a46bf30baae..63a5a9bb744 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.3 +bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.0 From 1d2439a6e54e8a5004de3908d905b8bbb7073a3d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 2 Sep 2022 10:50:05 -0400 Subject: [PATCH 028/231] Change zwave_js firmware update service API key (#77719) * Change zwave_js firmware update service API key * Update const.py --- homeassistant/components/zwave_js/const.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index cd10109bb3d..ddd4917e596 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -123,4 +123,6 @@ ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" # This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for # your own (https://github.com/zwave-js/firmware-updates/). -API_KEY_FIRMWARE_UPDATE_SERVICE = "b48e74337db217f44e1e003abb1e9144007d260a17e2b2422e0a45d0eaf6f4ad86f2a9943f17fee6dde343941f238a64" +API_KEY_FIRMWARE_UPDATE_SERVICE = ( + "55eea74f055bef2ad893348112df6a38980600aaf82d2b02011297fc7ba495f830ca2b70" +) From d6a99da461fa5b9b0b2defc9afbfcf52b96a1a66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Sep 2022 20:53:33 -0400 Subject: [PATCH 029/231] Bump frontend to 20220902.0 (#77734) --- 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 ebaa83f8d46..8459d08eab7 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==20220901.0"], + "requirements": ["home-assistant-frontend==20220902.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8cfb71c5db0..2e41ae13458 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 265d7b277e2..51b7a506ef3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63a5a9bb744..1b613121245 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220901.0 +home-assistant-frontend==20220902.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 041eaf27a96fbc8206b9c13d2864452cf9ff5145 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 2 Sep 2022 20:54:37 -0400 Subject: [PATCH 030/231] Bumped version to 2022.9.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 b3ca5fd9fe3..502dd3510e2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -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 993fcd99839..3d608e722a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b2" +version = "2022.9.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bc04755d05871fe3052324bcbbbcd7e043db4ffc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 2 Sep 2022 23:50:52 +0200 Subject: [PATCH 031/231] Register xiaomi_miio unload callbacks later in setup (#76714) --- homeassistant/components/xiaomi_miio/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 9f0be1da528..8719319aec8 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -387,8 +387,6 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> if gateway_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) - entry.async_on_unload(entry.add_update_listener(update_listener)) - # Connect to gateway gateway = ConnectXiaomiGateway(hass, entry) try: @@ -444,6 +442,8 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Xiaomi Miio device component from a config entry.""" @@ -453,10 +453,10 @@ async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> b if not platforms: return False - entry.async_on_unload(entry.add_update_listener(update_listener)) - await hass.config_entries.async_forward_entry_setups(entry, platforms) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True From cd4c31bc79aa98c1e363a1220125b439e2c5c8e8 Mon Sep 17 00:00:00 2001 From: Simon Hansen <67142049+DurgNomis-drol@users.noreply.github.com> Date: Sat, 3 Sep 2022 10:32:03 +0200 Subject: [PATCH 032/231] Convert platform in iss integration (#77218) * Hopefully fix everthing and be happy * ... * update coverage file * Fix tests --- .coveragerc | 4 +-- homeassistant/components/iss/__init__.py | 14 +++------ homeassistant/components/iss/config_flow.py | 7 ++--- .../iss/{binary_sensor.py => sensor.py} | 31 ++++++------------- tests/components/iss/test_config_flow.py | 17 ---------- 5 files changed, 17 insertions(+), 56 deletions(-) rename homeassistant/components/iss/{binary_sensor.py => sensor.py} (67%) diff --git a/.coveragerc b/.coveragerc index 3ff0d49965c..99d98d36e6b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -587,7 +587,7 @@ omit = homeassistant/components/iqvia/sensor.py homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/iss/__init__.py - homeassistant/components/iss/binary_sensor.py + homeassistant/components/iss/sensor.py homeassistant/components/isy994/__init__.py homeassistant/components/isy994/binary_sensor.py homeassistant/components/isy994/climate.py @@ -1216,7 +1216,7 @@ omit = homeassistant/components/switchbot/const.py homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/cover.py - homeassistant/components/switchbot/light.py + homeassistant/components/switchbot/light.py homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/coordinator.py homeassistant/components/switchmate/switch.py diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index d6065fd4f78..640e9d5d1da 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging import pyiss @@ -18,7 +18,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [Platform.SENSOR] @dataclass @@ -27,31 +27,25 @@ class IssData: number_of_people_in_space: int current_location: dict[str, str] - is_above: bool - next_rise: datetime -def update(iss: pyiss.ISS, latitude: float, longitude: float) -> IssData: +def update(iss: pyiss.ISS) -> IssData: """Retrieve data from the pyiss API.""" return IssData( number_of_people_in_space=iss.number_of_people_in_space(), current_location=iss.current_location(), - is_above=iss.is_ISS_above(latitude, longitude), - next_rise=iss.next_rise(latitude, longitude), ) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" hass.data.setdefault(DOMAIN, {}) - latitude = hass.config.latitude - longitude = hass.config.longitude iss = pyiss.ISS() async def async_update() -> IssData: try: - return await hass.async_add_executor_job(update, iss, latitude, longitude) + return await hass.async_add_executor_job(update, iss) except (HTTPError, requests.exceptions.ConnectionError) as ex: raise UpdateFailed("Unable to retrieve data") from ex diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py index b43949daadc..ebfd445f62c 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -7,9 +7,10 @@ from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .binary_sensor import DEFAULT_NAME from .const import DOMAIN +DEFAULT_NAME = "ISS" + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for iss component.""" @@ -30,10 +31,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - # Check if location have been defined. - if not self.hass.config.latitude and not self.hass.config.longitude: - return self.async_abort(reason="latitude_longitude_not_defined") - if user_input is not None: return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/sensor.py similarity index 67% rename from homeassistant/components/iss/binary_sensor.py rename to homeassistant/components/iss/sensor.py index 77cb86fc45a..fac23dfd9fa 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/sensor.py @@ -1,10 +1,10 @@ -"""Support for iss binary sensor.""" +"""Support for iss sensor.""" from __future__ import annotations import logging from typing import Any -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant @@ -19,12 +19,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_ISS_NEXT_RISE = "next_rise" -ATTR_ISS_NUMBER_PEOPLE_SPACE = "number_of_people_in_space" - -DEFAULT_NAME = "ISS" -DEFAULT_DEVICE_CLASS = "visible" - async def async_setup_entry( hass: HomeAssistant, @@ -37,15 +31,11 @@ async def async_setup_entry( name = entry.title show_on_map = entry.options.get(CONF_SHOW_ON_MAP, False) - async_add_entities([IssBinarySensor(coordinator, name, show_on_map)]) + async_add_entities([IssSensor(coordinator, name, show_on_map)]) -class IssBinarySensor( - CoordinatorEntity[DataUpdateCoordinator[IssData]], BinarySensorEntity -): - """Implementation of the ISS binary sensor.""" - - _attr_device_class = DEFAULT_DEVICE_CLASS +class IssSensor(CoordinatorEntity[DataUpdateCoordinator[IssData]], SensorEntity): + """Implementation of the ISS sensor.""" def __init__( self, coordinator: DataUpdateCoordinator[IssData], name: str, show: bool @@ -57,17 +47,14 @@ class IssBinarySensor( self._show_on_map = show @property - def is_on(self) -> bool: - """Return true if the binary sensor is on.""" - return self.coordinator.data.is_above is True + def native_value(self) -> int: + """Return number of people in space.""" + return self.coordinator.data.number_of_people_in_space @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - attrs = { - ATTR_ISS_NUMBER_PEOPLE_SPACE: self.coordinator.data.number_of_people_in_space, - ATTR_ISS_NEXT_RISE: self.coordinator.data.next_rise, - } + attrs = {} if self._show_on_map: attrs[ATTR_LONGITUDE] = self.coordinator.data.current_location.get( "longitude" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index eabca610ddf..a806bea3056 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -3,7 +3,6 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.iss.const import DOMAIN -from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant @@ -48,22 +47,6 @@ async def test_integration_already_exists(hass: HomeAssistant): assert result.get("reason") == "single_instance_allowed" -async def test_abort_no_home(hass: HomeAssistant): - """Test we don't create an entry if no coordinates are set.""" - - await async_process_ha_core_config( - hass, - {"latitude": 0.0, "longitude": 0.0}, - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={} - ) - - assert result.get("type") == data_entry_flow.FlowResultType.ABORT - assert result.get("reason") == "latitude_longitude_not_defined" - - async def test_options(hass: HomeAssistant): """Test options flow.""" From 0e930fd626c4ce41e531c1c0732f89bdbc781b26 Mon Sep 17 00:00:00 2001 From: Pete Date: Sat, 3 Sep 2022 22:55:34 +0200 Subject: [PATCH 033/231] Fix setting and reading percentage for MIOT based fans (#77626) --- homeassistant/components/xiaomi_miio/const.py | 9 +++++ homeassistant/components/xiaomi_miio/fan.py | 36 ++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c0711a02a36..11922956c25 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -112,6 +112,15 @@ MODELS_FAN_MIOT = [ MODEL_FAN_ZA5, ] +# number of speed levels each fan has +SPEEDS_FAN_MIOT = { + MODEL_FAN_1C: 3, + MODEL_FAN_P10: 4, + MODEL_FAN_P11: 4, + MODEL_FAN_P9: 4, + MODEL_FAN_ZA5: 4, +} + MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3C, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 39988976564..901211d1d2d 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -85,6 +85,7 @@ from .const import ( MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_EXTRA_FEATURES, + SPEEDS_FAN_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -234,9 +235,13 @@ async def async_setup_entry( elif model in MODELS_FAN_MIIO: entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) + speed_count = SPEEDS_FAN_MIOT[model] + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator, speed_count) elif model in MODELS_FAN_MIOT: - entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) + speed_count = SPEEDS_FAN_MIOT[model] + entity = XiaomiFanMiot( + device, config_entry, unique_id, coordinator, speed_count + ) else: return @@ -1044,6 +1049,11 @@ class XiaomiFanP5(XiaomiGenericFan): class XiaomiFanMiot(XiaomiGenericFan): """Representation of a Xiaomi Fan Miot.""" + def __init__(self, device, entry, unique_id, coordinator, speed_count): + """Initialize MIOT fan with speed count.""" + super().__init__(device, entry, unique_id, coordinator) + self._speed_count = speed_count + @property def operation_mode_class(self): """Hold operation mode class.""" @@ -1061,7 +1071,9 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: - self._percentage = self.coordinator.data.speed + self._percentage = ranged_value_to_percentage( + (1, self._speed_count), self.coordinator.data.speed + ) else: self._percentage = 0 @@ -1087,16 +1099,22 @@ class XiaomiFanMiot(XiaomiGenericFan): await self.async_turn_off() return - await self._try_command( - "Setting fan speed percentage of the miio device failed.", - self._device.set_speed, - percentage, + speed = math.ceil( + percentage_to_ranged_value((1, self._speed_count), percentage) ) - self._percentage = percentage + # if the fan is not on, we have to turn it on first if not self.is_on: await self.async_turn_on() - else: + + result = await self._try_command( + "Setting fan speed percentage of the miio device failed.", + self._device.set_speed, + speed, + ) + + if result: + self._percentage = ranged_value_to_percentage((1, self._speed_count), speed) self.async_write_ha_state() From b215514c901ae5f34167cc5333a3574dd9dbbfc8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:19:52 +0200 Subject: [PATCH 034/231] Fix upgrade api disabling during setup of Synology DSM (#77753) --- homeassistant/components/synology_dsm/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 12bad2954dd..82f2c214804 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -102,6 +102,7 @@ class SynoApi: self.dsm.upgrade.update() except SynologyDSMAPIErrorException as ex: self._with_upgrade = False + self.dsm.reset(SynoCoreUpgrade.API_KEY) LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) self._fetch_device_configuration() From 9733887b6aa88beb45b0e5109af07f18455f17ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 09:45:52 -0400 Subject: [PATCH 035/231] Add BlueMaestro integration (#77758) * Add BlueMaestro integration * tests * dc --- CODEOWNERS | 2 + .../components/bluemaestro/__init__.py | 49 +++++ .../components/bluemaestro/config_flow.py | 94 ++++++++ homeassistant/components/bluemaestro/const.py | 3 + .../components/bluemaestro/device.py | 31 +++ .../components/bluemaestro/manifest.json | 16 ++ .../components/bluemaestro/sensor.py | 149 +++++++++++++ .../components/bluemaestro/strings.json | 22 ++ .../bluemaestro/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 5 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bluemaestro/__init__.py | 26 +++ tests/components/bluemaestro/conftest.py | 8 + .../bluemaestro/test_config_flow.py | 200 ++++++++++++++++++ tests/components/bluemaestro/test_sensor.py | 50 +++++ 17 files changed, 684 insertions(+) create mode 100644 homeassistant/components/bluemaestro/__init__.py create mode 100644 homeassistant/components/bluemaestro/config_flow.py create mode 100644 homeassistant/components/bluemaestro/const.py create mode 100644 homeassistant/components/bluemaestro/device.py create mode 100644 homeassistant/components/bluemaestro/manifest.json create mode 100644 homeassistant/components/bluemaestro/sensor.py create mode 100644 homeassistant/components/bluemaestro/strings.json create mode 100644 homeassistant/components/bluemaestro/translations/en.json create mode 100644 tests/components/bluemaestro/__init__.py create mode 100644 tests/components/bluemaestro/conftest.py create mode 100644 tests/components/bluemaestro/test_config_flow.py create mode 100644 tests/components/bluemaestro/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index b135a418566..55eda64cbe8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,6 +137,8 @@ build.json @home-assistant/supervisor /tests/components/blebox/ @bbx-a @riokuu /homeassistant/components/blink/ @fronzbot /tests/components/blink/ @fronzbot +/homeassistant/components/bluemaestro/ @bdraco +/tests/components/bluemaestro/ @bdraco /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core /homeassistant/components/bluesound/ @thrawnarn diff --git a/homeassistant/components/bluemaestro/__init__.py b/homeassistant/components/bluemaestro/__init__.py new file mode 100644 index 00000000000..45eebedcfb2 --- /dev/null +++ b/homeassistant/components/bluemaestro/__init__.py @@ -0,0 +1,49 @@ +"""The BlueMaestro integration.""" +from __future__ import annotations + +import logging + +from bluemaestro_ble import BlueMaestroBluetoothDeviceData + +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 BlueMaestro BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = BlueMaestroBluetoothDeviceData() + 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/bluemaestro/config_flow.py b/homeassistant/components/bluemaestro/config_flow.py new file mode 100644 index 00000000000..ccb548fa42b --- /dev/null +++ b/homeassistant/components/bluemaestro/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for bluemaestro ble integration.""" +from __future__ import annotations + +from typing import Any + +from bluemaestro_ble import BlueMaestroBluetoothDeviceData 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 BlueMaestroConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for bluemaestro.""" + + 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/bluemaestro/const.py b/homeassistant/components/bluemaestro/const.py new file mode 100644 index 00000000000..757f1b7c810 --- /dev/null +++ b/homeassistant/components/bluemaestro/const.py @@ -0,0 +1,3 @@ +"""Constants for the BlueMaestro integration.""" + +DOMAIN = "bluemaestro" diff --git a/homeassistant/components/bluemaestro/device.py b/homeassistant/components/bluemaestro/device.py new file mode 100644 index 00000000000..3d6e4546882 --- /dev/null +++ b/homeassistant/components/bluemaestro/device.py @@ -0,0 +1,31 @@ +"""Support for BlueMaestro devices.""" +from __future__ import annotations + +from bluemaestro_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 bluemaestro 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/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json new file mode 100644 index 00000000000..0ff9cdd0794 --- /dev/null +++ b/homeassistant/components/bluemaestro/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "bluemaestro", + "name": "BlueMaestro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bluemaestro", + "bluetooth": [ + { + "manufacturer_id": 307, + "connectable": false + } + ], + "requirements": ["bluemaestro-ble==0.2.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py new file mode 100644 index 00000000000..8afdef48d51 --- /dev/null +++ b/homeassistant/components/bluemaestro/sensor.py @@ -0,0 +1,149 @@ +"""Support for BlueMaestro sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from bluemaestro_ble import ( + SensorDeviceClass as BlueMaestroSensorDeviceClass, + SensorUpdate, + Units, +) + +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 ( + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +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 = { + (BlueMaestroSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (BlueMaestroSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + 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, + ), + ( + BlueMaestroSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.DEW_POINT, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + BlueMaestroSensorDeviceClass.PRESSURE, + Units.PRESSURE_MBAR, + ): SensorEntityDescription( + key=f"{BlueMaestroSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +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[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + 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 BlueMaestro 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( + BlueMaestroBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class BlueMaestroBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a BlueMaestro sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/bluemaestro/strings.json b/homeassistant/components/bluemaestro/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/en.json b/homeassistant/components/bluemaestro/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/bluemaestro/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 14156ada20c..c217cf790b8 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,11 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ + { + "domain": "bluemaestro", + "manufacturer_id": 307, + "connectable": False + }, { "domain": "bthome", "connectable": False, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c5437e14562..19be9cfdb89 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -48,6 +48,7 @@ FLOWS = { "balboa", "blebox", "blink", + "bluemaestro", "bluetooth", "bmw_connected_drive", "bond", diff --git a/requirements_all.txt b/requirements_all.txt index 51b7a506ef3..368d2770cf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,6 +422,9 @@ blinkstick==1.2.0 # homeassistant.components.bitcoin blockchain==1.4.4 +# homeassistant.components.bluemaestro +bluemaestro-ble==0.2.0 + # homeassistant.components.decora # homeassistant.components.zengge # bluepy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b613121245..d13df506fe7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,6 +337,9 @@ blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 +# homeassistant.components.bluemaestro +bluemaestro-ble==0.2.0 + # homeassistant.components.bluetooth bluetooth-adapters==0.3.4 diff --git a/tests/components/bluemaestro/__init__.py b/tests/components/bluemaestro/__init__.py new file mode 100644 index 00000000000..bd9b86e040f --- /dev/null +++ b/tests/components/bluemaestro/__init__.py @@ -0,0 +1,26 @@ +"""Tests for the BlueMaestro integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_BLUEMAESTRO_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", +) + +BLUEMAESTRO_SERVICE_INFO = BluetoothServiceInfo( + name="FA17B62C", + manufacturer_data={ + 307: b"\x17d\x0e\x10\x00\x02\x00\xf2\x01\xf2\x00\x83\x01\x00\x01\r\x02\xab\x00\xf2\x01\xf2\x01\r\x02\xab\x00\xf2\x01\xf2\x00\xff\x02N\x00\x00\x00\x00\x00" + }, + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={}, + service_uuids=[], + source="local", +) diff --git a/tests/components/bluemaestro/conftest.py b/tests/components/bluemaestro/conftest.py new file mode 100644 index 00000000000..e40cf1e30f4 --- /dev/null +++ b/tests/components/bluemaestro/conftest.py @@ -0,0 +1,8 @@ +"""BlueMaestro session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/bluemaestro/test_config_flow.py b/tests/components/bluemaestro/test_config_flow.py new file mode 100644 index 00000000000..116380a0df0 --- /dev/null +++ b/tests/components/bluemaestro/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the BlueMaestro config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluemaestro.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import BLUEMAESTRO_SERVICE_INFO, NOT_BLUEMAESTRO_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=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.bluemaestro.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"] == "Tempo Disc THD EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_bluemaestro(hass): + """Test discovery via bluetooth not bluemaestro.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_BLUEMAESTRO_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.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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.bluemaestro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tempo Disc THD EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +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.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluemaestro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + 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="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=BLUEMAESTRO_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=BLUEMAESTRO_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=BLUEMAESTRO_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=BLUEMAESTRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.bluemaestro.config_flow.async_discovered_service_info", + return_value=[BLUEMAESTRO_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.bluemaestro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tempo Disc THD EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py new file mode 100644 index 00000000000..2f964e65481 --- /dev/null +++ b/tests/components/bluemaestro/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the BlueMaestro sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluemaestro.const import DOMAIN +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import BLUEMAESTRO_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + saved_callback(BLUEMAESTRO_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 4 + + humid_sensor = hass.states.get("sensor.tempo_disc_thd_eeff_temperature") + humid_sensor_attrs = humid_sensor.attributes + assert humid_sensor.state == "24.2" + assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "Tempo Disc THD EEFF Temperature" + assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 32a9fba58ebb51b8356cf990d7098f13b20a4d63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 05:13:34 -0400 Subject: [PATCH 036/231] Increase default august timeout (#77762) Fixes ``` 2022-08-28 20:32:46.223 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/debounce.py", line 82, in async_call await task File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 49, in _async_update_house_id await self._async_update_house_id(house_id) File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 137, in _async_update_house_id activities = await self._api.async_get_house_activities( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 96, in async_get_house_activities response = await self._async_dict_to_api( File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 294, in _async_dict_to_api response = await self._aiohttp_session.request(method, url, **api_dict) File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/client.py", line 466, in _request with timer: File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/helpers.py", line 721, in __exit__ raise asyncio.TimeoutError from None asyncio.exceptions.TimeoutError ``` --- homeassistant/components/august/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 9a724d4a87b..5b936e9f159 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -4,7 +4,7 @@ from datetime import timedelta from homeassistant.const import Platform -DEFAULT_TIMEOUT = 15 +DEFAULT_TIMEOUT = 25 CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file" CONF_LOGIN_METHOD = "login_method" From 3856178dc08d461cc37720c2fa108dac59571af4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 3 Sep 2022 16:53:21 -0400 Subject: [PATCH 037/231] Handle dead nodes in zwave_js update entity (#77763) --- homeassistant/components/zwave_js/update.py | 17 ++- tests/components/zwave_js/test_update.py | 140 +++++++++----------- 2 files changed, 77 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 134c6cc6661..1f04c3acc47 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -86,17 +86,24 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_installed_version = self._attr_latest_version = node.firmware_version - def _update_on_wake_up(self, _: dict[str, Any]) -> None: + def _update_on_status_change(self, _: dict[str, Any]) -> None: """Update the entity when node is awake.""" self._status_unsub = None self.hass.async_create_task(self.async_update(True)) async def async_update(self, write_state: bool = False) -> None: """Update the entity.""" - if self.node.status == NodeStatus.ASLEEP: - if not self._status_unsub: - self._status_unsub = self.node.once("wake up", self._update_on_wake_up) - return + for status, event_name in ( + (NodeStatus.ASLEEP, "wake up"), + (NodeStatus.DEAD, "alive"), + ): + if self.node.status == status: + if not self._status_unsub: + self._status_unsub = self.node.once( + event_name, self._update_on_status_change + ) + return + if available_firmware_updates := ( await self.driver.controller.async_get_available_firmware_updates( self.node, API_KEY_FIRMWARE_UPDATE_SERVICE diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 852dcba5954..c9ec8fa68c6 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -24,6 +24,31 @@ from homeassistant.util import datetime as dt_util from tests.common import async_fire_time_changed UPDATE_ENTITY = "update.z_wave_thermostat_firmware" +FIRMWARE_UPDATES = { + "updates": [ + { + "version": "10.11.1", + "changelog": "blah 1", + "files": [ + {"target": 0, "url": "https://example1.com", "integrity": "sha1"} + ], + }, + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"} + ], + }, + { + "version": "11.1.5", + "changelog": "blah 3", + "files": [ + {"target": 0, "url": "https://example3.com", "integrity": "sha3"} + ], + }, + ] +} async def test_update_entity_success( @@ -60,31 +85,7 @@ async def test_update_entity_success( result = await ws_client.receive_json() assert result["result"] is None - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2)) await hass.async_block_till_done() @@ -171,31 +172,7 @@ async def test_update_entity_failure( hass_ws_client, ): """Test update entity failed install.""" - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) await hass.async_block_till_done() @@ -228,31 +205,7 @@ async def test_update_entity_sleep( multisensor_6.receive_event(event) client.async_send_command.reset_mock() - client.async_send_command.return_value = { - "updates": [ - { - "version": "10.11.1", - "changelog": "blah 1", - "files": [ - {"target": 0, "url": "https://example1.com", "integrity": "sha1"} - ], - }, - { - "version": "11.2.4", - "changelog": "blah 2", - "files": [ - {"target": 0, "url": "https://example2.com", "integrity": "sha2"} - ], - }, - { - "version": "11.1.5", - "changelog": "blah 3", - "files": [ - {"target": 0, "url": "https://example3.com", "integrity": "sha3"} - ], - }, - ] - } + client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) await hass.async_block_till_done() @@ -273,3 +226,40 @@ async def test_update_entity_sleep( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" assert args["nodeId"] == multisensor_6.node_id + + +async def test_update_entity_dead( + hass, + client, + multisensor_6, + integration, +): + """Test update occurs when device is dead after it becomes alive.""" + event = Event( + "dead", + data={"source": "node", "event": "dead", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + client.async_send_command.reset_mock() + + client.async_send_command.return_value = FIRMWARE_UPDATES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) + await hass.async_block_till_done() + + # Because node is asleep we shouldn't attempt to check for firmware updates + assert len(client.async_send_command.call_args_list) == 0 + + event = Event( + "alive", + data={"source": "node", "event": "alive", "nodeId": multisensor_6.node_id}, + ) + multisensor_6.receive_event(event) + await hass.async_block_till_done() + + # Now that the node is up we can check for updates + assert len(client.async_send_command.call_args_list) > 0 + + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == multisensor_6.node_id From 5f4013164c1790b2270fc6b69bffd805012577a4 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 4 Sep 2022 07:51:48 -0700 Subject: [PATCH 038/231] Update smarttub to 0.0.33 (#77766) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 1d7500b9185..e2f72642a91 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.32"], + "requirements": ["python-smarttub==0.0.33"], "quality_scale": "platinum", "iot_class": "cloud_polling", "loggers": ["smarttub"] diff --git a/requirements_all.txt b/requirements_all.txt index 368d2770cf8..7cc189ff7d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.32 +python-smarttub==0.0.33 # homeassistant.components.songpal python-songpal==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d13df506fe7..0a59ca8e9df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1368,7 +1368,7 @@ python-nest==4.2.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.32 +python-smarttub==0.0.33 # homeassistant.components.songpal python-songpal==0.15 From 9387449abf98e05f6bd75be6717c660bf457efb7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 4 Sep 2022 18:57:50 +0200 Subject: [PATCH 039/231] Replace archived sucks by py-sucks and bump to 0.9.8 for Ecovacs integration (#77768) --- CODEOWNERS | 2 +- homeassistant/components/ecovacs/manifest.json | 4 ++-- requirements_all.txt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 55eda64cbe8..4609e6330d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,7 +277,7 @@ build.json @home-assistant/supervisor /tests/components/ecobee/ @marthoc /homeassistant/components/econet/ @vangorra @w1ll1am23 /tests/components/econet/ @vangorra @w1ll1am23 -/homeassistant/components/ecovacs/ @OverloadUT +/homeassistant/components/ecovacs/ @OverloadUT @mib1185 /homeassistant/components/ecowitt/ @pvizeli /tests/components/ecowitt/ @pvizeli /homeassistant/components/edl21/ @mtdcr diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 1712cea1578..3ac277217b8 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -2,8 +2,8 @@ "domain": "ecovacs", "name": "Ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs", - "requirements": ["sucks==0.9.4"], - "codeowners": ["@OverloadUT"], + "requirements": ["py-sucks==0.9.8"], + "codeowners": ["@OverloadUT", "@mib1185"], "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7cc189ff7d1..619035ae83b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1351,6 +1351,9 @@ py-nightscout==1.2.2 # homeassistant.components.schluter py-schluter==0.1.7 +# homeassistant.components.ecovacs +py-sucks==0.9.8 + # homeassistant.components.synology_dsm py-synologydsm-api==1.0.8 @@ -2308,9 +2311,6 @@ stringcase==1.2.0 # homeassistant.components.subaru subarulink==0.5.0 -# homeassistant.components.ecovacs -sucks==0.9.4 - # homeassistant.components.solarlog sunwatcher==0.2.1 From ea0b40669251cb4fef42e951673dd8ca16bd2ed4 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 2 Sep 2022 08:07:21 +1000 Subject: [PATCH 040/231] Add binary sensor platform to LIFX integration (#77535) Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/__init__.py | 2 +- .../components/lifx/binary_sensor.py | 70 ++++++++++++++++++ homeassistant/components/lifx/const.py | 11 ++- homeassistant/components/lifx/coordinator.py | 35 ++++++--- homeassistant/components/lifx/light.py | 8 +- tests/components/lifx/__init__.py | 26 ++++++- tests/components/lifx/test_binary_sensor.py | 74 +++++++++++++++++++ 7 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/lifx/binary_sensor.py create mode 100644 tests/components/lifx/test_binary_sensor.py diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 6af30b91d28..5c91efa1d02 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All( ) -PLATFORMS = [Platform.BUTTON, Platform.LIGHT] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] DISCOVERY_INTERVAL = timedelta(minutes=15) MIGRATION_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py new file mode 100644 index 00000000000..4a368a2f97f --- /dev/null +++ b/homeassistant/components/lifx/binary_sensor.py @@ -0,0 +1,70 @@ +"""Binary sensor entities for LIFX integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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, HEV_CYCLE_STATE +from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity +from .util import lifx_features + +HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription( + key=HEV_CYCLE_STATE, + name="Clean Cycle", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.RUNNING, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up LIFX from a config entry.""" + coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if lifx_features(coordinator.device)["hev"]: + async_add_entities( + [ + LIFXBinarySensorEntity( + coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR + ) + ] + ) + + +class LIFXBinarySensorEntity(LIFXEntity, BinarySensorEntity): + """LIFX sensor entity base class.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: LIFXUpdateCoordinator, + 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._async_update_attrs() + + @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_is_on = self.coordinator.async_get_hev_cycle_state() diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index f6ec653c994..74960d59bd1 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -29,6 +29,15 @@ IDENTIFY_WAVEFORM = { IDENTIFY = "identify" RESTART = "restart" +ATTR_DURATION = "duration" +ATTR_INDICATION = "indication" +ATTR_INFRARED = "infrared" +ATTR_POWER = "power" +ATTR_REMAINING = "remaining" +ATTR_ZONES = "zones" + +HEV_CYCLE_STATE = "hev_cycle_state" + DATA_LIFX_MANAGER = "lifx_manager" -_LOGGER = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 1f3f49368ca..d01fb266c6f 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( _LOGGER, + ATTR_REMAINING, IDENTIFY_WAVEFORM, MESSAGE_RETRIES, MESSAGE_TIMEOUT, @@ -101,26 +102,25 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.device.get_hostfirmware() if self.device.product is None: self.device.get_version() - try: - response = await async_execute_lifx(self.device.get_color) - except asyncio.TimeoutError as ex: - raise UpdateFailed( - f"Failed to fetch state from device: {self.device.ip_addr}" - ) from ex + response = await async_execute_lifx(self.device.get_color) + if self.device.product is None: raise UpdateFailed( f"Failed to fetch get version from device: {self.device.ip_addr}" ) + # device.mac_addr is not the mac_address, its the serial number if self.device.mac_addr == TARGET_ANY: self.device.mac_addr = response.target_addr + if lifx_features(self.device)["multizone"]: - try: - await self.async_update_color_zones() - except asyncio.TimeoutError as ex: - raise UpdateFailed( - f"Failed to fetch zones from device: {self.device.ip_addr}" - ) from ex + await self.async_update_color_zones() + + if lifx_features(self.device)["hev"]: + if self.device.hev_cycle_configuration is None: + self.device.get_hev_configuration() + + await self.async_get_hev_cycle() async def async_update_color_zones(self) -> None: """Get updated color information for each zone.""" @@ -138,6 +138,17 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if zone == top - 1: zone -= 1 + 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: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 67bb3e91748..fe17dd95788 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -28,7 +28,7 @@ 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 DATA_LIFX_MANAGER, DOMAIN +from .const import ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN from .coordinator import LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( @@ -39,14 +39,8 @@ from .manager import ( ) from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk -SERVICE_LIFX_SET_STATE = "set_state" - COLOR_ZONE_POPULATE_DELAY = 0.3 -ATTR_INFRARED = "infrared" -ATTR_ZONES = "zones" -ATTR_POWER = "power" - SERVICE_LIFX_SET_STATE = "set_state" LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index 8259314e77c..9e137c8532a 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -22,10 +22,13 @@ DEFAULT_ENTRY_TITLE = LABEL class MockMessage: """Mock a lifx message.""" - def __init__(self): + def __init__(self, **kwargs): """Init message.""" self.target_addr = SERIAL self.count = 9 + for k, v in kwargs.items(): + if k != "callb": + setattr(self, k, v) class MockFailingLifxCommand: @@ -50,15 +53,20 @@ class MockFailingLifxCommand: class MockLifxCommand: """Mock a lifx command.""" + def __name__(self): + """Return name.""" + return "mock_lifx_command" + def __init__(self, bulb, **kwargs): """Init command.""" self.bulb = bulb self.calls = [] + self.msg_kwargs = kwargs def __call__(self, *args, **kwargs): """Call command.""" if callb := kwargs.get("callb"): - callb(self.bulb, MockMessage()) + callb(self.bulb, MockMessage(**self.msg_kwargs)) self.calls.append([args, kwargs]) def reset_mock(self): @@ -108,6 +116,20 @@ def _mocked_brightness_bulb() -> Light: return bulb +def _mocked_clean_bulb() -> Light: + bulb = _mocked_bulb() + bulb.get_hev_cycle = MockLifxCommand( + bulb, duration=7200, remaining=0, last_power=False + ) + bulb.hev_cycle = { + "duration": 7200, + "remaining": 30, + "last_power": False, + } + bulb.product = 90 + return bulb + + def _mocked_light_strip() -> Light: bulb = _mocked_bulb() bulb.product = 31 # LIFX Z diff --git a/tests/components/lifx/test_binary_sensor.py b/tests/components/lifx/test_binary_sensor.py new file mode 100644 index 00000000000..bb0b210704a --- /dev/null +++ b/tests/components/lifx/test_binary_sensor.py @@ -0,0 +1,74 @@ +"""Test the lifx binary sensor platwform.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import lifx +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_HOST, + 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 +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + _mocked_clean_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_hev_cycle_state(hass: HomeAssistant) -> None: + """Test HEV cycle state binary sensor.""" + 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() + + entity_id = "binary_sensor.my_bulb_clean_cycle" + entity_registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.RUNNING + + entry = entity_registry.async_get(entity_id) + assert state + assert entry.unique_id == f"{SERIAL}_hev_cycle_state" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + bulb.hev_cycle = {"duration": 7200, "remaining": 0, "last_power": False} + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.hev_cycle = None + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNKNOWN From f60ae406617a52a17df5929fdfa781a2f415ac9d Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 2 Sep 2022 22:13:03 +1000 Subject: [PATCH 041/231] Rename the binary sensor to better reflect its purpose (#77711) --- homeassistant/components/lifx/binary_sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py index 4a368a2f97f..273ef035757 100644 --- a/homeassistant/components/lifx/binary_sensor.py +++ b/homeassistant/components/lifx/binary_sensor.py @@ -33,15 +33,15 @@ async def async_setup_entry( if lifx_features(coordinator.device)["hev"]: async_add_entities( [ - LIFXBinarySensorEntity( + LIFXHevCycleBinarySensorEntity( coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR ) ] ) -class LIFXBinarySensorEntity(LIFXEntity, BinarySensorEntity): - """LIFX sensor entity base class.""" +class LIFXHevCycleBinarySensorEntity(LIFXEntity, BinarySensorEntity): + """LIFX HEV cycle state binary sensor.""" _attr_has_entity_name = True From f9b95cc4a41a3fc0aaf9fd0d68c1660227231cc2 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 5 Sep 2022 01:51:57 +1000 Subject: [PATCH 042/231] Fix lifx service call interference (#77770) * Fix #77735 by restoring the wait to let state settle Signed-off-by: Avi Miller * Skip the asyncio.sleep during testing Signed-off-by: Avi Miller * Patch out asyncio.sleep for lifx tests Signed-off-by: Avi Miller * Patch out a constant instead of overriding asyncio.sleep directly Signed-off-by: Avi Miller Signed-off-by: Avi Miller --- homeassistant/components/lifx/coordinator.py | 3 ++- homeassistant/components/lifx/light.py | 7 +++++-- tests/components/lifx/conftest.py | 1 - tests/components/lifx/test_button.py | 11 +++++++++++ tests/components/lifx/test_light.py | 8 +++++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index d01fb266c6f..37e753c27a3 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -25,6 +25,7 @@ from .const import ( from .util import async_execute_lifx, get_real_mac_addr, lifx_features REQUEST_REFRESH_DELAY = 0.35 +LIFX_IDENTIFY_DELAY = 3.0 class LIFXUpdateCoordinator(DataUpdateCoordinator): @@ -92,7 +93,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): # 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(3) + await asyncio.sleep(LIFX_IDENTIFY_DELAY) await self.async_set_power(state=False, duration=1) async def _async_update_data(self) -> None: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index fe17dd95788..36d3b480f74 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -39,7 +39,7 @@ from .manager import ( ) from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk -COLOR_ZONE_POPULATE_DELAY = 0.3 +LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" @@ -231,6 +231,9 @@ class LIFXLight(LIFXEntity, LightEntity): if power_off: await self.set_power(False, duration=fade) + # Avoid state ping-pong by holding off updates as the state settles + await asyncio.sleep(LIFX_STATE_SETTLE_DELAY) + # Update when the transition starts and ends await self.update_during_transition(fade) @@ -338,7 +341,7 @@ class LIFXStrip(LIFXColor): # Zone brightness is not reported when powered off if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None: await self.set_power(True) - await asyncio.sleep(COLOR_ZONE_POPULATE_DELAY) + await asyncio.sleep(LIFX_STATE_SETTLE_DELAY) await self.update_color_zones() await self.set_power(False) diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py index 326c4f75413..a243132dc65 100644 --- a/tests/components/lifx/conftest.py +++ b/tests/components/lifx/conftest.py @@ -1,5 +1,4 @@ """Tests for the lifx integration.""" - from unittest.mock import AsyncMock, MagicMock, patch import pytest diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py index abc91128e25..b166aa05d66 100644 --- a/tests/components/lifx/test_button.py +++ b/tests/components/lifx/test_button.py @@ -1,4 +1,8 @@ """Tests for button platform.""" +from unittest.mock import patch + +import pytest + from homeassistant.components import lifx from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.lifx.const import DOMAIN @@ -21,6 +25,13 @@ from . import ( from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def mock_lifx_coordinator_sleep(): + """Mock out lifx coordinator sleeps.""" + with patch("homeassistant.components.lifx.coordinator.LIFX_IDENTIFY_DELAY", 0): + yield + + async def test_button_restart(hass: HomeAssistant) -> None: """Test that a bulb can be restarted.""" config_entry = MockConfigEntry( diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 5b641e850f2..6229e130a40 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -50,6 +50,13 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.fixture(autouse=True) +def patch_lifx_state_settle_delay(): + """Set asyncio.sleep for state settles to zero.""" + with patch("homeassistant.components.lifx.light.LIFX_STATE_SETTLE_DELAY", 0): + yield + + async def test_light_unique_id(hass: HomeAssistant) -> None: """Test a light unique id.""" already_migrated_config_entry = MockConfigEntry( @@ -98,7 +105,6 @@ async def test_light_unique_id_new_firmware(hass: HomeAssistant) -> None: assert device.identifiers == {(DOMAIN, SERIAL)} -@patch("homeassistant.components.lifx.light.COLOR_ZONE_POPULATE_DELAY", 0) async def test_light_strip(hass: HomeAssistant) -> None: """Test a light strip.""" already_migrated_config_entry = MockConfigEntry( From da83ceca5b0f6a611d7e97d8da506ed3b30a1364 Mon Sep 17 00:00:00 2001 From: Justin Vanderhooft Date: Sun, 4 Sep 2022 09:56:10 -0400 Subject: [PATCH 043/231] Tweak unique id formatting for Melnor Bluetooth switches (#77773) --- homeassistant/components/melnor/switch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index c2d32c428d3..7a615a8582d 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -48,10 +48,7 @@ class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): super().__init__(coordinator) self._valve_index = valve_index - self._attr_unique_id = ( - f"switch-{self._attr_unique_id}-zone{self._valve().id}-manual" - ) - + self._attr_unique_id = f"{self._attr_unique_id}-zone{self._valve().id}-manual" self._attr_name = f"{self._device.name} Zone {self._valve().id+1}" @property From 52abf0851b489e8f457cf976e1555b9ce3e610aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 12:00:19 -0400 Subject: [PATCH 044/231] Bump flux_led to 0.28.32 (#77787) --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4afd0cdb855..632ef04e456 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.31"], + "requirements": ["flux_led==0.28.32"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 619035ae83b..a7ba397b92c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -682,7 +682,7 @@ fjaraskupan==2.0.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.31 +flux_led==0.28.32 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a59ca8e9df..1074f3eff1c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -501,7 +501,7 @@ fjaraskupan==2.0.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.31 +flux_led==0.28.32 # homeassistant.components.homekit # homeassistant.components.recorder From 9f06baa778dce68de2640f1db00c8d4b49686922 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 12:54:40 -0400 Subject: [PATCH 045/231] Bump led-ble to 0.6.0 (#77788) * Bump ble-led to 0.6.0 Fixes reading the white channel on same devices Changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v0.5.4...v0.6.0 * Bump flux_led to 0.28.32 Changelog: https://github.com/Danielhiversen/flux_led/compare/0.28.31...0.28.32 Fixes white channel support for some more older protocols * keep them in sync * Update homeassistant/components/led_ble/manifest.json --- homeassistant/components/led_ble/manifest.json | 6 ++++-- homeassistant/generated/bluetooth.py | 8 ++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 376fadcb3be..a0f5e3481d5 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.5.4"], + "requirements": ["led-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ @@ -11,7 +11,9 @@ { "local_name": "BLE-LED*" }, { "local_name": "LEDBLE*" }, { "local_name": "Triones*" }, - { "local_name": "LEDBlue*" } + { "local_name": "LEDBlue*" }, + { "local_name": "Dream~*" }, + { "local_name": "QHM-*" } ], "iot_class": "local_polling" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index c217cf790b8..83bfd3ab5eb 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -156,6 +156,14 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "led_ble", "local_name": "LEDBlue*" }, + { + "domain": "led_ble", + "local_name": "Dream~*" + }, + { + "domain": "led_ble", + "local_name": "QHM-*" + }, { "domain": "melnor", "manufacturer_data_start": [ diff --git a/requirements_all.txt b/requirements_all.txt index a7ba397b92c..6b91183a0ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.5.4 +led-ble==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1074f3eff1c..92b944acdc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.5.4 +led-ble==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 From c8156d5de6b19dd0461fdd9d7a21e31a49fae4d7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Sep 2022 18:06:04 +0200 Subject: [PATCH 046/231] Bump pysensibo to 1.0.19 (#77790) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 5ef8ff6fa4e..a2a7cbe3bd0 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.18"], + "requirements": ["pysensibo==1.0.19"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6b91183a0ad..609b032cfe3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.18 +pysensibo==1.0.19 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92b944acdc6..d12689484d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1285,7 +1285,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.18 +pysensibo==1.0.19 # homeassistant.components.serial # homeassistant.components.zha From 0d042d496de231cfef69ba662c623ac784378496 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 Sep 2022 13:00:37 -0400 Subject: [PATCH 047/231] Bumped version to 2022.9.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 502dd3510e2..7ae1fccf7f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -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 3d608e722a0..fc5260f41df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b3" +version = "2022.9.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2fa517b81bc43816acac5d3e0a80743c6a85418a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 5 Sep 2022 14:12:37 -0400 Subject: [PATCH 048/231] Make Sonos typing more complete (#68072) --- homeassistant/components/sonos/__init__.py | 38 ++++++---- .../components/sonos/binary_sensor.py | 2 +- homeassistant/components/sonos/diagnostics.py | 15 ++-- .../components/sonos/household_coordinator.py | 3 +- homeassistant/components/sonos/media.py | 5 +- .../components/sonos/media_browser.py | 59 ++++++++------ .../components/sonos/media_player.py | 76 +++++++++---------- homeassistant/components/sonos/number.py | 11 ++- homeassistant/components/sonos/sensor.py | 2 +- homeassistant/components/sonos/speaker.py | 60 ++++++++------- homeassistant/components/sonos/statistics.py | 2 +- homeassistant/components/sonos/switch.py | 37 +++++---- mypy.ini | 36 --------- script/hassfest/mypy_config.py | 15 +--- 14 files changed, 168 insertions(+), 193 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index d94b49e52f2..f0dd8e668fa 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -8,6 +8,7 @@ import datetime from functools import partial import logging import socket +from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import urlparse from soco import events_asyncio @@ -21,7 +22,7 @@ from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval, call_later @@ -93,7 +94,7 @@ class SonosData: self.favorites: dict[str, SonosFavorites] = {} self.alarms: dict[str, SonosAlarms] = {} self.topology_condition = asyncio.Condition() - self.hosts_heartbeat = None + self.hosts_heartbeat: CALLBACK_TYPE | None = None self.discovery_known: set[str] = set() self.boot_counts: dict[str, int] = {} self.mdns_names: dict[str, str] = {} @@ -168,10 +169,10 @@ class SonosDiscoveryManager: self.data = data self.hosts = set(hosts) self.discovery_lock = asyncio.Lock() - self._known_invisible = set() + self._known_invisible: set[SoCo] = set() self._manual_config_required = bool(hosts) - async def async_shutdown(self): + async def async_shutdown(self) -> None: """Stop all running tasks.""" await self._async_stop_event_listener() self._stop_manual_heartbeat() @@ -236,6 +237,8 @@ class SonosDiscoveryManager: (SonosAlarms, self.data.alarms), (SonosFavorites, self.data.favorites), ): + if TYPE_CHECKING: + coord_dict = cast(dict[str, Any], coord_dict) if soco.household_id not in coord_dict: new_coordinator = coordinator(self.hass, soco.household_id) new_coordinator.setup(soco) @@ -298,7 +301,7 @@ class SonosDiscoveryManager: ) async def _async_handle_discovery_message( - self, uid: str, discovered_ip: str, boot_seqnum: int + self, uid: str, discovered_ip: str, boot_seqnum: int | None ) -> None: """Handle discovered player creation and activity.""" async with self.discovery_lock: @@ -338,22 +341,27 @@ class SonosDiscoveryManager: async_dispatcher_send(self.hass, f"{SONOS_VANISHED}-{uid}", reason) return - discovered_ip = urlparse(info.ssdp_location).hostname - boot_seqnum = info.ssdp_headers.get("X-RINCON-BOOTSEQ") self.async_discovered_player( "SSDP", info, - discovered_ip, + cast(str, urlparse(info.ssdp_location).hostname), uid, - boot_seqnum, - info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME), + info.ssdp_headers.get("X-RINCON-BOOTSEQ"), + cast(str, info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME)), None, ) @callback def async_discovered_player( - self, source, info, discovered_ip, uid, boot_seqnum, model, mdns_name - ): + self, + source: str, + info: ssdp.SsdpServiceInfo, + discovered_ip: str, + uid: str, + boot_seqnum: str | int | None, + model: str, + mdns_name: str | None, + ) -> None: """Handle discovery via ssdp or zeroconf.""" if self._manual_config_required: _LOGGER.warning( @@ -376,10 +384,12 @@ class SonosDiscoveryManager: _LOGGER.debug("New %s discovery uid=%s: %s", source, uid, info) self.data.discovery_known.add(uid) asyncio.create_task( - self._async_handle_discovery_message(uid, discovered_ip, boot_seqnum) + self._async_handle_discovery_message( + uid, discovered_ip, cast(Optional[int], boot_seqnum) + ) ) - async def setup_platforms_and_discovery(self): + async def setup_platforms_and_discovery(self) -> None: """Set up platforms and discovery.""" await self.hass.config_entries.async_forward_entry_setups(self.entry, PLATFORMS) self.entry.async_on_unload( diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index e890c1c64a8..3f736f83922 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -109,6 +109,6 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): self.speaker.mic_enabled = self.soco.mic_enabled @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return the state of the binary sensor.""" return self.speaker.mic_enabled diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index 463884e1ea8..fda96b86215 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -47,11 +47,11 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - payload = {"current_timestamp": time.monotonic()} + payload: dict[str, Any] = {"current_timestamp": time.monotonic()} for section in ("discovered", "discovery_known"): payload[section] = {} - data = getattr(hass.data[DATA_SONOS], section) + data: set[Any] | dict[str, Any] = getattr(hass.data[DATA_SONOS], section) if isinstance(data, set): payload[section] = data continue @@ -60,7 +60,6 @@ async def async_get_config_entry_diagnostics( payload[section][key] = await async_generate_speaker_info(hass, value) else: payload[section][key] = value - return payload @@ -85,12 +84,12 @@ async def async_generate_media_info( hass: HomeAssistant, speaker: SonosSpeaker ) -> dict[str, Any]: """Generate a diagnostic payload for current media metadata.""" - payload = {} + payload: dict[str, Any] = {} for attrib in MEDIA_DIAGNOSTIC_ATTRIBUTES: payload[attrib] = getattr(speaker.media, attrib) - def poll_current_track_info(): + def poll_current_track_info() -> dict[str, Any] | str: try: return speaker.soco.avTransport.GetPositionInfo( [("InstanceID", 0), ("Channel", "Master")], @@ -110,9 +109,11 @@ async def async_generate_speaker_info( hass: HomeAssistant, speaker: SonosSpeaker ) -> dict[str, Any]: """Generate the diagnostic payload for a specific speaker.""" - payload = {} + payload: dict[str, Any] = {} - def get_contents(item): + def get_contents( + item: int | float | str | dict[str, Any] + ) -> int | float | str | dict[str, Any]: if isinstance(item, (int, float, str)): return item if isinstance(item, dict): diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index 51d7e9cec8c..29b9a005552 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -20,13 +20,14 @@ _LOGGER = logging.getLogger(__name__) class SonosHouseholdCoordinator: """Base class for Sonos household-level storage.""" + cache_update_lock: asyncio.Lock + def __init__(self, hass: HomeAssistant, household_id: str) -> None: """Initialize the data.""" self.hass = hass self.household_id = household_id self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None self.last_processed_event_id: int | None = None - self.cache_update_lock: asyncio.Lock | None = None def setup(self, soco: SoCo) -> None: """Set up the SonosAlarm instance.""" diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index 9608356ba64..24233b1316f 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -2,7 +2,6 @@ from __future__ import annotations import datetime -import logging from typing import Any from soco.core import ( @@ -43,8 +42,6 @@ UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} DURATION_SECONDS = "duration_in_s" POSITION_SECONDS = "position_in_s" -_LOGGER = logging.getLogger(__name__) - def _timespan_secs(timespan: str | None) -> None | float: """Parse a time-span into number of seconds.""" @@ -106,7 +103,7 @@ class SonosMedia: @soco_error() def poll_track_info(self) -> dict[str, Any]: """Poll the speaker for current track info, add converted position values, and return.""" - track_info = self.soco.get_current_track_info() + track_info: dict[str, Any] = self.soco.get_current_track_info() track_info[DURATION_SECONDS] = _timespan_secs(track_info.get("duration")) track_info[POSITION_SECONDS] = _timespan_secs(track_info.get("position")) return track_info diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index b2d881e8bf2..95ff08cb87b 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -5,8 +5,13 @@ from collections.abc import Callable from contextlib import suppress from functools import partial import logging +from typing import cast from urllib.parse import quote_plus, unquote +from soco.data_structures import DidlFavorite, DidlObject +from soco.ms_data_structures import MusicServiceItem +from soco.music_library import MusicLibrary + from homeassistant.components import media_source, plex, spotify from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( @@ -50,12 +55,12 @@ def get_thumbnail_url_full( ) -> str | None: """Get thumbnail URL.""" if is_internal: - item = get_media( # type: ignore[no-untyped-call] + item = get_media( media.library, media_content_id, media_content_type, ) - return getattr(item, "album_art_uri", None) # type: ignore[no-any-return] + return getattr(item, "album_art_uri", None) return get_browse_image_url( media_content_type, @@ -64,19 +69,19 @@ def get_thumbnail_url_full( ) -def media_source_filter(item: BrowseMedia): +def media_source_filter(item: BrowseMedia) -> bool: """Filter media sources.""" return item.media_content_type.startswith("audio/") async def async_browse_media( - hass, + hass: HomeAssistant, speaker: SonosSpeaker, media: SonosMedia, get_browse_image_url: GetBrowseImageUrlType, media_content_id: str | None, media_content_type: str | None, -): +) -> BrowseMedia: """Browse media.""" if media_content_id is None: @@ -86,6 +91,7 @@ async def async_browse_media( media, get_browse_image_url, ) + assert media_content_type is not None if media_source.is_media_source_id(media_content_id): return await media_source.async_browse_media( @@ -150,7 +156,9 @@ async def async_browse_media( return response -def build_item_response(media_library, payload, get_thumbnail_url=None): +def build_item_response( + media_library: MusicLibrary, payload: dict[str, str], get_thumbnail_url=None +) -> BrowseMedia | None: """Create response payload for the provided media query.""" if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith( ("A:GENRE", "A:COMPOSER") @@ -166,7 +174,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): "Unknown media type received when building item response: %s", payload["search_type"], ) - return + return None media = media_library.browse_by_idstring( search_type, @@ -176,7 +184,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): ) if media is None: - return + return None thumbnail = None title = None @@ -222,7 +230,7 @@ def build_item_response(media_library, payload, get_thumbnail_url=None): ) -def item_payload(item, get_thumbnail_url=None): +def item_payload(item: DidlObject, get_thumbnail_url=None) -> BrowseMedia: """ Create response payload for a single media item. @@ -256,9 +264,9 @@ async def root_payload( speaker: SonosSpeaker, media: SonosMedia, get_browse_image_url: GetBrowseImageUrlType, -): +) -> BrowseMedia: """Return root payload for Sonos.""" - children = [] + children: list[BrowseMedia] = [] if speaker.favorites: children.append( @@ -303,14 +311,15 @@ async def root_payload( if "spotify" in hass.config.components: result = await spotify.async_browse_media(hass, None, None) - children.extend(result.children) + if result.children: + children.extend(result.children) try: item = await media_source.async_browse_media( hass, None, content_filter=media_source_filter ) # If domain is None, it's overview of available sources - if item.domain is None: + if item.domain is None and item.children is not None: children.extend(item.children) else: children.append(item) @@ -338,7 +347,7 @@ async def root_payload( ) -def library_payload(media_library, get_thumbnail_url=None): +def library_payload(media_library: MusicLibrary, get_thumbnail_url=None) -> BrowseMedia: """ Create response payload to describe contents of a specific library. @@ -360,7 +369,7 @@ def library_payload(media_library, get_thumbnail_url=None): ) -def favorites_payload(favorites): +def favorites_payload(favorites: list[DidlFavorite]) -> BrowseMedia: """ Create response payload to describe contents of a specific library. @@ -398,7 +407,9 @@ def favorites_payload(favorites): ) -def favorites_folder_payload(favorites, media_content_id): +def favorites_folder_payload( + favorites: list[DidlFavorite], media_content_id: str +) -> BrowseMedia: """Create response payload to describe all items of a type of favorite. Used by async_browse_media. @@ -432,7 +443,7 @@ def favorites_folder_payload(favorites, media_content_id): ) -def get_media_type(item): +def get_media_type(item: DidlObject) -> str: """Extract media type of item.""" if item.item_class == "object.item.audioItem.musicTrack": return SONOS_TRACKS @@ -450,7 +461,7 @@ def get_media_type(item): return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class) -def can_play(item): +def can_play(item: DidlObject) -> bool: """ Test if playable. @@ -459,7 +470,7 @@ def can_play(item): return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES -def can_expand(item): +def can_expand(item: DidlObject) -> bool: """ Test if expandable. @@ -474,14 +485,16 @@ def can_expand(item): return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES -def get_content_id(item): +def get_content_id(item: DidlObject) -> str: """Extract content id or uri.""" if item.item_class == "object.item.audioItem.musicTrack": - return item.get_uri() - return item.item_id + return cast(str, item.get_uri()) + return cast(str, item.item_id) -def get_media(media_library, item_id, search_type): +def get_media( + media_library: MusicLibrary, item_id: str, search_type: str +) -> MusicServiceItem: """Fetch media/album.""" search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 1f57cafbf09..14e0693f55a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -130,11 +130,11 @@ async def async_setup_entry( if service_call.service == SERVICE_SNAPSHOT: await SonosSpeaker.snapshot_multi( - hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + hass, speakers, service_call.data[ATTR_WITH_GROUP] ) elif service_call.service == SERVICE_RESTORE: await SonosSpeaker.restore_multi( - hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] + hass, speakers, service_call.data[ATTR_WITH_GROUP] ) config_entry.async_on_unload( @@ -153,7 +153,7 @@ async def async_setup_entry( SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_SET_TIMER, { vol.Required(ATTR_SLEEP_TIME): vol.All( @@ -163,9 +163,9 @@ async def async_setup_entry( "set_sleep_timer", ) - platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") # type: ignore + platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_UPDATE_ALARM, { vol.Required(ATTR_ALARM_ID): cv.positive_int, @@ -177,13 +177,13 @@ async def async_setup_entry( "set_alarm", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_PLAY_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "play_queue", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_REMOVE_FROM_QUEUE, {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, "remove_from_queue", @@ -239,8 +239,8 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return if the media_player is available.""" return ( self.speaker.available - and self.speaker.sonos_group_entities - and self.media.playback_status + and bool(self.speaker.sonos_group_entities) + and self.media.playback_status is not None ) @property @@ -257,7 +257,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return a hash of self.""" return hash(self.unique_id) - @property # type: ignore[misc] + @property def state(self) -> str: """Return the state of the entity.""" if self.media.playback_status in ( @@ -300,13 +300,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return true if volume is muted.""" return self.speaker.muted - @property # type: ignore[misc] - def shuffle(self) -> str | None: + @property + def shuffle(self) -> bool | None: """Shuffling state.""" - shuffle: str = PLAY_MODES[self.media.play_mode][0] - return shuffle + return PLAY_MODES[self.media.play_mode][0] - @property # type: ignore[misc] + @property def repeat(self) -> str | None: """Return current repeat mode.""" sonos_repeat = PLAY_MODES[self.media.play_mode][1] @@ -317,32 +316,32 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return the SonosMedia object from the coordinator speaker.""" return self.coordinator.media - @property # type: ignore[misc] + @property def media_content_id(self) -> str | None: """Content id of current playing media.""" return self.media.uri - @property # type: ignore[misc] - def media_duration(self) -> float | None: + @property + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - return self.media.duration + return int(self.media.duration) if self.media.duration else None - @property # type: ignore[misc] - def media_position(self) -> float | None: + @property + def media_position(self) -> int | None: """Position of current playing media in seconds.""" - return self.media.position + return int(self.media.position) if self.media.position else None - @property # type: ignore[misc] + @property def media_position_updated_at(self) -> datetime.datetime | None: """When was the position of the current playing media valid.""" return self.media.position_updated_at - @property # type: ignore[misc] + @property def media_image_url(self) -> str | None: """Image url of current playing media.""" return self.media.image_url or None - @property # type: ignore[misc] + @property def media_channel(self) -> str | None: """Channel currently playing.""" return self.media.channel or None @@ -352,22 +351,22 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Title of playlist currently playing.""" return self.media.playlist_name - @property # type: ignore[misc] + @property def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" return self.media.artist or None - @property # type: ignore[misc] + @property def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" return self.media.album_name or None - @property # type: ignore[misc] + @property def media_title(self) -> str | None: """Title of current playing media.""" return self.media.title or None - @property # type: ignore[misc] + @property def source(self) -> str | None: """Name of the current input source.""" return self.media.source_name or None @@ -383,12 +382,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.soco.volume -= VOLUME_INCREMENT @soco_error() - def set_volume_level(self, volume: str) -> None: + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.soco.volume = str(int(volume * 100)) @soco_error(UPNP_ERRORS_TO_IGNORE) - def set_shuffle(self, shuffle: str) -> None: + def set_shuffle(self, shuffle: bool) -> None: """Enable/Disable shuffle mode.""" sonos_shuffle = shuffle sonos_repeat = PLAY_MODES[self.media.play_mode][1] @@ -486,7 +485,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.coordinator.soco.previous() @soco_error(UPNP_ERRORS_TO_IGNORE) - def media_seek(self, position: str) -> None: + def media_seek(self, position: float) -> None: """Send seek command.""" self.coordinator.soco.seek(str(datetime.timedelta(seconds=int(position)))) @@ -606,7 +605,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): - item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] + item = media_browser.get_media(self.media.library, media_id, media_type) soco.play_uri(item.get_uri()) return try: @@ -619,7 +618,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.add_to_queue(playlist) soco.play_from_queue(0) elif media_type in PLAYABLE_MEDIA_TYPES: - item = media_browser.get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] + item = media_browser.get_media(self.media.library, media_id, media_type) if not item: _LOGGER.error('Could not find "%s" in the library', media_id) @@ -649,7 +648,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): include_linked_zones: bool | None = None, ) -> None: """Set the alarm clock on the player.""" - alarm = None + alarm: alarms.Alarm | None = None for one_alarm in alarms.get_alarms(self.coordinator.soco): if one_alarm.alarm_id == str(alarm_id): alarm = one_alarm @@ -710,8 +709,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): MEDIA_TYPES_TO_SONOS[media_content_type], ) if image_url := getattr(item, "album_art_uri", None): - result = await self._async_fetch_image(image_url) # type: ignore[no-untyped-call] - return result # type: ignore + return await self._async_fetch_image(image_url) return (None, None) @@ -728,7 +726,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_content_type, ) - async def async_join_players(self, group_members): + async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" speakers = [] for entity_id in group_members: @@ -739,7 +737,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): await SonosSpeaker.join_multi(self.hass, self.speaker, speakers) - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Remove this player from any group. Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to allow use of SonosSpeaker.unjoin_multi() diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index ccbcbc3c339..7a6edb0d293 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry @@ -24,6 +25,8 @@ LEVEL_TYPES = { "music_surround_level": (-15, 15), } +SocoFeatures = list[tuple[str, tuple[int, int]]] + _LOGGER = logging.getLogger(__name__) @@ -34,8 +37,8 @@ async def async_setup_entry( ) -> None: """Set up the Sonos number platform from a config entry.""" - def available_soco_attributes(speaker: SonosSpeaker) -> list[str]: - features = [] + def available_soco_attributes(speaker: SonosSpeaker) -> SocoFeatures: + features: SocoFeatures = [] for level_type, valid_range in LEVEL_TYPES.items(): if (state := getattr(speaker.soco, level_type, None)) is not None: setattr(speaker, level_type, state) @@ -67,7 +70,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int] + self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int, int] ) -> None: """Initialize the level entity.""" super().__init__(speaker) @@ -94,4 +97,4 @@ class SonosLevelEntity(SonosEntity, NumberEntity): @property def native_value(self) -> float: """Return the current value.""" - return getattr(self.speaker, self.level_type) + return cast(float, getattr(self.speaker, self.level_type)) diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 8477e523a40..d1705fb030d 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -100,7 +100,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): @property def available(self) -> bool: """Return whether this device is available.""" - return self.speaker.available and self.speaker.power_source + return self.speaker.available and self.speaker.power_source is not None class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 0c5bec06dfb..516a431295a 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -8,7 +8,7 @@ import datetime from functools import partial import logging import time -from typing import Any +from typing import Any, cast import async_timeout import defusedxml.ElementTree as ET @@ -97,17 +97,17 @@ class SonosSpeaker: self.media = SonosMedia(hass, soco) self._plex_plugin: PlexPlugin | None = None self._share_link_plugin: ShareLinkPlugin | None = None - self.available = True + self.available: bool = True # Device information - self.hardware_version = speaker_info["hardware_version"] - self.software_version = speaker_info["software_version"] - self.mac_address = speaker_info["mac_address"] - self.model_name = speaker_info["model_name"] - self.model_number = speaker_info["model_number"] - self.uid = speaker_info["uid"] - self.version = speaker_info["display_version"] - self.zone_name = speaker_info["zone_name"] + self.hardware_version: str = speaker_info["hardware_version"] + self.software_version: str = speaker_info["software_version"] + self.mac_address: str = speaker_info["mac_address"] + self.model_name: str = speaker_info["model_name"] + self.model_number: str = speaker_info["model_number"] + self.uid: str = speaker_info["uid"] + self.version: str = speaker_info["display_version"] + self.zone_name: str = speaker_info["zone_name"] # Subscriptions and events self.subscriptions_failed: bool = False @@ -160,12 +160,12 @@ class SonosSpeaker: self.sonos_group: list[SonosSpeaker] = [self] self.sonos_group_entities: list[str] = [] self.soco_snapshot: Snapshot | None = None - self.snapshot_group: list[SonosSpeaker] | None = None + self.snapshot_group: list[SonosSpeaker] = [] self._group_members_missing: set[str] = set() async def async_setup_dispatchers(self, entry: ConfigEntry) -> None: """Connect dispatchers in async context during setup.""" - dispatch_pairs = ( + dispatch_pairs: tuple[tuple[str, Callable[..., Any]], ...] = ( (SONOS_CHECK_ACTIVITY, self.async_check_activity), (SONOS_SPEAKER_ADDED, self.update_group_for_uid), (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), @@ -283,18 +283,17 @@ class SonosSpeaker: return self._share_link_plugin @property - def subscription_address(self) -> str | None: - """Return the current subscription callback address if any.""" - if self._subscriptions: - addr, port = self._subscriptions[0].event_listener.address - return ":".join([addr, str(port)]) - return None + def subscription_address(self) -> str: + """Return the current subscription callback address.""" + assert len(self._subscriptions) > 0 + addr, port = self._subscriptions[0].event_listener.address + return ":".join([addr, str(port)]) # # Subscription handling and event dispatchers # def log_subscription_result( - self, result: Any, event: str, level: str = logging.DEBUG + self, result: Any, event: str, level: int = logging.DEBUG ) -> None: """Log a message if a subscription action (create/renew/stop) results in an exception.""" if not isinstance(result, Exception): @@ -304,7 +303,7 @@ class SonosSpeaker: message = "Request timed out" exc_info = None else: - message = result + message = str(result) exc_info = result if not str(result) else None _LOGGER.log( @@ -554,7 +553,7 @@ class SonosSpeaker: ) @callback - def speaker_activity(self, source): + def speaker_activity(self, source: str) -> None: """Track the last activity on this speaker, set availability and resubscribe.""" if self._resub_cooldown_expires_at: if time.monotonic() < self._resub_cooldown_expires_at: @@ -593,6 +592,7 @@ class SonosSpeaker: async def async_offline(self) -> None: """Handle removal of speaker when unavailable.""" + assert self._subscription_lock is not None async with self._subscription_lock: await self._async_offline() @@ -826,8 +826,8 @@ class SonosSpeaker: if speaker: self._group_members_missing.discard(uid) sonos_group.append(speaker) - entity_id = entity_registry.async_get_entity_id( - MP_DOMAIN, DOMAIN, uid + entity_id = cast( + str, entity_registry.async_get_entity_id(MP_DOMAIN, DOMAIN, uid) ) sonos_group_entities.append(entity_id) else: @@ -850,7 +850,9 @@ class SonosSpeaker: self.async_write_entity_states() for joined_uid in group[1:]: - joined_speaker = self.hass.data[DATA_SONOS].discovered.get(joined_uid) + joined_speaker: SonosSpeaker = self.hass.data[ + DATA_SONOS + ].discovered.get(joined_uid) if joined_speaker: joined_speaker.coordinator = self joined_speaker.sonos_group = sonos_group @@ -936,7 +938,7 @@ class SonosSpeaker: if with_group: self.snapshot_group = self.sonos_group.copy() else: - self.snapshot_group = None + self.snapshot_group = [] @staticmethod async def snapshot_multi( @@ -969,7 +971,7 @@ class SonosSpeaker: _LOGGER.warning("Error on restore %s: %s", self.zone_name, ex) self.soco_snapshot = None - self.snapshot_group = None + self.snapshot_group = [] @staticmethod async def restore_multi( @@ -996,7 +998,7 @@ class SonosSpeaker: exc_info=exc, ) - groups = [] + groups: list[list[SonosSpeaker]] = [] if not with_group: return groups @@ -1022,7 +1024,7 @@ class SonosSpeaker: # Bring back the original group topology for speaker in (s for s in speakers if s.snapshot_group): - assert speaker.snapshot_group is not None + assert len(speaker.snapshot_group) if speaker.snapshot_group[0] == speaker: if speaker.snapshot_group not in (speaker.sonos_group, [speaker]): speaker.join(speaker.snapshot_group) @@ -1047,7 +1049,7 @@ class SonosSpeaker: if with_group: for speaker in [s for s in speakers_set if s.snapshot_group]: - assert speaker.snapshot_group is not None + assert len(speaker.snapshot_group) speakers_set.update(speaker.snapshot_group) async with hass.data[DATA_SONOS].topology_condition: diff --git a/homeassistant/components/sonos/statistics.py b/homeassistant/components/sonos/statistics.py index a850e5a8caf..b761469aea5 100644 --- a/homeassistant/components/sonos/statistics.py +++ b/homeassistant/components/sonos/statistics.py @@ -14,7 +14,7 @@ class SonosStatistics: def __init__(self, zone_name: str, kind: str) -> None: """Initialize SonosStatistics.""" - self._stats = {} + self._stats: dict[str, dict[str, int | float]] = {} self._stat_type = kind self.zone_name = zone_name diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index a348b40cb0f..acf33ea34aa 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -3,8 +3,9 @@ from __future__ import annotations import datetime import logging -from typing import Any +from typing import Any, cast +from soco.alarms import Alarm from soco.exceptions import SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity @@ -183,14 +184,14 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): def is_on(self) -> bool: """Return True if entity is on.""" if self.needs_coordinator and not self.speaker.is_coordinator: - return getattr(self.speaker.coordinator, self.feature_type) - return getattr(self.speaker, self.feature_type) + return cast(bool, getattr(self.speaker.coordinator, self.feature_type)) + return cast(bool, getattr(self.speaker, self.feature_type)) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self.send_command(True) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self.send_command(False) @@ -233,7 +234,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): ) @property - def alarm(self): + def alarm(self) -> Alarm: """Return the alarm instance.""" return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id) @@ -247,7 +248,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll() @callback - def async_check_if_available(self): + def async_check_if_available(self) -> bool: """Check if alarm exists and remove alarm entity if not available.""" if self.alarm: return True @@ -279,7 +280,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): self.async_write_ha_state() @callback - def _async_update_device(self): + def _async_update_device(self) -> None: """Update the device, since this alarm moved to a different player.""" device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) @@ -288,22 +289,20 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): if entity is None: raise RuntimeError("Alarm has been deleted by accident.") - entry_id = entity.config_entry_id - new_device = device_registry.async_get_or_create( - config_entry_id=entry_id, + config_entry_id=cast(str, entity.config_entry_id), identifiers={(SONOS_DOMAIN, self.soco.uid)}, connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, ) - if not entity_registry.async_get(self.entity_id).device_id == new_device.id: + if ( + device := entity_registry.async_get(self.entity_id) + ) and device.device_id != new_device.id: _LOGGER.debug("%s is moving to %s", self.entity_id, new_device.name) - # pylint: disable=protected-access - entity_registry._async_update_entity( - self.entity_id, device_id=new_device.id - ) + entity_registry.async_update_entity(self.entity_id, device_id=new_device.id) @property - def _is_today(self): + def _is_today(self) -> bool: + """Return whether this alarm is scheduled for today.""" recurrence = self.alarm.recurrence timestr = int(datetime.datetime.today().strftime("%w")) return ( @@ -321,12 +320,12 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): return (self.alarm is not None) and self.speaker.available @property - def is_on(self): + def is_on(self) -> bool: """Return state of Sonos alarm switch.""" return self.alarm.enabled @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return attributes of Sonos alarm switch.""" return { ATTR_ID: str(self.alarm_id), diff --git a/mypy.ini b/mypy.ini index d6665cb40c8..957da7254eb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2589,39 +2589,3 @@ disallow_untyped_decorators = false disallow_untyped_defs = false warn_return_any = false warn_unreachable = false - -[mypy-homeassistant.components.sonos] -ignore_errors = true - -[mypy-homeassistant.components.sonos.alarms] -ignore_errors = true - -[mypy-homeassistant.components.sonos.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.sonos.diagnostics] -ignore_errors = true - -[mypy-homeassistant.components.sonos.entity] -ignore_errors = true - -[mypy-homeassistant.components.sonos.favorites] -ignore_errors = true - -[mypy-homeassistant.components.sonos.media_browser] -ignore_errors = true - -[mypy-homeassistant.components.sonos.media_player] -ignore_errors = true - -[mypy-homeassistant.components.sonos.number] -ignore_errors = true - -[mypy-homeassistant.components.sonos.sensor] -ignore_errors = true - -[mypy-homeassistant.components.sonos.speaker] -ignore_errors = true - -[mypy-homeassistant.components.sonos.statistics] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b6c31751e12..0c598df9cd1 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -15,20 +15,7 @@ from .model import Config, Integration # If you are an author of component listed here, please fix these errors and # remove your component from this list to enable type checks. # Do your best to not add anything new here. -IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.sonos", - "homeassistant.components.sonos.alarms", - "homeassistant.components.sonos.binary_sensor", - "homeassistant.components.sonos.diagnostics", - "homeassistant.components.sonos.entity", - "homeassistant.components.sonos.favorites", - "homeassistant.components.sonos.media_browser", - "homeassistant.components.sonos.media_player", - "homeassistant.components.sonos.number", - "homeassistant.components.sonos.sensor", - "homeassistant.components.sonos.speaker", - "homeassistant.components.sonos.statistics", -] +IGNORED_MODULES: Final[list[str]] = [] # Component modules which should set no_implicit_reexport = true. NO_IMPLICIT_REEXPORT_MODULES: set[str] = { From e07554dc25f89134f66e2917a1be83a54538a6d2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 4 Sep 2022 21:43:38 +0200 Subject: [PATCH 049/231] Bump yale_smart_alarm_client to 0.3.9 (#77797) --- homeassistant/components/yale_smart_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 0b1a5a94da0..865751d18e0 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "yale_smart_alarm", "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": ["yalesmartalarmclient==0.3.8"], + "requirements": ["yalesmartalarmclient==0.3.9"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 609b032cfe3..fd169055887 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2539,7 +2539,7 @@ xmltodict==0.13.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.8 +yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble yalexs-ble==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d12689484d6..7069c0fb3fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1743,7 +1743,7 @@ xknx==1.0.2 xmltodict==0.13.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.3.8 +yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble yalexs-ble==1.6.4 From 1231ba4d03292b3ada972608f6cf301a7fcb55fc Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 5 Sep 2022 13:52:50 +0200 Subject: [PATCH 050/231] Rename BThome to BTHome (#77807) Co-authored-by: Paulus Schoutsen Co-authored-by: J. Nick Koston --- homeassistant/components/bthome/__init__.py | 10 +++++----- homeassistant/components/bthome/config_flow.py | 10 +++++----- homeassistant/components/bthome/const.py | 2 +- homeassistant/components/bthome/device.py | 2 +- homeassistant/components/bthome/manifest.json | 4 ++-- homeassistant/components/bthome/sensor.py | 10 +++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/__init__.py | 2 +- tests/components/bthome/test_config_flow.py | 6 +++--- tests/components/bthome/test_sensor.py | 2 +- 11 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 4cc3b5cf4da..93ebd7b288f 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -1,9 +1,9 @@ -"""The BThome Bluetooth integration.""" +"""The BTHome Bluetooth integration.""" from __future__ import annotations import logging -from bthome_ble import BThomeBluetoothDeviceData, SensorUpdate +from bthome_ble import BTHomeBluetoothDeviceData, SensorUpdate from bthome_ble.parser import EncryptionScheme from homeassistant.components.bluetooth import ( @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) def process_service_info( hass: HomeAssistant, entry: ConfigEntry, - data: BThomeBluetoothDeviceData, + data: BTHomeBluetoothDeviceData, service_info: BluetoothServiceInfoBleak, ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" @@ -40,14 +40,14 @@ def process_service_info( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up BThome Bluetooth from a config entry.""" + """Set up BTHome Bluetooth from a config entry.""" address = entry.unique_id assert address is not None kwargs = {} if bindkey := entry.data.get("bindkey"): kwargs["bindkey"] = bytes.fromhex(bindkey) - data = BThomeBluetoothDeviceData(**kwargs) + data = BTHomeBluetoothDeviceData(**kwargs) coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id diff --git a/homeassistant/components/bthome/config_flow.py b/homeassistant/components/bthome/config_flow.py index e8e49cab566..6514f2c5396 100644 --- a/homeassistant/components/bthome/config_flow.py +++ b/homeassistant/components/bthome/config_flow.py @@ -1,11 +1,11 @@ -"""Config flow for BThome Bluetooth integration.""" +"""Config flow for BTHome Bluetooth integration.""" from __future__ import annotations from collections.abc import Mapping import dataclasses from typing import Any -from bthome_ble import BThomeBluetoothDeviceData as DeviceData +from bthome_ble import BTHomeBluetoothDeviceData as DeviceData from bthome_ble.parser import EncryptionScheme import voluptuous as vol @@ -34,8 +34,8 @@ def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name -class BThomeConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for BThome Bluetooth.""" +class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for BTHome Bluetooth.""" VERSION = 1 @@ -68,7 +68,7 @@ class BThomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_encryption_key( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Enter a bindkey for an encrypted BThome device.""" + """Enter a bindkey for an encrypted BTHome device.""" assert self._discovery_info assert self._discovered_device diff --git a/homeassistant/components/bthome/const.py b/homeassistant/components/bthome/const.py index e397e288071..e46aa50e148 100644 --- a/homeassistant/components/bthome/const.py +++ b/homeassistant/components/bthome/const.py @@ -1,3 +1,3 @@ -"""Constants for the BThome Bluetooth integration.""" +"""Constants for the BTHome Bluetooth integration.""" DOMAIN = "bthome" diff --git a/homeassistant/components/bthome/device.py b/homeassistant/components/bthome/device.py index f16b2f49998..bd011752db1 100644 --- a/homeassistant/components/bthome/device.py +++ b/homeassistant/components/bthome/device.py @@ -1,4 +1,4 @@ -"""Support for BThome Bluetooth devices.""" +"""Support for BTHome Bluetooth devices.""" from __future__ import annotations from bthome_ble import DeviceKey, SensorDeviceInfo diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index bdb4b75bfa9..597d52c72e4 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -1,6 +1,6 @@ { "domain": "bthome", - "name": "BThome", + "name": "BTHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bthome", "bluetooth": [ @@ -13,7 +13,7 @@ "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==0.5.2"], + "requirements": ["bthome-ble==1.0.0"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 71601fa24c0..a0068596b01 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -1,4 +1,4 @@ -"""Support for BThome sensors.""" +"""Support for BTHome sensors.""" from __future__ import annotations from typing import Optional, Union @@ -202,26 +202,26 @@ async def async_setup_entry( entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BThome BLE sensors.""" + """Set up the BTHome 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( - BThomeBluetoothSensorEntity, async_add_entities + BTHomeBluetoothSensorEntity, async_add_entities ) ) entry.async_on_unload(coordinator.async_register_processor(processor)) -class BThomeBluetoothSensorEntity( +class BTHomeBluetoothSensorEntity( PassiveBluetoothProcessorEntity[ PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): - """Representation of a BThome BLE sensor.""" + """Representation of a BTHome BLE sensor.""" @property def native_value(self) -> int | float | None: diff --git a/requirements_all.txt b/requirements_all.txt index fd169055887..6a44809cd96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ bsblan==0.5.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==0.5.2 +bthome-ble==1.0.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7069c0fb3fe..30a8fe4ccdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ brunt==1.2.0 bsblan==0.5.0 # homeassistant.components.bthome -bthome-ble==0.5.2 +bthome-ble==1.0.0 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index be59cd7e8cb..e480c0a3810 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the BThome integration.""" +"""Tests for the BTHome integration.""" from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index fd8f8dfaa35..64a298e3460 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -1,8 +1,8 @@ -"""Test the BThome config flow.""" +"""Test the BTHome config flow.""" from unittest.mock import patch -from bthome_ble import BThomeBluetoothDeviceData as DeviceData +from bthome_ble import BTHomeBluetoothDeviceData as DeviceData from homeassistant import config_entries from homeassistant.components.bluetooth import BluetoothChange @@ -167,7 +167,7 @@ async def test_async_step_user_no_devices_found_2(hass): """ Test setup from service info cache with no devices found. - This variant tests with a non-BThome device known to us. + This variant tests with a non-BTHome device known to us. """ with patch( "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index f73d3bf379c..bb0c5b3f459 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1,4 +1,4 @@ -"""Test the BThome sensors.""" +"""Test the BTHome sensors.""" from unittest.mock import patch From f3e811417f25aa40c6c18a20af9b0b1ba0088dbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Sep 2022 20:57:40 -0400 Subject: [PATCH 051/231] Prefilter noisy apple devices from bluetooth (#77808) --- homeassistant/components/bluetooth/manager.py | 18 ++- tests/components/bluetooth/test_init.py | 128 ++++++++++++------ 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index d274939c610..9fc00aa159b 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -54,6 +54,10 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" +APPLE_MFR_ID: Final = 76 +APPLE_HOMEKIT_START_BYTE: Final = 0x06 # homekit_controller +APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker +APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE} RSSI_SWITCH_THRESHOLD = 6 @@ -290,6 +294,19 @@ class BluetoothManager: 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 + # traffic on a typical network. + advertisement_data = service_info.advertisement + manufacturer_data = advertisement_data.manufacturer_data + if ( + len(manufacturer_data) == 1 + and (apple_data := manufacturer_data.get(APPLE_MFR_ID)) + and apple_data[0] not in APPLE_START_BYTES_WANTED + and not advertisement_data.service_data + ): + return + device = service_info.device connectable = service_info.connectable address = device.address @@ -299,7 +316,6 @@ class BluetoothManager: return self._history[address] = service_info - advertisement_data = service_info.advertisement source = service_info.source if connectable: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ade68fdb94d..e4b84b943b4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1291,16 +1291,16 @@ async def test_register_callback_by_manufacturer_id( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76}, + {MANUFACTURER_ID: 21}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, apple_device, apple_adv) @@ -1316,9 +1316,59 @@ async def test_register_callback_by_manufacturer_id( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 + + +async def test_filtering_noisy_apple_devices( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test filtering noisy apple devices.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {MANUFACTURER_ID: 21}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") + apple_adv = AdvertisementData( + local_name="noisy", + manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 0 async def test_register_callback_by_address_connectable_manufacturer_id( @@ -1346,21 +1396,21 @@ async def test_register_callback_by_address_connectable_manufacturer_id( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"}, + {MANUFACTURER_ID: 21, CONNECTABLE: False, ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, apple_device, apple_adv) - apple_device_wrong_address = BLEDevice("44:44:33:11:23:46", "apple") + apple_device_wrong_address = BLEDevice("44:44:33:11:23:46", "rtx") inject_advertisement(hass, apple_device_wrong_address, apple_adv) await hass.async_block_till_done() @@ -1370,9 +1420,9 @@ async def test_register_callback_by_address_connectable_manufacturer_id( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_manufacturer_id_and_address( @@ -1400,19 +1450,19 @@ async def test_register_callback_by_manufacturer_id_and_address( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {MANUFACTURER_ID: 76, ADDRESS: "44:44:33:11:23:45"}, + {MANUFACTURER_ID: 21, ADDRESS: "44:44:33:11:23:45"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device, apple_adv) + inject_advertisement(hass, rtx_device, rtx_adv) yale_device = BLEDevice("44:44:33:11:23:45", "apple") yale_adv = AdvertisementData( @@ -1426,7 +1476,7 @@ async def test_register_callback_by_manufacturer_id_and_address( other_apple_device = BLEDevice("44:44:33:11:23:22", "apple") other_apple_adv = AdvertisementData( local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) inject_advertisement(hass, other_apple_device, other_apple_adv) @@ -1435,9 +1485,9 @@ async def test_register_callback_by_manufacturer_id_and_address( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_service_uuid_and_address( @@ -1603,31 +1653,31 @@ async def test_register_callback_by_local_name( cancel = bluetooth.async_register_callback( hass, _fake_subscriber, - {LOCAL_NAME: "apple"}, + {LOCAL_NAME: "rtx"}, BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 - apple_device = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv = AdvertisementData( - local_name="apple", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device, apple_adv) + inject_advertisement(hass, rtx_device, rtx_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") inject_advertisement(hass, empty_device, empty_adv) - apple_device_2 = BLEDevice("44:44:33:11:23:45", "apple") - apple_adv_2 = AdvertisementData( - local_name="apple2", - manufacturer_data={76: b"\xd8.\xad\xcd\r\x85"}, + rtx_device_2 = BLEDevice("44:44:33:11:23:45", "rtx") + rtx_adv_2 = AdvertisementData( + local_name="rtx2", + manufacturer_data={21: b"\xd8.\xad\xcd\r\x85"}, ) - inject_advertisement(hass, apple_device_2, apple_adv_2) + inject_advertisement(hass, rtx_device_2, rtx_adv_2) await hass.async_block_till_done() @@ -1636,9 +1686,9 @@ async def test_register_callback_by_local_name( assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] - assert service_info.name == "apple" - assert service_info.manufacturer == "Apple, Inc." - assert service_info.manufacturer_id == 76 + assert service_info.name == "rtx" + assert service_info.manufacturer == "RTX Telecom A/S" + assert service_info.manufacturer_id == 21 async def test_register_callback_by_local_name_overly_broad( From b1241bf0f2b06645ec31172fddc74cae3b6d778c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 01:45:35 -0500 Subject: [PATCH 052/231] Fix isy994 calling sync api in async context (#77812) --- homeassistant/components/isy994/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 54ee9a2ded5..61f42a60a6e 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -75,7 +75,7 @@ class ISYEntity(Entity): # New state attributes may be available, update the state. self.async_write_ha_state() - self.hass.bus.fire("isy994_control", event_data) + self.hass.bus.async_fire("isy994_control", event_data) @property def device_info(self) -> DeviceInfo | None: From e8ab4eef44a391cc7b7bc41749f0a23662f8f69b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 5 Sep 2022 06:15:14 -0400 Subject: [PATCH 053/231] Fix device info for zwave_js device entities (#77821) --- homeassistant/components/zwave_js/button.py | 10 ++++------ homeassistant/components/zwave_js/helpers.py | 13 +++++++++++++ homeassistant/components/zwave_js/sensor.py | 10 ++++------ homeassistant/components/zwave_js/update.py | 13 +++---------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index cef64f1724a..1d97ed05da5 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -9,11 +9,11 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DOMAIN, LOGGER -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 0 @@ -58,10 +58,8 @@ class ZWaveNodePingButton(ButtonEntity): self._attr_name = f"{name}: Ping" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.ping" - # device is precreated in main handler - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, node)}, - ) + # device may not be precreated in main handler yet + self._attr_device_info = get_device_info(driver, node) async def async_poll_value(self, _: bool) -> None: """Poll a value.""" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index c047a3a9903..6175b7db353 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -30,6 +30,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from .const import ( @@ -413,3 +414,15 @@ def get_value_state_schema( vol.Coerce(int), vol.Range(min=value.metadata.min, max=value.metadata.max), ) + + +def get_device_info(driver: Driver, node: ZwaveNode) -> DeviceInfo: + """Get DeviceInfo for node.""" + return DeviceInfo( + identifiers={get_device_id(driver, node)}, + sw_version=node.firmware_version, + name=node.name or node.device_config.description or f"Node {node.node_id}", + model=node.device_config.label, + manufacturer=node.device_config.manufacturer, + suggested_area=node.location if node.location else None, + ) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 22fbfdab728..75d8066d595 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -63,7 +63,7 @@ from .discovery_data_template import ( NumericSensorDataTemplateData, ) from .entity import ZWaveBaseEntity -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 0 @@ -493,10 +493,8 @@ class ZWaveNodeStatusSensor(SensorEntity): self._attr_name = f"{name}: Node Status" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.node_status" - # device is precreated in main handler - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, self.node)}, - ) + # device may not be precreated in main handler yet + self._attr_device_info = get_device_info(driver, node) self._attr_native_value: str = node.status.name.lower() async def async_poll_value(self, _: bool) -> None: diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 1f04c3acc47..7f25788e0be 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -19,11 +19,11 @@ 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 DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER -from .helpers import get_device_id, get_valueless_base_unique_id +from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 1 SCAN_INTERVAL = timedelta(days=1) @@ -75,14 +75,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" # device may not be precreated in main handler yet - self._attr_device_info = DeviceInfo( - identifiers={get_device_id(driver, node)}, - sw_version=node.firmware_version, - name=node.name or node.device_config.description or f"Node {node.node_id}", - model=node.device_config.label, - manufacturer=node.device_config.manufacturer, - suggested_area=node.location if node.location else None, - ) + self._attr_device_info = get_device_info(driver, node) self._attr_installed_version = self._attr_latest_version = node.firmware_version From ad8cd9c95798ed0d9672335936d8956de39bc277 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Mon, 5 Sep 2022 17:04:33 +0300 Subject: [PATCH 054/231] Bump pybravia to 0.2.1 (#77832) --- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 8a18cac5a99..fa172957781 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.0"], + "requirements": ["pybravia==0.2.1"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6a44809cd96..db3390b3eb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.0 +pybravia==0.2.1 # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30a8fe4ccdd..b209c9b163d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1019,7 +1019,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.0 +pybravia==0.2.1 # homeassistant.components.cloudflare pycfdns==1.2.2 From 605e350159228ab068416d86c63f2fc09cad7dbd Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 5 Sep 2022 09:20:37 -0400 Subject: [PATCH 055/231] Add remoteAdminPasswordEnd to redacted keys in fully_kiosk diagnostics (#77837) Add remoteAdminPasswordEnd to redacted keys in diagnostics --- homeassistant/components/fully_kiosk/diagnostics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fully_kiosk/diagnostics.py b/homeassistant/components/fully_kiosk/diagnostics.py index 89a894d5353..121621186cd 100644 --- a/homeassistant/components/fully_kiosk/diagnostics.py +++ b/homeassistant/components/fully_kiosk/diagnostics.py @@ -51,6 +51,7 @@ SETTINGS_TO_REDACT = { "sebExamKey", "sebConfigKey", "kioskPinEnc", + "remoteAdminPasswordEnc", } From b0ff4fc057229552e180535f824ea67f62d76f72 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 5 Sep 2022 15:33:10 +0100 Subject: [PATCH 056/231] Less verbose error logs for bleak connection errors in ActiveBluetoothProcessorCoordinator (#77839) Co-authored-by: J. Nick Koston --- .../bluetooth/active_update_coordinator.py | 9 +++ .../test_active_update_coordinator.py | 76 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index e73414fe79f..b207f6fa2e1 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -6,6 +6,8 @@ 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 @@ -109,6 +111,13 @@ class ActiveBluetoothProcessorCoordinator( try: update = await self._async_poll_data(self._last_service_info) + except BleakError as exc: + if self.last_poll_successful: + self.logger.error( + "%s: Bluetooth error whilst polling: %s", self.address, str(exc) + ) + self.last_poll_successful = False + return except Exception: # pylint: disable=broad-except if self.last_poll_successful: self.logger.exception("%s: Failure while polling", self.address) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 24ad96c523e..7677584e890 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -5,6 +5,8 @@ import asyncio import logging from unittest.mock import MagicMock, call, patch +from bleak import BleakError + from homeassistant.components.bluetooth import ( DOMAIN, BluetoothChange, @@ -162,6 +164,80 @@ async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start cancel() +async def test_bleak_error_and_recover( + hass: HomeAssistant, mock_bleak_scanner_start, caplog +): + """Test bleak error handling and recovery.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + flag = True + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + nonlocal flag + if flag: + raise BleakError("Connection was aborted") + return {"testdata": flag} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + poll_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=0, + immediate=True, + ), + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll fails + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": None}) + + assert ( + "aa:bb:cc:dd:ee:ff: Bluetooth error whilst polling: Connection was aborted" + in caplog.text + ) + + # Second poll works + flag = False + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": False}) + + cancel() + + async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start): """Test error handling and recovery.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) From 40421b41f7fe07a29ba40c4a3e1cc488dc64dc2a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 Sep 2022 16:18:49 +0200 Subject: [PATCH 057/231] Add the hardware integration to default_config (#77840) --- homeassistant/components/default_config/manifest.json | 3 ++- homeassistant/package_constraints.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 593ac26dbc9..6701e62c71f 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -11,8 +11,9 @@ "dhcp", "energy", "frontend", - "homeassistant_alerts", + "hardware", "history", + "homeassistant_alerts", "input_boolean", "input_button", "input_datetime", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2e41ae13458..8b65b9c0285 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,6 +28,7 @@ orjson==3.7.11 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.3 +psutil-home-assistant==0.0.1 pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 From 4f8421617eb567276dab38968e84db07831c118c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:05:37 -0500 Subject: [PATCH 058/231] Bump led-ble to 0.7.0 (#77845) --- homeassistant/components/led_ble/manifest.json | 5 +++-- homeassistant/generated/bluetooth.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index a0f5e3481d5..273fbfedc04 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.6.0"], + "requirements": ["led-ble==0.7.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ @@ -13,7 +13,8 @@ { "local_name": "Triones*" }, { "local_name": "LEDBlue*" }, { "local_name": "Dream~*" }, - { "local_name": "QHM-*" } + { "local_name": "QHM-*" }, + { "local_name": "AP-*" } ], "iot_class": "local_polling" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 83bfd3ab5eb..d7230213302 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -164,6 +164,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "led_ble", "local_name": "QHM-*" }, + { + "domain": "led_ble", + "local_name": "AP-*" + }, { "domain": "melnor", "manufacturer_data_start": [ diff --git a/requirements_all.txt b/requirements_all.txt index db3390b3eb9..460216f6394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.6.0 +led-ble==0.7.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b209c9b163d..77f28f755e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.6.0 +led-ble==0.7.0 # homeassistant.components.foscam libpyfoscam==1.0 From bca9dc1f6160a915bcb62b96cc53da421cb96fb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:05:53 -0500 Subject: [PATCH 059/231] Bump govee-ble to 0.17.2 (#77849) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/govee_ble/__init__.py | 4 ++-- tests/components/govee_ble/test_config_flow.py | 6 +++--- tests/components/govee_ble/test_sensor.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index e24e3bfea14..2ce68498968 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -53,7 +53,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.1"], + "requirements": ["govee-ble==0.17.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 460216f6394..2976fb6a630 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -772,7 +772,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.1 +govee-ble==0.17.2 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77f28f755e0..008a6b39079 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -573,7 +573,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.1 +govee-ble==0.17.2 # homeassistant.components.gree greeclimate==1.3.0 diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index 3baea5e1140..c440317fa43 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -14,7 +14,7 @@ NOT_GOVEE_SERVICE_INFO = BluetoothServiceInfo( ) GVH5075_SERVICE_INFO = BluetoothServiceInfo( - name="GVH5075_2762", + name="GVH5075 2762", address="61DE521B-F0BF-9F44-64D4-75BBE1738105", rssi=-63, manufacturer_data={ @@ -26,7 +26,7 @@ GVH5075_SERVICE_INFO = BluetoothServiceInfo( ) GVH5177_SERVICE_INFO = BluetoothServiceInfo( - name="GVH5177_2EC8", + name="GVH5177 2EC8", address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", rssi=-56, manufacturer_data={ diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index 188672cdf18..73cbb903f31 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -27,7 +27,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5075_2762" + assert result2["title"] == "H5075 2762" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -73,7 +73,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5177_2EC8" + assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -192,7 +192,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "H5177_2EC8" + assert result2["title"] == "H5177 2EC8" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index da67d32e681..e7828fdc496 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -42,7 +42,7 @@ async def test_sensors(hass): temp_sensor = hass.states.get("sensor.h5075_2762_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "21.34" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075 2762 Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From e8c4711d884a9c65990ee00114ecb5e51d16eb37 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 5 Sep 2022 20:27:48 +0200 Subject: [PATCH 060/231] Update frontend to 20220905.0 (#77854) --- 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 8459d08eab7..416634053d6 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==20220902.0"], + "requirements": ["home-assistant-frontend==20220905.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b65b9c0285..3bf00427954 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2976fb6a630..77cd2faa49e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 008a6b39079..a518ff1d9cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220902.0 +home-assistant-frontend==20220905.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 6c36d5acaaf8dfadb18095f3e053477ed425b759 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 5 Sep 2022 14:28:36 -0400 Subject: [PATCH 061/231] Bumped version to 2022.9.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 7ae1fccf7f1..c4c15302a44 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -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 fc5260f41df..0a8db19d4ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b4" +version = "2022.9.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 74ddc336cad1fdfd5cb59310c528a22fbcb62131 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 6 Sep 2022 17:33:16 +0200 Subject: [PATCH 062/231] Use identifiers host and serial number to match device (#75657) --- homeassistant/components/upnp/__init__.py | 29 ++++++++++------ homeassistant/components/upnp/config_flow.py | 8 +++-- homeassistant/components/upnp/const.py | 5 +-- homeassistant/components/upnp/device.py | 8 ++++- tests/components/upnp/conftest.py | 7 ++-- tests/components/upnp/test_config_flow.py | 36 +++++++++++++++++++- 6 files changed, 75 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index a45e58f28bc..95531450e5a 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -25,15 +25,18 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, DOMAIN, + IDENTIFIER_HOST, + IDENTIFIER_SERIAL_NUMBER, LOGGER, ) -from .device import Device, async_create_device, async_get_mac_address_from_host +from .device import Device, async_create_device NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" @@ -106,24 +109,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device.original_udn = entry.data[CONFIG_ENTRY_ORIGINAL_UDN] # Store mac address for changed UDN matching. - if device.host: - device.mac_address = await async_get_mac_address_from_host(hass, device.host) - if device.mac_address and not entry.data.get("CONFIG_ENTRY_MAC_ADDRESS"): + device_mac_address = await device.async_get_mac_address() + if device_mac_address and not entry.data.get(CONFIG_ENTRY_MAC_ADDRESS): hass.config_entries.async_update_entry( entry=entry, data={ **entry.data, - CONFIG_ENTRY_MAC_ADDRESS: device.mac_address, + CONFIG_ENTRY_MAC_ADDRESS: device_mac_address, + CONFIG_ENTRY_HOST: device.host, }, ) + identifiers = {(DOMAIN, device.usn)} + if device.host: + identifiers.add((IDENTIFIER_HOST, device.host)) + if device.serial_number: + identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number)) + connections = {(dr.CONNECTION_UPNP, device.udn)} - if device.mac_address: - connections.add((dr.CONNECTION_NETWORK_MAC, device.mac_address)) + if device_mac_address: + connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address)) device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device( - identifiers=set(), connections=connections + identifiers=identifiers, connections=connections ) if device_entry: LOGGER.debug( @@ -136,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections=connections, - identifiers={(DOMAIN, device.usn)}, + identifiers=identifiers, name=device.name, manufacturer=device.manufacturer, model=device.model_name, @@ -148,7 +157,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Update identifier. device_entry = device_registry.async_update_device( device_entry.id, - new_identifiers={(DOMAIN, device.usn)}, + new_identifiers=identifiers, ) assert device_entry diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 7d4e768e855..3386cf40711 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -161,22 +162,25 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info) + host = discovery_info.ssdp_headers["_host"] self._abort_if_unique_id_configured( # Store mac address for older entries. # The location is stored in the config entry such that when the location changes, the entry is reloaded. updates={ CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location, + CONFIG_ENTRY_HOST: host, }, ) # Handle devices changing their UDN, only allow a single host. for entry in self._async_current_entries(include_ignore=True): entry_mac_address = entry.data.get(CONFIG_ENTRY_MAC_ADDRESS) - entry_st = entry.data.get(CONFIG_ENTRY_ST) - if entry_mac_address != mac_address: + entry_host = entry.data.get(CONFIG_ENTRY_HOST) + if entry_mac_address != mac_address and entry_host != host: continue + entry_st = entry.data.get(CONFIG_ENTRY_ST) if discovery_info.ssdp_st != entry_st: # Check ssdp_st to prevent swapping between IGDv1 and IGDv2. continue diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index e673922d1c2..023ec82a487 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -6,7 +6,6 @@ from homeassistant.const import TIME_SECONDS LOGGER = logging.getLogger(__package__) -CONF_LOCAL_IP = "local_ip" DOMAIN = "upnp" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" @@ -24,7 +23,9 @@ CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_ORIGINAL_UDN = "original_udn" CONFIG_ENTRY_MAC_ADDRESS = "mac_address" CONFIG_ENTRY_LOCATION = "location" +CONFIG_ENTRY_HOST = "host" +IDENTIFIER_HOST = "upnp_host" +IDENTIFIER_SERIAL_NUMBER = "upnp_serial_number" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() ST_IGD_V1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" ST_IGD_V2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2" -SSDP_SEARCH_TIMEOUT = 4 diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 3a688b8571d..e06ada02b77 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -69,9 +69,15 @@ class Device: self.hass = hass self._igd_device = igd_device self.coordinator: DataUpdateCoordinator | None = None - self.mac_address: str | None = None self.original_udn: str | None = None + async def async_get_mac_address(self) -> str | None: + """Get mac address.""" + if not self.host: + return None + + return await async_get_mac_address_from_host(self.hass, self.host) + @property def udn(self) -> str: """Get the UDN.""" diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index e7cd24d0c7c..b159a371d9a 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -25,7 +25,7 @@ TEST_UDN = "uuid:device" TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" TEST_USN = f"{TEST_UDN}::{TEST_ST}" TEST_LOCATION = "http://192.168.1.1/desc.xml" -TEST_HOSTNAME = urlparse(TEST_LOCATION).hostname +TEST_HOST = urlparse(TEST_LOCATION).hostname TEST_FRIENDLY_NAME = "mock-name" TEST_MAC_ADDRESS = "00:11:22:33:44:55" TEST_DISCOVERY = ssdp.SsdpServiceInfo( @@ -41,10 +41,11 @@ TEST_DISCOVERY = ssdp.SsdpServiceInfo( ssdp.ATTR_UPNP_FRIENDLY_NAME: TEST_FRIENDLY_NAME, ssdp.ATTR_UPNP_MANUFACTURER: "mock-manufacturer", ssdp.ATTR_UPNP_MODEL_NAME: "mock-model-name", + ssdp.ATTR_UPNP_SERIAL: "mock-serial", ssdp.ATTR_UPNP_UDN: TEST_UDN, }, ssdp_headers={ - "_host": TEST_HOSTNAME, + "_host": TEST_HOST, }, ) @@ -54,8 +55,10 @@ def mock_igd_device() -> IgdDevice: """Mock async_upnp_client device.""" mock_upnp_device = create_autospec(UpnpDevice, instance=True) mock_upnp_device.device_url = TEST_DISCOVERY.ssdp_location + mock_upnp_device.serial_number = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_SERIAL] mock_igd_device = create_autospec(IgdDevice) + mock_igd_device.device_type = TEST_DISCOVERY.ssdp_st mock_igd_device.name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] mock_igd_device.manufacturer = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MANUFACTURER] mock_igd_device.model_name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MODEL_NAME] diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index e89b8274c18..f0a1de1ce37 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -21,6 +22,7 @@ from homeassistant.core import HomeAssistant from .conftest import ( TEST_DISCOVERY, TEST_FRIENDLY_NAME, + TEST_HOST, TEST_LOCATION, TEST_MAC_ADDRESS, TEST_ST, @@ -140,7 +142,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): @pytest.mark.usefixtures("mock_mac_address_from_host") -async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): +async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant): """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -171,6 +173,38 @@ async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): assert result["reason"] == "config_entry_updated" +@pytest.mark.usefixtures("mock_mac_address_from_host") +async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant): + """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_USN, + data={ + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_HOST: TEST_HOST, + }, + source=config_entries.SOURCE_SSDP, + state=config_entries.ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) + + # New discovery via step ssdp. + new_udn = TEST_UDN + "2" + new_discovery = deepcopy(TEST_DISCOVERY) + new_discovery.ssdp_usn = f"{new_udn}::{TEST_ST}" + new_discovery.upnp["_udn"] = new_udn + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=new_discovery, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "config_entry_updated" + + @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", From 3240f8f93864b6bae9272bb011082bf04a031780 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 3 Sep 2022 23:19:05 +0200 Subject: [PATCH 063/231] Refactor zwave_js event handling (#77732) * Refactor zwave_js event handling * Clean up --- homeassistant/components/zwave_js/__init__.py | 585 ++++++++++-------- homeassistant/components/zwave_js/const.py | 1 - 2 files changed, 338 insertions(+), 248 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 538fe911dd0..03a8ee5fce2 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable +from collections.abc import Coroutine from typing import Any from async_timeout import timeout @@ -79,7 +79,6 @@ from .const import ( CONF_USB_PATH, CONF_USE_ADDON, DATA_CLIENT, - DATA_PLATFORM_SETUP, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, @@ -104,7 +103,8 @@ from .services import ZWaveServices CONNECT_TIMEOUT = 10 DATA_CLIENT_LISTEN_TASK = "client_listen_task" -DATA_START_PLATFORM_TASK = "start_platform_task" +DATA_DRIVER_EVENTS = "driver_events" +DATA_START_CLIENT_TASK = "start_client_task" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -118,51 +118,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@callback -def register_node_in_dev_reg( - hass: HomeAssistant, - entry: ConfigEntry, - dev_reg: device_registry.DeviceRegistry, - driver: Driver, - node: ZwaveNode, - remove_device_func: Callable[[device_registry.DeviceEntry], None], -) -> device_registry.DeviceEntry: - """Register node in dev reg.""" - device_id = get_device_id(driver, node) - device_id_ext = get_device_id_ext(driver, node) - device = dev_reg.async_get_device({device_id}) - - # Replace the device if it can be determined that this node is not the - # same product as it was previously. - if ( - device_id_ext - and device - and len(device.identifiers) == 2 - and device_id_ext not in device.identifiers - ): - remove_device_func(device) - device = None - - if device_id_ext: - ids = {device_id, device_id_ext} - else: - ids = {device_id} - - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers=ids, - sw_version=node.firmware_version, - name=node.name or node.device_config.description or f"Node {node.node_id}", - model=node.device_config.label, - manufacturer=node.device_config.manufacturer, - suggested_area=node.location if node.location else UNDEFINED, - ) - - async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) - - return device - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" if use_addon := entry.data.get(CONF_USE_ADDON): @@ -191,37 +146,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up websocket API async_register_api(hass) - platform_task = hass.async_create_task(start_platforms(hass, entry, client)) + # Create a task to allow the config entry to be unloaded before the driver is ready. + # Unloading the config entry is needed if the client listen task errors. + start_client_task = hass.async_create_task(start_client(hass, entry, client)) hass.data[DOMAIN].setdefault(entry.entry_id, {})[ - DATA_START_PLATFORM_TASK - ] = platform_task + DATA_START_CLIENT_TASK + ] = start_client_task return True -async def start_platforms( +async def start_client( hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient ) -> None: - """Start platforms and perform discovery.""" + """Start listening with the client.""" entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) entry_hass_data[DATA_CLIENT] = client - entry_hass_data[DATA_PLATFORM_SETUP] = {} - driver_ready = asyncio.Event() + driver_events = entry_hass_data[DATA_DRIVER_EVENTS] = DriverEvents(hass, entry) async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await disconnect_client(hass, entry) - listen_task = asyncio.create_task(client_listen(hass, entry, client, driver_ready)) + listen_task = asyncio.create_task( + client_listen(hass, entry, client, driver_events.ready) + ) entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task entry.async_on_unload( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) ) try: - await driver_ready.wait() + await driver_events.ready.wait() except asyncio.CancelledError: - LOGGER.debug("Cancelling start platforms") + LOGGER.debug("Cancelling start client") return LOGGER.info("Connection to Zwave JS Server initialized") @@ -229,37 +187,289 @@ async def start_platforms( if client.driver is None: raise RuntimeError("Driver not ready.") - await setup_driver(hass, entry, client, client.driver) + await driver_events.setup(client.driver) -async def setup_driver( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient, driver: Driver -) -> None: - """Set up devices using the ready driver.""" - dev_reg = device_registry.async_get(hass) - ent_reg = entity_registry.async_get(hass) - entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) - platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] - registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) - discovered_value_ids: dict[str, set[str]] = defaultdict(set) +class DriverEvents: + """Represent driver events.""" - async def async_setup_platform(platform: Platform) -> None: - """Set up platform if needed.""" - if platform not in platform_setup_tasks: - platform_setup_tasks[platform] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) + driver: Driver + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Set up the driver events instance.""" + self.config_entry = entry + self.dev_reg = device_registry.async_get(hass) + self.hass = hass + self.platform_setup_tasks: dict[str, asyncio.Task] = {} + self.ready = asyncio.Event() + # Make sure to not pass self to ControllerEvents until all attributes are set. + self.controller_events = ControllerEvents(hass, self) + + async def setup(self, driver: Driver) -> None: + """Set up devices using the ready driver.""" + self.driver = driver + + # If opt in preference hasn't been specified yet, we do nothing, otherwise + # we apply the preference + if opted_in := self.config_entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): + await async_enable_statistics(driver) + elif opted_in is False: + await driver.async_disable_statistics() + + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + self.dev_reg, self.config_entry.entry_id + ) + known_devices = [ + self.dev_reg.async_get_device({get_device_id(driver, node)}) + for node in driver.controller.nodes.values() + ] + + # Devices that are in the device registry that are not known by the controller can be removed + for device in stored_devices: + if device not in known_devices: + self.dev_reg.async_remove_device(device.id) + + # run discovery on all ready nodes + await asyncio.gather( + *( + self.controller_events.async_on_node_added(node) + for node in driver.controller.nodes.values() ) - await platform_setup_tasks[platform] + ) + + # listen for new nodes being added to the mesh + self.config_entry.async_on_unload( + driver.controller.on( + "node added", + lambda event: self.hass.async_create_task( + self.controller_events.async_on_node_added(event["node"]) + ), + ) + ) + # listen for nodes being removed from the mesh + # NOTE: This will not remove nodes that were removed when HA was not running + self.config_entry.async_on_unload( + driver.controller.on( + "node removed", self.controller_events.async_on_node_removed + ) + ) + + async def async_setup_platform(self, platform: Platform) -> None: + """Set up platform if needed.""" + if platform not in self.platform_setup_tasks: + self.platform_setup_tasks[platform] = self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + ) + await self.platform_setup_tasks[platform] + + +class ControllerEvents: + """Represent controller events. + + Handle the following events: + - node added + - node removed + """ + + def __init__(self, hass: HomeAssistant, driver_events: DriverEvents) -> None: + """Set up the controller events instance.""" + self.hass = hass + self.config_entry = driver_events.config_entry + self.discovered_value_ids: dict[str, set[str]] = defaultdict(set) + self.driver_events = driver_events + self.dev_reg = driver_events.dev_reg + self.registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) + self.node_events = NodeEvents(hass, self) @callback - def remove_device(device: device_registry.DeviceEntry) -> None: + def remove_device(self, device: device_registry.DeviceEntry) -> None: """Remove device from registry.""" # note: removal of entity registry entry is handled by core - dev_reg.async_remove_device(device.id) - registered_unique_ids.pop(device.id, None) - discovered_value_ids.pop(device.id, None) + self.dev_reg.async_remove_device(device.id) + self.registered_unique_ids.pop(device.id, None) + self.discovered_value_ids.pop(device.id, None) + + async def async_on_node_added(self, node: ZwaveNode) -> None: + """Handle node added event.""" + # No need for a ping button or node status sensor for controller nodes + if not node.is_controller_node: + # Create a node status sensor for each device + await self.driver_events.async_setup_platform(Platform.SENSOR) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_node_status_sensor", + node, + ) + + # Create a ping button for each device + await self.driver_events.async_setup_platform(Platform.BUTTON) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_ping_button_entity", + node, + ) + + # Create a firmware update entity for each device + await self.driver_events.async_setup_platform(Platform.UPDATE) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", + node, + ) + + # we only want to run discovery when the node has reached ready state, + # otherwise we'll have all kinds of missing info issues. + if node.ready: + await self.node_events.async_on_node_ready(node) + return + # if node is not yet ready, register one-time callback for ready state + LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) + node.once( + "ready", + lambda event: self.hass.async_create_task( + self.node_events.async_on_node_ready(event["node"]) + ), + ) + # we do submit the node to device registry so user has + # some visual feedback that something is (in the process of) being added + self.register_node_in_dev_reg(node) + + @callback + def async_on_node_removed(self, event: dict) -> None: + """Handle node removed event.""" + node: ZwaveNode = event["node"] + replaced: bool = event.get("replaced", False) + # grab device in device registry attached to this node + dev_id = get_device_id(self.driver_events.driver, node) + device = self.dev_reg.async_get_device({dev_id}) + # We assert because we know the device exists + assert device + if replaced: + self.discovered_value_ids.pop(device.id, None) + + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{get_valueless_base_unique_id(self.driver_events.driver, node)}_remove_entity", + ) + else: + self.remove_device(device) + + @callback + def register_node_in_dev_reg(self, node: ZwaveNode) -> device_registry.DeviceEntry: + """Register node in dev reg.""" + driver = self.driver_events.driver + device_id = get_device_id(driver, node) + device_id_ext = get_device_id_ext(driver, node) + device = self.dev_reg.async_get_device({device_id}) + + # Replace the device if it can be determined that this node is not the + # same product as it was previously. + if ( + device_id_ext + and device + and len(device.identifiers) == 2 + and device_id_ext not in device.identifiers + ): + self.remove_device(device) + device = None + + if device_id_ext: + ids = {device_id, device_id_ext} + else: + ids = {device_id} + + device = self.dev_reg.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers=ids, + sw_version=node.firmware_version, + name=node.name or node.device_config.description or f"Node {node.node_id}", + model=node.device_config.label, + manufacturer=node.device_config.manufacturer, + suggested_area=node.location if node.location else UNDEFINED, + ) + + async_dispatcher_send(self.hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) + + return device + + +class NodeEvents: + """Represent node events. + + Handle the following events: + - ready + - value added + - value updated + - metadata updated + - value notification + - notification + """ + + def __init__( + self, hass: HomeAssistant, controller_events: ControllerEvents + ) -> None: + """Set up the node events instance.""" + self.config_entry = controller_events.config_entry + self.controller_events = controller_events + self.dev_reg = controller_events.dev_reg + self.ent_reg = entity_registry.async_get(hass) + self.hass = hass + + async def async_on_node_ready(self, node: ZwaveNode) -> None: + """Handle node ready event.""" + LOGGER.debug("Processing node %s", node) + # register (or update) node in device registry + device = self.controller_events.register_node_in_dev_reg(node) + # We only want to create the defaultdict once, even on reinterviews + if device.id not in self.controller_events.registered_unique_ids: + self.controller_events.registered_unique_ids[device.id] = defaultdict(set) + + value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} + + # run discovery on all node values and create/update entities + await asyncio.gather( + *( + self.async_handle_discovery_info( + device, disc_info, value_updates_disc_info + ) + for disc_info in async_discover_node_values( + node, device, self.controller_events.discovered_value_ids + ) + ) + ) + + # add listeners to handle new values that get added later + for event in ("value added", "value updated", "metadata updated"): + self.config_entry.async_on_unload( + node.on( + event, + lambda event: self.hass.async_create_task( + self.async_on_value_added( + value_updates_disc_info, event["value"] + ) + ), + ) + ) + + # add listener for stateless node value notification events + self.config_entry.async_on_unload( + node.on( + "value notification", + lambda event: self.async_on_value_notification( + event["value_notification"] + ), + ) + ) + # add listener for stateless node notification events + self.config_entry.async_on_unload( + node.on("notification", self.async_on_notification) + ) async def async_handle_discovery_info( + self, device: device_registry.DeviceEntry, disc_info: ZwaveDiscoveryInfo, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], @@ -269,20 +479,22 @@ async def setup_driver( # noqa: C901 # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. async_migrate_discovered_value( - hass, - ent_reg, - registered_unique_ids[device.id][disc_info.platform], + self.hass, + self.ent_reg, + self.controller_events.registered_unique_ids[device.id][disc_info.platform], device, - driver, + self.controller_events.driver_events.driver, disc_info, ) platform = disc_info.platform - await async_setup_platform(platform) + await self.controller_events.driver_events.async_setup_platform(platform) LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_{platform}", + disc_info, ) # If we don't need to watch for updates return early @@ -294,151 +506,57 @@ async def setup_driver( # noqa: C901 if len(value_updates_disc_info) != 1: return # add listener for value updated events - entry.async_on_unload( + self.config_entry.async_on_unload( disc_info.node.on( "value updated", - lambda event: async_on_value_updated_fire_event( + lambda event: self.async_on_value_updated_fire_event( value_updates_disc_info, event["value"] ), ) ) - async def async_on_node_ready(node: ZwaveNode) -> None: - """Handle node ready event.""" - LOGGER.debug("Processing node %s", node) - # register (or update) node in device registry - device = register_node_in_dev_reg( - hass, entry, dev_reg, driver, node, remove_device - ) - # We only want to create the defaultdict once, even on reinterviews - if device.id not in registered_unique_ids: - registered_unique_ids[device.id] = defaultdict(set) - - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} - - # run discovery on all node values and create/update entities - await asyncio.gather( - *( - async_handle_discovery_info(device, disc_info, value_updates_disc_info) - for disc_info in async_discover_node_values( - node, device, discovered_value_ids - ) - ) - ) - - # add listeners to handle new values that get added later - for event in ("value added", "value updated", "metadata updated"): - entry.async_on_unload( - node.on( - event, - lambda event: hass.async_create_task( - async_on_value_added(value_updates_disc_info, event["value"]) - ), - ) - ) - - # add listener for stateless node value notification events - entry.async_on_unload( - node.on( - "value notification", - lambda event: async_on_value_notification(event["value_notification"]), - ) - ) - # add listener for stateless node notification events - entry.async_on_unload(node.on("notification", async_on_notification)) - - async def async_on_node_added(node: ZwaveNode) -> None: - """Handle node added event.""" - # No need for a ping button or node status sensor for controller nodes - if not node.is_controller_node: - # Create a node status sensor for each device - await async_setup_platform(Platform.SENSOR) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node - ) - - # Create a ping button for each device - await async_setup_platform(Platform.BUTTON) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node - ) - - # Create a firmware update entity for each device - await async_setup_platform(Platform.UPDATE) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_firmware_update_entity", node - ) - - # we only want to run discovery when the node has reached ready state, - # otherwise we'll have all kinds of missing info issues. - if node.ready: - await async_on_node_ready(node) - return - # if node is not yet ready, register one-time callback for ready state - LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) - node.once( - "ready", - lambda event: hass.async_create_task(async_on_node_ready(event["node"])), - ) - # we do submit the node to device registry so user has - # some visual feedback that something is (in the process of) being added - register_node_in_dev_reg(hass, entry, dev_reg, driver, node, remove_device) - async def async_on_value_added( - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value + self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value ) -> None: """Fire value updated event.""" # If node isn't ready or a device for this node doesn't already exist, we can # let the node ready event handler perform discovery. If a value has already # been processed, we don't need to do it again - device_id = get_device_id(driver, value.node) + device_id = get_device_id( + self.controller_events.driver_events.driver, value.node + ) if ( not value.node.ready - or not (device := dev_reg.async_get_device({device_id})) - or value.value_id in discovered_value_ids[device.id] + or not (device := self.dev_reg.async_get_device({device_id})) + or value.value_id in self.controller_events.discovered_value_ids[device.id] ): return LOGGER.debug("Processing node %s added value %s", value.node, value) await asyncio.gather( *( - async_handle_discovery_info(device, disc_info, value_updates_disc_info) + self.async_handle_discovery_info( + device, disc_info, value_updates_disc_info + ) for disc_info in async_discover_single_value( - value, device, discovered_value_ids + value, device, self.controller_events.discovered_value_ids ) ) ) @callback - def async_on_node_removed(event: dict) -> None: - """Handle node removed event.""" - node: ZwaveNode = event["node"] - replaced: bool = event.get("replaced", False) - # grab device in device registry attached to this node - dev_id = get_device_id(driver, node) - device = dev_reg.async_get_device({dev_id}) - # We assert because we know the device exists - assert device - if replaced: - discovered_value_ids.pop(device.id, None) - - async_dispatcher_send( - hass, - f"{DOMAIN}_{get_valueless_base_unique_id(driver, node)}_remove_entity", - ) - else: - remove_device(device) - - @callback - def async_on_value_notification(notification: ValueNotification) -> None: + def async_on_value_notification(self, notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" - device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) + driver = self.controller_events.driver_events.driver + device = self.dev_reg.async_get_device( + {get_device_id(driver, notification.node)} + ) # We assert because we know the device exists assert device raw_value = value = notification.value if notification.metadata.states: value = notification.metadata.states.get(str(value), value) - hass.bus.async_fire( + self.hass.bus.async_fire( ZWAVE_JS_VALUE_NOTIFICATION_EVENT, { ATTR_DOMAIN: DOMAIN, @@ -459,15 +577,19 @@ async def setup_driver( # noqa: C901 ) @callback - def async_on_notification(event: dict[str, Any]) -> None: + def async_on_notification(self, event: dict[str, Any]) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" if "notification" not in event: LOGGER.info("Unknown notification: %s", event) return + + driver = self.controller_events.driver_events.driver notification: EntryControlNotification | NotificationNotification | PowerLevelNotification | MultilevelSwitchNotification = event[ "notification" ] - device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) + device = self.dev_reg.async_get_device( + {get_device_id(driver, notification.node)} + ) # We assert because we know the device exists assert device event_data = { @@ -521,31 +643,35 @@ async def setup_driver( # noqa: C901 else: raise TypeError(f"Unhandled notification type: {notification}") - hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) + self.hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) @callback def async_on_value_updated_fire_event( - value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value + self, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value ) -> None: """Fire value updated event.""" # Get the discovery info for the value that was updated. If there is # no discovery info for this value, we don't need to fire an event if value.value_id not in value_updates_disc_info: return + + driver = self.controller_events.driver_events.driver disc_info = value_updates_disc_info[value.value_id] - device = dev_reg.async_get_device({get_device_id(driver, value.node)}) + device = self.dev_reg.async_get_device({get_device_id(driver, value.node)}) # We assert because we know the device exists assert device unique_id = get_unique_id(driver, disc_info.primary_value.value_id) - entity_id = ent_reg.async_get_entity_id(disc_info.platform, DOMAIN, unique_id) + entity_id = self.ent_reg.async_get_entity_id( + disc_info.platform, DOMAIN, unique_id + ) raw_value = value_ = value.value if value.metadata.states: value_ = value.metadata.states.get(str(value), value_) - hass.bus.async_fire( + self.hass.bus.async_fire( ZWAVE_JS_VALUE_UPDATED_EVENT, { ATTR_NODE_ID: value.node.node_id, @@ -564,43 +690,6 @@ async def setup_driver( # noqa: C901 }, ) - # If opt in preference hasn't been specified yet, we do nothing, otherwise - # we apply the preference - if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): - await async_enable_statistics(driver) - elif opted_in is False: - await driver.async_disable_statistics() - - # Check for nodes that no longer exist and remove them - stored_devices = device_registry.async_entries_for_config_entry( - dev_reg, entry.entry_id - ) - known_devices = [ - dev_reg.async_get_device({get_device_id(driver, node)}) - for node in driver.controller.nodes.values() - ] - - # Devices that are in the device registry that are not known by the controller can be removed - for device in stored_devices: - if device not in known_devices: - dev_reg.async_remove_device(device.id) - - # run discovery on all ready nodes - await asyncio.gather( - *(async_on_node_added(node) for node in driver.controller.nodes.values()) - ) - - # listen for new nodes being added to the mesh - entry.async_on_unload( - driver.controller.on( - "node added", - lambda event: hass.async_create_task(async_on_node_added(event["node"])), - ) - ) - # listen for nodes being removed from the mesh - # NOTE: This will not remove nodes that were removed when HA was not running - entry.async_on_unload(driver.controller.on("node removed", async_on_node_removed)) - async def client_listen( hass: HomeAssistant, @@ -633,14 +722,15 @@ async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: data = hass.data[DOMAIN][entry.entry_id] client: ZwaveClient = data[DATA_CLIENT] listen_task: asyncio.Task = data[DATA_CLIENT_LISTEN_TASK] - platform_task: asyncio.Task = data[DATA_START_PLATFORM_TASK] + start_client_task: asyncio.Task = data[DATA_START_CLIENT_TASK] + driver_events: DriverEvents = data[DATA_DRIVER_EVENTS] listen_task.cancel() - platform_task.cancel() - platform_setup_tasks = data.get(DATA_PLATFORM_SETUP, {}).values() + start_client_task.cancel() + platform_setup_tasks = driver_events.platform_setup_tasks.values() for task in platform_setup_tasks: task.cancel() - await asyncio.gather(listen_task, platform_task, *platform_setup_tasks) + await asyncio.gather(listen_task, start_client_task, *platform_setup_tasks) if client.connected: await client.disconnect() @@ -650,9 +740,10 @@ async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" info = hass.data[DOMAIN][entry.entry_id] + driver_events: DriverEvents = info[DATA_DRIVER_EVENTS] - tasks = [] - for platform, task in info[DATA_PLATFORM_SETUP].items(): + 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) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index ddd4917e596..db3da247e7d 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -21,7 +21,6 @@ CONF_DATA_COLLECTION_OPTED_IN = "data_collection_opted_in" DOMAIN = "zwave_js" DATA_CLIENT = "client" -DATA_PLATFORM_SETUP = "platform_setup" EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" From 2bfcdc66b6ce6c38a4a3df431321eb1af2a10b96 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 5 Sep 2022 21:50:47 +0200 Subject: [PATCH 064/231] Allow empty db in SQL options flow (#77777) --- homeassistant/components/sql/config_flow.py | 7 +- tests/components/sql/test_config_flow.py | 75 ++++++++++++++++++--- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index dc3a839ef1d..bcbece9f7f6 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -147,9 +147,12 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage SQL options.""" errors = {} + db_url_default = DEFAULT_URL.format( + hass_config_path=self.hass.config.path(DEFAULT_DB_FILE) + ) if user_input is not None: - db_url = user_input[CONF_DB_URL] + db_url = user_input.get(CONF_DB_URL, db_url_default) query = user_input[CONF_QUERY] column = user_input[CONF_COLUMN_NAME] @@ -176,7 +179,7 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema( { - vol.Required( + vol.Optional( CONF_DB_URL, description={ "suggested_value": self.entry.options[CONF_DB_URL] diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 7c3571f8f19..96402e1bc7a 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -20,7 +20,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, recorder_mock) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -52,7 +52,7 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(hass: HomeAssistant) -> None: +async def test_import_flow_success(hass: HomeAssistant, recorder_mock) -> None: """Test a successful import of yaml.""" with patch( @@ -79,7 +79,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: +async def test_import_flow_already_exist(hass: HomeAssistant, recorder_mock) -> None: """Test import of yaml already exist.""" MockConfigEntry( @@ -102,7 +102,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: assert result3["reason"] == "already_configured" -async def test_flow_fails_db_url(hass: HomeAssistant) -> None: +async def test_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -123,7 +123,7 @@ async def test_flow_fails_db_url(hass: HomeAssistant) -> None: assert result4["errors"] == {"db_url": "db_url_invalid"} -async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: +async def test_flow_fails_invalid_query(hass: HomeAssistant, recorder_mock) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -169,7 +169,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: } -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow(hass: HomeAssistant, recorder_mock) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -218,7 +218,9 @@ async def test_options_flow(hass: HomeAssistant) -> None: } -async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None: +async def test_options_flow_name_previously_removed( + hass: HomeAssistant, recorder_mock +) -> None: """Test options config flow where the name was missing.""" entry = MockConfigEntry( domain=DOMAIN, @@ -269,7 +271,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None } -async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None: +async def test_options_flow_fails_db_url(hass: HomeAssistant, recorder_mock) -> None: """Test options flow fails incorrect db url.""" entry = MockConfigEntry( domain=DOMAIN, @@ -312,7 +314,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None: async def test_options_flow_fails_invalid_query( - hass: HomeAssistant, + hass: HomeAssistant, recorder_mock ) -> None: """Test options flow fails incorrect query and template.""" entry = MockConfigEntry( @@ -367,3 +369,58 @@ async def test_options_flow_fails_invalid_query( "column": "size", "unit_of_measurement": "MiB", } + + +async def test_options_flow_db_url_empty(hass: HomeAssistant, recorder_mock) -> None: + """Test options config flow with leaving db_url empty.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "db_url": "sqlite://", + "name": "Get Value", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "value_template": None, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "query": "SELECT 5 as size", + "column": "size", + "unit_of_measurement": "MiB", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "name": "Get Value", + "db_url": "sqlite://", + "query": "SELECT 5 as size", + "column": "size", + "value_template": None, + "unit_of_measurement": "MiB", + } From f5e61ecdec449373c9877fe3a0247109311a2956 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Tue, 6 Sep 2022 17:34:11 +0200 Subject: [PATCH 065/231] Handle exception on projector being unavailable (#77802) --- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epson/media_player.py | 14 +++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 82b74486377..0ba8351fd15 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -3,7 +3,7 @@ "name": "Epson", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": ["epson-projector==0.4.6"], + "requirements": ["epson-projector==0.5.0"], "codeowners": ["@pszafer"], "iot_class": "local_polling", "loggers": ["epson_projector"] diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 57bb0165f6a..0e70984ac31 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from epson_projector import Projector +from epson_projector import Projector, ProjectorUnavailableError from epson_projector.const import ( BACK, BUSY, @@ -20,7 +20,6 @@ from epson_projector.const import ( POWER, SOURCE, SOURCE_LIST, - STATE_UNAVAILABLE as EPSON_STATE_UNAVAILABLE, TURN_OFF, TURN_ON, VOL_DOWN, @@ -123,11 +122,16 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): async def async_update(self) -> None: """Update state of device.""" - power_state = await self._projector.get_power() - _LOGGER.debug("Projector status: %s", power_state) - if not power_state or power_state == EPSON_STATE_UNAVAILABLE: + try: + power_state = await self._projector.get_power() + except ProjectorUnavailableError as ex: + _LOGGER.debug("Projector is unavailable: %s", ex) self._attr_available = False return + if not power_state: + self._attr_available = False + return + _LOGGER.debug("Projector status: %s", power_state) self._attr_available = True if power_state == EPSON_CODES[POWER]: self._attr_state = STATE_ON diff --git a/requirements_all.txt b/requirements_all.txt index 77cd2faa49e..496101c9703 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -630,7 +630,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.6 +epson-projector==0.5.0 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a518ff1d9cc..30cf9d2edb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -477,7 +477,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.6 +epson-projector==0.5.0 # homeassistant.components.faa_delays faadelays==0.0.7 From 61ee621c90cb5294d8d7bef426840ac2a9ec4fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20V=C3=B6lker?= Date: Tue, 6 Sep 2022 11:10:35 +0200 Subject: [PATCH 066/231] Adjust Renault default scan interval (#77823) raise DEFAULT_SCAN_INTERVAL to 7 minutes This PR is raising the default scan interval for the Renault API from 5 minutes to 7 minutes. Lower intervals fail sometimes, maybe due to quota limitations. This seems to be a working interval as described in home-assistant#73220 --- homeassistant/components/renault/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 89bf322c2bf..b29f4ad0701 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -6,7 +6,7 @@ DOMAIN = "renault" CONF_LOCALE = "locale" CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" -DEFAULT_SCAN_INTERVAL = 300 # 5 minutes +DEFAULT_SCAN_INTERVAL = 420 # 7 minutes PLATFORMS = [ Platform.BINARY_SENSOR, From 31d085cdf896c32a3cb1ea71c41b0eec6525e687 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 5 Sep 2022 13:56:27 -0500 Subject: [PATCH 067/231] Fix history stats device class when type is not time (#77855) --- .../components/history_stats/sensor.py | 3 +- tests/components/history_stats/test_sensor.py | 45 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index a42c516f12b..642c327e29d 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -143,7 +143,6 @@ class HistoryStatsSensorBase( class HistoryStatsSensor(HistoryStatsSensorBase): """A HistoryStats sensor.""" - _attr_device_class = SensorDeviceClass.DURATION _attr_state_class = SensorStateClass.MEASUREMENT def __init__( @@ -157,6 +156,8 @@ class HistoryStatsSensor(HistoryStatsSensorBase): self._attr_native_unit_of_measurement = UNITS[sensor_type] self._type = sensor_type self._process_update() + if self._type == CONF_TYPE_TIME: + self._attr_device_class = SensorDeviceClass.DURATION @callback def _process_update(self) -> None: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 8907f381a6c..5de74f71d1e 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -9,7 +9,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components.history_stats import DOMAIN -from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.const import ATTR_DEVICE_CLASS, SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component, setup_component @@ -1496,3 +1496,46 @@ async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock) async_fire_time_changed(hass, rolled_to_next_day_plus_18) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" + + +async def test_device_classes(hass, recorder_mock): + """Test the device classes.""" + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "time", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "count", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "ratio", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ as_timestamp(now()) + 3600 }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.time").attributes[ATTR_DEVICE_CLASS] == "duration" + assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.ratio").attributes + assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.count").attributes From 6989b16274c60c0e5b99c261c18b1e1f07bde9df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 08:00:05 -0500 Subject: [PATCH 068/231] Bump zeroconf to 0.39.1 (#77859) --- 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 4438a22040d..ec670558e66 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.0"], + "requirements": ["zeroconf==0.39.1"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3bf00427954..1299abcd61b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -40,7 +40,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.39.0 +zeroconf==0.39.1 # 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 496101c9703..b82217b6937 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2566,7 +2566,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.0 +zeroconf==0.39.1 # homeassistant.components.zha zha-quirks==0.0.79 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30cf9d2edb9..62dbd57e577 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1761,7 +1761,7 @@ yolink-api==0.0.9 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.39.0 +zeroconf==0.39.1 # homeassistant.components.zha zha-quirks==0.0.79 From 62dcbc4d4a33cef00f24a7c7c4caa99f4e5bbbbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 02:54:52 -0500 Subject: [PATCH 069/231] Add RSSI to the bluetooth debug log (#77860) --- homeassistant/components/bluetooth/manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 9fc00aa159b..80817deb2a1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -327,12 +327,13 @@ class BluetoothManager: matched_domains = self._integration_matcher.match_domains(service_info) _LOGGER.debug( - "%s: %s %s connectable: %s match: %s", + "%s: %s %s connectable: %s match: %s rssi: %s", source, address, advertisement_data, connectable, matched_domains, + device.rssi, ) for match in self._callback_index.match_callbacks(service_info): From 319b0b8902ab94d7d60b600947e0affb69db20b2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 5 Sep 2022 23:39:42 +0200 Subject: [PATCH 070/231] Pin astroid to fix pylint (#77862) --- requirements_test.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_test.txt b/requirements_test.txt index d15431a1d85..99e2f8d8402 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,6 +7,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt +astroid==2.12.5 codecov==2.1.12 coverage==6.4.4 freezegun==1.2.1 From d98687b78914285b301914d7f28dc06fde2274ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 02:55:43 -0500 Subject: [PATCH 071/231] Bump thermopro-ble to 0.4.3 (#77863) * Bump thermopro-ble to 0.4.2 - Turns on rounding of long values - Uses bluetooth-data-tools under the hood - Adds the TP393 since it works without any changes to the parser Changelog: https://github.com/Bluetooth-Devices/thermopro-ble/compare/v0.4.0...v0.4.2 * bump again for device detection fix --- homeassistant/components/thermopro/manifest.json | 7 +++++-- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json index 912a070ccf1..dca643a28cf 100644 --- a/homeassistant/components/thermopro/manifest.json +++ b/homeassistant/components/thermopro/manifest.json @@ -3,9 +3,12 @@ "name": "ThermoPro", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/thermopro", - "bluetooth": [{ "local_name": "TP35*", "connectable": false }], + "bluetooth": [ + { "local_name": "TP35*", "connectable": false }, + { "local_name": "TP39*", "connectable": false } + ], "dependencies": ["bluetooth"], - "requirements": ["thermopro-ble==0.4.0"], + "requirements": ["thermopro-ble==0.4.3"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d7230213302..b2400f733da 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -269,6 +269,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "local_name": "TP35*", "connectable": False }, + { + "domain": "thermopro", + "local_name": "TP39*", + "connectable": False + }, { "domain": "xiaomi_ble", "connectable": False, diff --git a/requirements_all.txt b/requirements_all.txt index b82217b6937..18612e9ee5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,7 +2366,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.3.1 # homeassistant.components.thermopro -thermopro-ble==0.4.0 +thermopro-ble==0.4.3 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62dbd57e577..9b6edc09648 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1615,7 +1615,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.3.1 # homeassistant.components.thermopro -thermopro-ble==0.4.0 +thermopro-ble==0.4.3 # homeassistant.components.todoist todoist-python==8.0.0 From a13438c5b06986a83b3abb12a7bd6f3f916150a1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 6 Sep 2022 09:40:20 -0400 Subject: [PATCH 072/231] Improve performance impact of zwave_js update entity and other tweaks (#77866) * Improve performance impact of zwave_js update entity and other tweaks * Reduce concurrent polls * we need to write state after setting in progress to false * Fix existing tests * Fix tests by fixing fixtures * remove redundant conditional * Add test for delayed startup * tweaks * outdent happy path * Add missing PROGRESS feature support * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * Fix tests by reverting outdent, PR comments, mark callback * Remove redundant conditional * make more readable * Remove unused SCAN_INTERVAL * Catch FailedZWaveCommand * Add comment and remove poll unsub on update * Fix catching error and add test * readability * Fix tests * Add assertions * rely on built in progress indicator Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 25 +- homeassistant/components/zwave_js/update.py | 98 +++--- tests/components/zwave_js/conftest.py | 3 +- .../aeotec_radiator_thermostat_state.json | 8 +- .../fixtures/aeotec_zw164_siren_state.json | 174 +++++------ .../fixtures/bulb_6_multi_color_state.json | 4 +- .../fixtures/chain_actuator_zws12_state.json | 9 +- .../fixtures/climate_adc_t3000_state.json | 294 +++++++++--------- .../fixtures/climate_danfoss_lc_13_state.json | 56 ---- .../climate_eurotronic_spirit_z_state.json | 3 +- .../climate_heatit_z_trm2fx_state.json | 186 +++++------ .../climate_heatit_z_trm3_no_value_state.json | 198 ++++++------ .../fixtures/climate_heatit_z_trm3_state.json | 4 +- ...setpoint_on_different_endpoints_state.json | 174 +++++------ ..._ct100_plus_different_endpoints_state.json | 116 ------- ...ostat_ct101_multiple_temp_units_state.json | 174 +++++------ .../cover_aeotec_nano_shutter_state.json | 174 +++++------ .../fixtures/cover_fibaro_fgr222_state.json | 138 ++++---- .../fixtures/cover_iblinds_v2_state.json | 4 +- .../fixtures/cover_qubino_shutter_state.json | 150 ++++----- .../zwave_js/fixtures/cover_zw062_state.json | 4 +- .../fixtures/eaton_rf9640_dimmer_state.json | 4 +- .../fixtures/ecolink_door_sensor_state.json | 4 +- .../express_controls_ezmultipli_state.json | 162 +++++----- .../zwave_js/fixtures/fan_ge_12730_state.json | 4 +- .../zwave_js/fixtures/fan_generic_state.json | 4 +- .../zwave_js/fixtures/fan_hs_fc200_state.json | 198 ++++++------ .../fixtures/fortrezz_ssa1_siren_state.json | 66 ++-- .../fixtures/fortrezz_ssa3_siren_state.json | 66 ++-- .../fixtures/inovelli_lzw36_state.json | 210 ++++++------- .../light_color_null_values_state.json | 138 ++++---- .../fixtures/lock_august_asl03_state.json | 4 +- .../fixtures/lock_id_lock_as_id150_state.json | 162 +++++----- ...pp_electric_strike_lock_control_state.json | 162 +++++----- .../fixtures/multisensor_6_state.json | 4 +- .../nortek_thermostat_added_event.json | 4 +- .../nortek_thermostat_removed_event.json | 4 +- .../fixtures/nortek_thermostat_state.json | 4 +- .../fixtures/null_name_check_state.json | 150 ++++----- .../fixtures/srt321_hrt4_zw_state.json | 54 ++-- .../vision_security_zl7432_state.json | 66 ++-- .../zwave_js/fixtures/zen_31_state.json | 246 +++++++-------- .../fixtures/zp3111-5_not_ready_state.json | 4 +- .../zwave_js/fixtures/zp3111-5_state.json | 162 +++++----- tests/components/zwave_js/test_init.py | 8 +- tests/components/zwave_js/test_update.py | 82 ++++- 46 files changed, 1941 insertions(+), 2027 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 03a8ee5fce2..98219520693 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -8,6 +8,7 @@ from typing import Any from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode @@ -312,14 +313,6 @@ class ControllerEvents: node, ) - # Create a firmware update entity for each device - await self.driver_events.async_setup_platform(Platform.UPDATE) - async_dispatcher_send( - self.hass, - f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", - node, - ) - # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: @@ -463,11 +456,27 @@ class NodeEvents: ), ) ) + # add listener for stateless node notification events self.config_entry.async_on_unload( node.on("notification", self.async_on_notification) ) + # Create a firmware update entity for each non-controller device that + # supports firmware updates + if not node.is_controller_node and any( + CommandClass.FIRMWARE_UPDATE_MD.value == cc.id + for cc in node.command_classes + ): + await self.controller_events.driver_events.async_setup_platform( + Platform.UPDATE + ) + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_firmware_update_entity", + node, + ) + async def async_handle_discovery_info( self, device: device_registry.DeviceEntry, diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 7f25788e0be..97c14746dd9 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -1,14 +1,15 @@ """Representation of Z-Wave updates.""" from __future__ import annotations +import asyncio from collections.abc import Callable -from datetime import timedelta +from datetime import datetime, timedelta from typing import Any from awesomeversion import AwesomeVersion from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import NodeStatus -from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.exceptions import BaseZwaveJSServerError, FailedZWaveCommand from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import FirmwareUpdateInfo from zwave_js_server.model.node import Node as ZwaveNode @@ -21,12 +22,13 @@ 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 +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start from .const import API_KEY_FIRMWARE_UPDATE_SERVICE, DATA_CLIENT, DOMAIN, LOGGER from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 1 -SCAN_INTERVAL = timedelta(days=1) async def async_setup_entry( @@ -37,12 +39,14 @@ async def async_setup_entry( """Set up Z-Wave button from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + semaphore = asyncio.Semaphore(3) + @callback def async_add_firmware_update_entity(node: ZwaveNode) -> None: """Add firmware update entity.""" driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. - async_add_entities([ZWaveNodeFirmwareUpdate(driver, node)], True) + async_add_entities([ZWaveNodeFirmwareUpdate(driver, node, semaphore)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -62,30 +66,36 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES ) _attr_has_entity_name = True + _attr_should_poll = False - def __init__(self, driver: Driver, node: ZwaveNode) -> None: + def __init__( + self, driver: Driver, node: ZwaveNode, semaphore: asyncio.Semaphore + ) -> None: """Initialize a Z-Wave device firmware update entity.""" self.driver = driver self.node = node + self.semaphore = semaphore self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None + self._poll_unsub: Callable[[], None] | None = None # Entity class attributes self._attr_name = "Firmware" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" + self._attr_installed_version = self._attr_latest_version = node.firmware_version # device may not be precreated in main handler yet self._attr_device_info = get_device_info(driver, node) - self._attr_installed_version = self._attr_latest_version = node.firmware_version - + @callback def _update_on_status_change(self, _: dict[str, Any]) -> None: """Update the entity when node is awake.""" self._status_unsub = None - self.hass.async_create_task(self.async_update(True)) + self.hass.async_create_task(self._async_update()) - async def async_update(self, write_state: bool = False) -> None: + async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: """Update the entity.""" + self._poll_unsub = None for status, event_name in ( (NodeStatus.ASLEEP, "wake up"), (NodeStatus.DEAD, "alive"), @@ -97,34 +107,38 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) return - if available_firmware_updates := ( - await self.driver.controller.async_get_available_firmware_updates( - self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + try: + async with self.semaphore: + available_firmware_updates = ( + await self.driver.controller.async_get_available_firmware_updates( + self.node, API_KEY_FIRMWARE_UPDATE_SERVICE + ) + ) + except FailedZWaveCommand as err: + LOGGER.debug( + "Failed to get firmware updates for node %s: %s", + self.node.node_id, + err, ) - ): - self._latest_version_firmware = max( - available_firmware_updates, - key=lambda x: AwesomeVersion(x.version), - ) - self._async_process_available_updates(write_state) - - @callback - def _async_process_available_updates(self, write_state: bool = True) -> None: - """ - Process available firmware updates. - - Sets latest version attribute and FirmwareUpdateInfo instance. - """ - # If we have an available firmware update that is a higher version than what's - # on the node, we should advertise it, otherwise we are on the latest version - if (firmware := self._latest_version_firmware) and AwesomeVersion( - firmware.version - ) > AwesomeVersion(self.node.firmware_version): - self._attr_latest_version = firmware.version else: - self._attr_latest_version = self._attr_installed_version - if write_state: - self.async_write_ha_state() + if available_firmware_updates: + self._latest_version_firmware = latest_firmware = max( + available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) + + # If we have an available firmware update that is a higher version than + # what's on the node, we should advertise it, otherwise there is + # nothing to do. + new_version = latest_firmware.version + current_version = self.node.firmware_version + if AwesomeVersion(new_version) > AwesomeVersion(current_version): + self._attr_latest_version = new_version + self.async_write_ha_state() + finally: + self._poll_unsub = async_call_later( + self.hass, timedelta(days=1), self._async_update + ) async def async_release_notes(self) -> str | None: """Get release notes.""" @@ -138,8 +152,6 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._attr_in_progress = True - self.async_write_ha_state() try: for file in firmware.files: await self.driver.controller.async_begin_ota_firmware_update( @@ -148,11 +160,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): except BaseZwaveJSServerError as err: raise HomeAssistantError(err) from err else: - self._attr_installed_version = firmware.version + self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None - self._async_process_available_updates() - finally: - self._attr_in_progress = False + self.async_write_ha_state() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -179,8 +189,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) ) + self.async_on_remove(async_at_start(self.hass, self._async_update)) + async def async_will_remove_from_hass(self) -> None: """Call when entity will be removed.""" if self._status_unsub: self._status_unsub() self._status_unsub = None + + if self._poll_unsub: + self._poll_unsub() + self._poll_unsub = None diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 7131b1ade69..04a9c5671f9 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -584,7 +584,8 @@ def mock_client_fixture(controller_state, version_state, log_config_state): async def listen(driver_ready: asyncio.Event) -> None: driver_ready.set() - await asyncio.sleep(30) + listen_block = asyncio.Event() + await listen_block.wait() assert False, "Listen wasn't canceled!" async def disconnect(): diff --git a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json index 789a72c98fa..111714560b5 100644 --- a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json @@ -39,7 +39,13 @@ "neighbors": [6, 7, 45, 67], "interviewAttempts": 1, "endpoints": [ - { "nodeId": 4, "index": 0, "installerIcon": 4608, "userIcon": 4608 } + { + "nodeId": 4, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "commandClasses": [] + } ], "values": [ { diff --git a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json index 0f0dde61c83..f2b43878990 100644 --- a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json @@ -77,7 +77,93 @@ 32, 133, 89, 128, 121, 114, 115, 159, 108, 85, 134, 94 ], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 121, + "name": "Sound Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + } + ] }, { "nodeId": 2, @@ -3665,92 +3751,6 @@ ], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 121, - "name": "Sound Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 8, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3", "isControllerNode": false diff --git a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json index 7243cbe9383..172580f563e 100644 --- a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json +++ b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json @@ -57,10 +57,10 @@ "nodeId": 39, "index": 0, "installerIcon": 1536, - "userIcon": 1536 + "userIcon": 1536, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json index d17385f7d1e..f89fce5561e 100644 --- a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json +++ b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json @@ -44,9 +44,14 @@ "neighbors": [1, 2], "interviewAttempts": 1, "endpoints": [ - { "nodeId": 6, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + { + "nodeId": 6, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "commandClasses": [] + } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json index bc19c034099..ab80b46069c 100644 --- a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json +++ b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json @@ -58,7 +58,153 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": true + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": true + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": true + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 3, + "isSecure": true + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 100, + "name": "Humidity Control Setpoint", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 109, + "name": "Humidity Control Mode", + "version": 2, + "isSecure": true + }, + { + "id": 110, + "name": "Humidity Control Operating State", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 7, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -3940,152 +4086,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 11, - "isSecure": true - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": true - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": true - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": true - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 3, - "isSecure": true - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": true - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 100, - "name": "Humidity Control Setpoint", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 109, - "name": "Humidity Control Mode", - "version": 2, - "isSecure": true - }, - { - "id": 110, - "name": "Humidity Control Operating State", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 7, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0190:0x0006:0x0001:1.44", "statistics": { diff --git a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json index cb8e78881df..8a88c1fc6e2 100644 --- a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json +++ b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json @@ -67,62 +67,6 @@ "neighbors": [1, 14], "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 70, - "name": "Climate Control Schedule", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 132, - "name": "Wake Up", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 143, - "name": "Multi Command", - "version": 1, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 5, diff --git a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json index dfca647ae67..7025c27182f 100644 --- a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json +++ b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json @@ -72,7 +72,8 @@ "nodeId": 8, "index": 0, "installerIcon": 4608, - "userIcon": 4608 + "userIcon": 4608, + "commandClasses": [] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json index c99898fb595..d1a5fb8c1ee 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json @@ -66,7 +66,99 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 26, @@ -1348,98 +1440,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 3, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - } - ], "interviewStage": "Complete", "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json index 61b138ebbe7..5c95fe1ffc0 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json @@ -68,7 +68,105 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 1, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] }, { "nodeId": 74, @@ -1148,104 +1246,6 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 3, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 1, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 3, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json index da6876dceaa..bd3bf2d560e 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json @@ -66,7 +66,8 @@ "nodeId": 24, "index": 0, "installerIcon": 4608, - "userIcon": 4609 + "userIcon": 4609, + "commandClasses": [] }, { "nodeId": 24, @@ -93,7 +94,6 @@ "userIcon": 3329 } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json index d5f540e8343..cbd25aa4ffd 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json @@ -62,7 +62,93 @@ "endpoints": [ { "nodeId": 8, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 8, @@ -741,91 +827,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index ca0efb56711..21b8a7457eb 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1316,121 +1316,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 5, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json index ba87b585b3c..98a0fab8dbb 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json @@ -54,7 +54,93 @@ "endpoints": [ { "nodeId": 4, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 4, @@ -873,91 +959,5 @@ "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 2, - "isSecure": false - }, - { - "id": 66, - "name": "Thermostat Operating State", - "version": 2, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 2, - "isSecure": false - }, - { - "id": 68, - "name": "Thermostat Fan Mode", - "version": 1, - "isSecure": false - }, - { - "id": 69, - "name": "Thermostat Fan State", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json index cd6ceb2f192..48e692b2395 100644 --- a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json @@ -63,7 +63,93 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": true + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": true + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -386,92 +472,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": true - }, - { - "id": 43, - "name": "Scene Activation", - "version": 1, - "isSecure": true - }, - { - "id": 44, - "name": "Scene Actuator Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": true - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1", "isControllerNode": false diff --git a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json index 4e50345195b..54b976a94a6 100644 --- a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json +++ b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json @@ -74,7 +74,75 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 2, + "isSecure": false + }, + { + "id": 145, + "name": "Manufacturer Proprietary", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -1029,74 +1097,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 3, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 2, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 2, - "isSecure": false - }, - { - "id": 145, - "name": "Manufacturer Proprietary", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x010f:0x0302:0x1000:25.25", "statistics": { diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json index f1e08bf7795..d71f719bc3e 100644 --- a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json @@ -54,10 +54,10 @@ "nodeId": 54, "index": 0, "installerIcon": 6400, - "userIcon": 6400 + "userIcon": 6400, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json index 4c9320085c3..015e4b91cd5 100644 --- a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json @@ -57,7 +57,81 @@ }, "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 3, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 4, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] } ], "values": [ @@ -792,80 +866,6 @@ "mandatorySupportedCCs": [32, 38, 37, 114, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 3, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 4, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 5, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0159:0x0003:0x0052:71.0", "statistics": { diff --git a/tests/components/zwave_js/fixtures/cover_zw062_state.json b/tests/components/zwave_js/fixtures/cover_zw062_state.json index a2033e30bd6..8e819faa347 100644 --- a/tests/components/zwave_js/fixtures/cover_zw062_state.json +++ b/tests/components/zwave_js/fixtures/cover_zw062_state.json @@ -62,10 +62,10 @@ "nodeId": 12, "index": 0, "installerIcon": 7680, - "userIcon": 7680 + "userIcon": 7680, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json index 23f8628b6d3..6885c1aa342 100644 --- a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json +++ b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json @@ -55,10 +55,10 @@ "nodeId": 19, "index": 0, "installerIcon": 1536, - "userIcon": 1536 + "userIcon": 1536, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json index 225f532dfb8..9633b84c394 100644 --- a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json +++ b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json @@ -47,10 +47,10 @@ "endpoints": [ { "nodeId": 2, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json index ea267d86b8c..502dd573420 100644 --- a/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json +++ b/tests/components/zwave_js/fixtures/express_controls_ezmultipli_state.json @@ -60,7 +60,87 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 6, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 2, + "isSecure": false + }, + { + "id": 119, + "name": "Node Naming and Location", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -578,86 +658,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 3, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 6, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 2, - "isSecure": false - }, - { - "id": 119, - "name": "Node Naming and Location", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x001e:0x0004:0x0001:1.8", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json index a1fa0294fd5..59aff4035da 100644 --- a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json +++ b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json @@ -47,10 +47,10 @@ "endpoints": [ { "nodeId": 24, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "endpoint": 0, diff --git a/tests/components/zwave_js/fixtures/fan_generic_state.json b/tests/components/zwave_js/fixtures/fan_generic_state.json index a13b99d882f..29f49bb50dc 100644 --- a/tests/components/zwave_js/fixtures/fan_generic_state.json +++ b/tests/components/zwave_js/fixtures/fan_generic_state.json @@ -57,10 +57,10 @@ "nodeId": 17, "index": 0, "installerIcon": 1024, - "userIcon": 1024 + "userIcon": 1024, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Multilevel Switch", diff --git a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json index a47904a6833..d8ae5fc899a 100644 --- a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json +++ b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json @@ -63,7 +63,105 @@ }, "mandatorySupportedCCs": [32, 38, 133, 89, 114, 115, 134, 94], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 44, + "name": "Scene Actuator Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -9859,104 +9957,6 @@ "mandatorySupportedCCs": [32, 38, 133, 89, 114, 115, 134, 94], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 43, - "name": "Scene Activation", - "version": 1, - "isSecure": false - }, - { - "id": 44, - "name": "Scene Actuator Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x000c:0x0203:0x0001:50.5", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json index f24f611ebe9..98f26c9a669 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json @@ -62,7 +62,39 @@ }, "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -306,38 +338,6 @@ "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0313:0x010b:1.11", "statistics": { diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json index 5768510fb3d..86d04a3fa59 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json @@ -56,7 +56,39 @@ }, "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -298,38 +330,6 @@ "mandatorySupportedCCs": [32, 38], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 2, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0084:0x0331:0x010b:1.11", "statistics": { diff --git a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json index b5986aaf35d..2dbbadcf138 100644 --- a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json +++ b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json @@ -65,116 +65,116 @@ "aggregatedEndpointCount": 0, "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 3, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 5, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 135, - "name": "Indicator", - "version": 3, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 19, "index": 0, "installerIcon": 7168, - "userIcon": 7168 + "userIcon": 7168, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 19, diff --git a/tests/components/zwave_js/fixtures/light_color_null_values_state.json b/tests/components/zwave_js/fixtures/light_color_null_values_state.json index 6f4055a66fa..92e7e4ef30c 100644 --- a/tests/components/zwave_js/fixtures/light_color_null_values_state.json +++ b/tests/components/zwave_js/fixtures/light_color_null_values_state.json @@ -83,7 +83,75 @@ }, "mandatorySupportedCCs": [32], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + } + ] } ], "values": [ @@ -616,73 +684,5 @@ "mandatorySupportedCCs": [32], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 38, - "name": "Multilevel Switch", - "version": 2, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json index 2b092b9d3b0..07c4a441d02 100644 --- a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json +++ b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json @@ -58,10 +58,10 @@ "nodeId": 6, "index": 0, "installerIcon": 768, - "userIcon": 768 + "userIcon": 768, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Door Lock", diff --git a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json index 5e64724ba3b..3e3e9a7e0df 100644 --- a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json +++ b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json @@ -48,7 +48,87 @@ "nodeId": 60, "index": 0, "installerIcon": 768, - "userIcon": 768 + "userIcon": 768, + "commandClasses": [ + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -2836,85 +2916,5 @@ "mandatorySupportedCCs": [32, 98, 99, 114, 152, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 98, - "name": "Door Lock", - "version": 2, - "isSecure": true - }, - { - "id": 99, - "name": "User Code", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 4, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json index 9ae86f1d581..e0d583764cf 100644 --- a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json +++ b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json @@ -35,7 +35,87 @@ }, "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 48, + "name": "Binary Sensor", + "version": 2, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": true + }, + { + "id": 98, + "name": "Door Lock", + "version": 2, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 5, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] } ], "values": [ @@ -476,86 +556,6 @@ "mandatorySupportedCCs": [113, 133, 98, 114, 152, 134], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 48, - "name": "Binary Sensor", - "version": 2, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": true - }, - { - "id": 98, - "name": "Door Lock", - "version": 2, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 5, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0154:0x0005:0x0001:1.3", "statistics": { diff --git a/tests/components/zwave_js/fixtures/multisensor_6_state.json b/tests/components/zwave_js/fixtures/multisensor_6_state.json index 62535414b5b..580393ae6cd 100644 --- a/tests/components/zwave_js/fixtures/multisensor_6_state.json +++ b/tests/components/zwave_js/fixtures/multisensor_6_state.json @@ -61,10 +61,10 @@ "nodeId": 52, "index": 0, "installerIcon": 3079, - "userIcon": 3079 + "userIcon": 3079, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json index c2a2802d273..73515b1c2ac 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json @@ -18,10 +18,10 @@ "endpoints": [ { "nodeId": 67, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Basic", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json index 48885802751..8491e65c037 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_removed_event.json @@ -53,10 +53,10 @@ "endpoints": [ { "nodeId": 67, - "index": 0 + "index": 0, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json index 912cbe30574..a0cd7867b1a 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json @@ -61,10 +61,10 @@ "nodeId": 67, "index": 0, "installerIcon": 4608, - "userIcon": 4608 + "userIcon": 4608, + "commandClasses": [] } ], - "commandClasses": [], "values": [ { "commandClassName": "Manufacturer Specific", diff --git a/tests/components/zwave_js/fixtures/null_name_check_state.json b/tests/components/zwave_js/fixtures/null_name_check_state.json index b283041c3c6..b0ee80b146b 100644 --- a/tests/components/zwave_js/fixtures/null_name_check_state.json +++ b/tests/components/zwave_js/fixtures/null_name_check_state.json @@ -31,7 +31,81 @@ "nodeId": 10, "index": 0, "installerIcon": 3328, - "userIcon": 3328 + "userIcon": 3328, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 10, @@ -337,79 +411,5 @@ "mandatorySupportedCCs": [32, 49], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 7, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 3, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json index 836cb20cf34..ac5232d55e0 100644 --- a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json +++ b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json @@ -53,36 +53,36 @@ "neighbors": [1, 5, 10, 12, 13, 14, 15, 18, 21], "interviewAttempts": 1, "interviewStage": 7, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 64, - "name": "Thermostat Mode", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - } - ], "endpoints": [ { "nodeId": 20, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + } + ] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json index c510c60a479..4878a26beab 100644 --- a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json +++ b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json @@ -60,7 +60,39 @@ }, "mandatorySupportedCCs": [32, 37, 39], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + } + ] }, { "nodeId": 7, @@ -363,37 +395,5 @@ "mandatorySupportedCCs": [32, 37, 39], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 3, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zen_31_state.json b/tests/components/zwave_js/fixtures/zen_31_state.json index 982e96d9adf..0d307154359 100644 --- a/tests/components/zwave_js/fixtures/zen_31_state.json +++ b/tests/components/zwave_js/fixtures/zen_31_state.json @@ -66,7 +66,129 @@ 32, 38, 133, 89, 51, 90, 114, 115, 159, 108, 85, 134, 94 ], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 3, + "isSecure": false + }, + { + "id": 86, + "name": "CRC-16 Encapsulation", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] }, { "nodeId": 94, @@ -2587,127 +2709,5 @@ ], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 11, - "isSecure": false - }, - { - "id": 50, - "name": "Meter", - "version": 3, - "isSecure": false - }, - { - "id": 51, - "name": "Color Switch", - "version": 3, - "isSecure": false - }, - { - "id": 86, - "name": "CRC-16 Encapsulation", - "version": 1, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 91, - "name": "Central Scene", - "version": 3, - "isSecure": false - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 4, - "isSecure": false - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 8, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 117, - "name": "Protection", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 4, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": false - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ], "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json index 1c4805b5c22..4e7d5f6a9dc 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -26,7 +26,8 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [] } ], "values": [], @@ -53,7 +54,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [], "interviewStage": "ProtocolInfo", "statistics": { "commandsTX": 0, diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json index c9d37b74c29..54f37d389dd 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -63,7 +63,87 @@ }, "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 7, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + } + ] } ], "values": [ @@ -607,86 +687,6 @@ "mandatorySupportedCCs": [], "mandatoryControlledCCs": [] }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 2, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": false - }, - { - "id": 89, - "name": "Association Group Information", - "version": 1, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": false - }, - { - "id": 113, - "name": "Notification", - "version": 4, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 7, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 132, - "name": "Wake Up", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 2, - "isSecure": false - } - ], "interviewStage": "Complete", "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0109:0x2021:0x2101:5.1", "statistics": { diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 57f552c9502..d038949d494 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -211,8 +211,8 @@ async def test_on_node_added_not_ready( client.driver.receive_event(event) await hass.async_block_till_done() - # the only entities are the node status sensor, ping button, and firmware update - assert len(hass.states.async_all()) == 3 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -254,8 +254,8 @@ async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integrati assert not device.model assert not device.sw_version - # the only entities are the node status sensor, ping button, and firmware update - assert len(hass.states.async_all()) == 3 + # the only entities are the node status sensor and ping button + assert len(hass.states.async_all()) == 2 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index c9ec8fa68c6..76fecfdee6d 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -19,9 +19,9 @@ from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import datetime as dt_util +from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed UPDATE_ENTITY = "update.z_wave_thermostat_firmware" FIRMWARE_UPDATES = { @@ -162,14 +162,12 @@ async def test_update_entity_success( client.async_send_command.reset_mock() -async def test_update_entity_failure( +async def test_update_entity_install_failure( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, controller_node, integration, - caplog, - hass_ws_client, ): """Test update entity failed install.""" client.async_send_command.return_value = FIRMWARE_UPDATES @@ -194,15 +192,15 @@ async def test_update_entity_failure( async def test_update_entity_sleep( hass, client, - multisensor_6, + zen_31, integration, ): """Test update occurs when device is asleep after it wakes up.""" event = Event( "sleep", - data={"source": "node", "event": "sleep", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "sleep", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) client.async_send_command.reset_mock() client.async_send_command.return_value = FIRMWARE_UPDATES @@ -215,9 +213,9 @@ async def test_update_entity_sleep( event = Event( "wake up", - data={"source": "node", "event": "wake up", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "wake up", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) await hass.async_block_till_done() # Now that the node is up we can check for updates @@ -225,21 +223,21 @@ async def test_update_entity_sleep( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == multisensor_6.node_id + assert args["nodeId"] == zen_31.node_id async def test_update_entity_dead( hass, client, - multisensor_6, + zen_31, integration, ): """Test update occurs when device is dead after it becomes alive.""" event = Event( "dead", - data={"source": "node", "event": "dead", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "dead", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) client.async_send_command.reset_mock() client.async_send_command.return_value = FIRMWARE_UPDATES @@ -252,9 +250,9 @@ async def test_update_entity_dead( event = Event( "alive", - data={"source": "node", "event": "alive", "nodeId": multisensor_6.node_id}, + data={"source": "node", "event": "alive", "nodeId": zen_31.node_id}, ) - multisensor_6.receive_event(event) + zen_31.receive_event(event) await hass.async_block_till_done() # Now that the node is up we can check for updates @@ -262,4 +260,54 @@ async def test_update_entity_dead( args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "controller.get_available_firmware_updates" - assert args["nodeId"] == multisensor_6.node_id + assert args["nodeId"] == zen_31.node_id + + +async def test_update_entity_ha_not_running( + hass, + client, + zen_31, + hass_ws_client, +): + """Test update occurs after HA starts.""" + await hass.async_stop() + + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(client.async_send_command.call_args_list) == 0 + + await hass.async_start() + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert args["nodeId"] == zen_31.node_id + + +async def test_update_entity_failure( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, + integration, +): + """Test update entity update failed.""" + assert len(client.async_send_command.call_args_list) == 0 + client.async_send_command.side_effect = FailedZWaveCommand("test", 260, "test") + + 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_OFF + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "controller.get_available_firmware_updates" + assert ( + args["nodeId"] + == climate_radio_thermostat_ct100_plus_different_endpoints.node_id + ) From 1dbcf88e156d44865ac0a4dd84511dc21b6ef194 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Tue, 6 Sep 2022 10:52:27 +0300 Subject: [PATCH 073/231] Bump pybravia to 0.2.2 (#77867) --- homeassistant/components/braviatv/coordinator.py | 12 +++++++++++- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index bdacddcdb2f..49c902e0d44 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -7,7 +7,13 @@ from functools import wraps import logging from typing import Any, Final, TypeVar -from pybravia import BraviaTV, BraviaTVError, BraviaTVNotFound +from pybravia import ( + BraviaTV, + BraviaTVConnectionError, + BraviaTVConnectionTimeout, + BraviaTVError, + BraviaTVNotFound, +) from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player.const import ( @@ -130,6 +136,10 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err + except (BraviaTVConnectionError, BraviaTVConnectionTimeout): + self.is_on = False + self.connected = False + _LOGGER.debug("Update skipped, Bravia TV is off") except BraviaTVError as err: self.is_on = False self.connected = False diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index fa172957781..dca9d65cff0 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.1"], + "requirements": ["pybravia==0.2.2"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 18612e9ee5c..d3637791e7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.1 +pybravia==0.2.2 # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b6edc09648..e34022971fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1019,7 +1019,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.1 +pybravia==0.2.2 # homeassistant.components.cloudflare pycfdns==1.2.2 From e1e153f391633a41955566454783ee60b168f312 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 08:48:39 -0500 Subject: [PATCH 074/231] Bump bluetooth-auto-recovery to 0.3.1 (#77898) --- 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 b98312040f0..ca6a76c55ae 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.3.4", - "bluetooth-auto-recovery==0.3.0" + "bluetooth-auto-recovery==0.3.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1299abcd61b..09102494074 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 bluetooth-adapters==0.3.4 -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index d3637791e7c..0ab025b66c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e34022971fd..13c3ad85d9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.0 +bluetooth-auto-recovery==0.3.1 # homeassistant.components.bond bond-async==0.1.22 From 9155f669e9f39f5f1ab4b8c0f7c40cc7c76f71ee Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 Sep 2022 18:54:53 +0200 Subject: [PATCH 075/231] Update frontend to 20220906.0 (#77910) --- 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 416634053d6..8abc8fd4e32 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==20220905.0"], + "requirements": ["home-assistant-frontend==20220906.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 09102494074..58a000fbc25 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 0ab025b66c5..ed85d669deb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13c3ad85d9c..81a71d5df1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220905.0 +home-assistant-frontend==20220906.0 # homeassistant.components.home_connect homeconnect==0.7.2 From c8ad8a6d86323482e479e5aa17ca267f9054b5e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 Sep 2022 12:55:44 -0400 Subject: [PATCH 076/231] Bumped version to 2022.9.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 c4c15302a44..156ba1d133d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -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 0a8db19d4ee..1fdc8d87e07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b5" +version = "2022.9.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d1b637ea7a17d75cc3c6936c909c19f5de839893 Mon Sep 17 00:00:00 2001 From: Matthew Simpson Date: Tue, 6 Sep 2022 20:50:03 +0100 Subject: [PATCH 077/231] Bump btsmarthub_devicelist to 0.2.2 (#77609) --- homeassistant/components/bt_smarthub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 6a0453752e9..fb34117eb6b 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -2,7 +2,7 @@ "domain": "bt_smarthub", "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", - "requirements": ["btsmarthub_devicelist==0.2.0"], + "requirements": ["btsmarthub_devicelist==0.2.2"], "codeowners": ["@jxwolstenholme"], "iot_class": "local_polling", "loggers": ["btsmarthub_devicelist"] diff --git a/requirements_all.txt b/requirements_all.txt index ed85d669deb..63eee62c878 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -470,7 +470,7 @@ bthome-ble==1.0.0 bthomehub5-devicelist==0.1.1 # homeassistant.components.bt_smarthub -btsmarthub_devicelist==0.2.0 +btsmarthub_devicelist==0.2.2 # homeassistant.components.buienradar buienradar==1.0.5 From 9aa87761cf5c9a7f1fc729cbc6809bf7c7791f02 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 7 Sep 2022 11:10:24 -0400 Subject: [PATCH 078/231] Fix ZHA lighting initial hue/saturation attribute read (#77727) * Handle the case of `current_hue` being `None` * WIP unit tests --- homeassistant/components/zha/light.py | 18 +++--- tests/components/zha/common.py | 21 +++++- tests/components/zha/test_light.py | 93 ++++++++++++++++++++++++--- 3 files changed, 114 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 5a8011e2386..9858c6803f9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -610,16 +610,18 @@ class Light(BaseLight, ZhaEntity): and self._color_channel.enhanced_current_hue is not None ): curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 - else: + elif self._color_channel.current_hue is not None: curr_hue = self._color_channel.current_hue * 254 / 360 - curr_saturation = self._color_channel.current_saturation - if curr_hue is not None and curr_saturation is not None: - self._attr_hs_color = ( - int(curr_hue), - int(curr_saturation * 2.54), - ) else: - self._attr_hs_color = (0, 0) + curr_hue = 0 + + if (curr_saturation := self._color_channel.current_saturation) is None: + curr_saturation = 0 + + self._attr_hs_color = ( + int(curr_hue), + int(curr_saturation * 2.54), + ) if self._color_channel.color_loop_supported: self._attr_supported_features |= light.LightEntityFeature.EFFECT diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index cad8020267f..56197fa39ec 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -2,12 +2,14 @@ import asyncio from datetime import timedelta import math -from unittest.mock import AsyncMock, Mock +from typing import Any +from unittest.mock import AsyncMock, Mock, patch import zigpy.zcl import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const +from homeassistant.components.zha.core.helpers import async_get_zha_config_value from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util @@ -243,3 +245,20 @@ async def async_shift_time(hass): next_update = dt_util.utcnow() + timedelta(seconds=11) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + + +def patch_zha_config(component: str, overrides: dict[tuple[str, str], Any]): + """Patch the ZHA custom configuration defaults.""" + + def new_get_config(config_entry, section, config_key, default): + if (section, config_key) in overrides: + return overrides[section, config_key] + else: + return async_get_zha_config_value( + config_entry, section, config_key, default + ) + + return patch( + f"homeassistant.components.zha.{component}.async_get_zha_config_value", + side_effect=new_get_config, + ) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 156f692aa14..16678393f52 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,6 +14,10 @@ from homeassistant.components.light import ( FLASH_SHORT, ColorMode, ) +from homeassistant.components.zha.core.const import ( + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + ZHA_OPTIONS, +) from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform @@ -26,6 +30,7 @@ from .common import ( async_test_rejoin, find_entity_id, get_zha_gateway, + patch_zha_config, send_attributes_report, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -342,7 +347,11 @@ async def test_light( if cluster_identify: await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_SHORT) - # test turning the lights on and off from the HA + # test long flashing the lights from the HA + if cluster_identify: + await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + + # test dimming the lights on and off from the HA if cluster_level: await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id @@ -357,16 +366,82 @@ async def test_light( # test rejoin await async_test_off_from_hass(hass, cluster_on_off, entity_id) - clusters = [cluster_on_off] - if cluster_level: - clusters.append(cluster_level) - if cluster_color: - clusters.append(cluster_color) + clusters = [c for c in (cluster_on_off, cluster_level, cluster_color) if c] await async_test_rejoin(hass, zigpy_device, clusters, reporting) - # test long flashing the lights from the HA - if cluster_identify: - await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) + +@pytest.mark.parametrize( + "plugged_attr_reads, config_override, expected_state", + [ + # HS light without cached hue or saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached hue + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with cached saturation + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + # HS light with both + ( + { + "color_capabilities": ( + lighting.Color.ColorCapabilities.Hue_and_saturation + ), + "current_hue": 100, + "current_saturation": 100, + }, + {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, + {}, + ), + ], +) +async def test_light_initialization( + hass, + zigpy_device_mock, + zha_device_joined_restored, + plugged_attr_reads, + config_override, + expected_state, +): + """Test zha light initialization with cached attributes and color modes.""" + + # create zigpy devices + zigpy_device = zigpy_device_mock(LIGHT_COLOR) + + # mock attribute reads + zigpy_device.endpoints[1].light_color.PLUGGED_ATTR_READS = plugged_attr_reads + + with patch_zha_config("light", config_override): + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(Platform.LIGHT, zha_device, hass) + + assert entity_id is not None + + # TODO ensure hue and saturation are properly set on startup @patch( From a4f528e90855884933d15a0e9d8c14c6406117ce Mon Sep 17 00:00:00 2001 From: Chris McCurdy Date: Wed, 7 Sep 2022 11:43:05 -0400 Subject: [PATCH 079/231] Add additional method of retrieving UUID for LG soundbar configuration (#77856) --- .../components/lg_soundbar/config_flow.py | 19 +- .../lg_soundbar/test_config_flow.py | 168 +++++++++++++++++- 2 files changed, 182 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py index bd9a727d1f4..0606bad2d67 100644 --- a/homeassistant/components/lg_soundbar/config_flow.py +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the LG Soundbar integration.""" -from queue import Queue +from queue import Full, Queue import socket import temescal @@ -20,18 +20,29 @@ def test_connect(host, port): uuid_q = Queue(maxsize=1) name_q = Queue(maxsize=1) + def queue_add(attr_q, data): + try: + attr_q.put_nowait(data) + except Full: + pass + def msg_callback(response): - if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]: - uuid_q.put_nowait(response["data"]["s_uuid"]) + if ( + response["msg"] in ["MAC_INFO_DEV", "PRODUCT_INFO"] + and "s_uuid" in response["data"] + ): + queue_add(uuid_q, response["data"]["s_uuid"]) if ( response["msg"] == "SPK_LIST_VIEW_INFO" and "s_user_name" in response["data"] ): - name_q.put_nowait(response["data"]["s_user_name"]) + queue_add(name_q, response["data"]["s_user_name"]) try: connection = temescal.temescal(host, port=port, callback=msg_callback) connection.get_mac_info() + if uuid_q.empty(): + connection.get_product_info() connection.get_info() details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} return details diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py index 3fafc2c7628..8bcf817cbba 100644 --- a/tests/components/lg_soundbar/test_config_flow.py +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -1,5 +1,5 @@ """Test the lg_soundbar config flow.""" -from unittest.mock import MagicMock, patch +from unittest.mock import DEFAULT, MagicMock, Mock, call, patch from homeassistant import config_entries from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN @@ -43,6 +43,172 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_uuid_missing_from_mac_info(hass): + """Test we get the form, but uuid is missing from the initial get_mac_info function call.""" + + 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.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} + instance.get_product_info.side_effect = lambda: callback(product_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_uuid_present_in_both_functions_uuid_q_empty(hass): + """Get the form, uuid present in both get_mac_info and get_product_info calls. + + Value from get_mac_info is not added to uuid_q before get_product_info is run. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_uuid_q = MagicMock() + mock_name_q = MagicMock() + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.config_flow.Queue", + return_value=MagicMock(), + ) as mock_q, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_q.side_effect = [mock_uuid_q, mock_name_q] + mock_uuid_q.empty.return_value = True + mock_uuid_q.get.return_value = "uuid" + mock_name_q.get.return_value = "name" + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} + instance.get_mac_info.side_effect = lambda: callback(mac_info) + product_info = {"msg": "PRODUCT_INFO", "data": {"s_uuid": "uuid"}} + instance.get_product_info.side_effect = lambda: callback(product_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + mock_uuid_q.empty.assert_called_once() + mock_uuid_q.put_nowait.has_calls([call("uuid"), call("uuid")]) + mock_uuid_q.get.assert_called_once() + + +async def test_form_uuid_present_in_both_functions_uuid_q_not_empty(hass): + """Get the form, uuid present in both get_mac_info and get_product_info calls. + + Value from get_mac_info is added to uuid_q before get_product_info is run. + """ + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_uuid_q = MagicMock() + mock_name_q = MagicMock() + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", return_value=Mock() + ) as mock_temescal, patch( + "homeassistant.components.lg_soundbar.config_flow.Queue", + return_value=MagicMock(), + ) as mock_q, patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_q.side_effect = [mock_uuid_q, mock_name_q] + mock_uuid_q.empty.return_value = False + mock_uuid_q.get.return_value = "uuid" + mock_name_q.get.return_value = "name" + tmock = mock_temescal.temescal + tmock.return_value = Mock() + instance = tmock.return_value + + def temescal_side_effect(addr, port, callback): + mac_info = {"msg": "MAC_INFO_DEV", "data": {"s_uuid": "uuid"}} + instance.get_mac_info.side_effect = lambda: callback(mac_info) + info = {"msg": "SPK_LIST_VIEW_INFO", "data": {"s_user_name": "name"}} + instance.get_info.side_effect = lambda: callback(info) + return DEFAULT + + tmock.side_effect = temescal_side_effect + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + mock_uuid_q.empty.assert_called_once() + mock_uuid_q.put_nowait.assert_called_once() + mock_uuid_q.get.assert_called_once() + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( From 9901b31316f591ffd5840c2924c536751009f1f5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 7 Sep 2022 01:28:47 -0400 Subject: [PATCH 080/231] Bump zwave-js-server-python to 0.41.1 (#77915) * Bump zwave-js-server-python to 0.41.1 * Fix fixture --- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/climate_adc_t3000_state.json | 112 +++++------------- 4 files changed, 31 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index b906efec96c..7c569301831 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.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.41.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 63eee62c878..7a945fc9deb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2596,7 +2596,7 @@ zigpy==0.50.2 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.0 +zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81a71d5df1d..6a76db50147 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1782,7 +1782,7 @@ zigpy-znp==0.8.2 zigpy==0.50.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.41.0 +zwave-js-server-python==0.41.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json index ab80b46069c..b6a235ad45e 100644 --- a/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json +++ b/tests/components/zwave_js/fixtures/climate_adc_t3000_state.json @@ -227,9 +227,7 @@ "unit": "\u00b0F" }, "value": 72, - "nodeId": 68, - "newValue": 73, - "prevValue": 72.5 + "nodeId": 68 }, { "endpoint": 0, @@ -250,9 +248,7 @@ "unit": "%" }, "value": 34, - "nodeId": 68, - "newValue": 34, - "prevValue": 34 + "nodeId": 68 }, { "endpoint": 0, @@ -448,9 +444,7 @@ "8": "Quiet circulation mode" } }, - "value": 0, - "newValue": 1, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -581,9 +575,7 @@ "2": "De-humidifying" } }, - "value": 0, - "newValue": 1, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1295,9 +1287,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1323,9 +1313,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1351,9 +1339,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1379,9 +1365,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1407,9 +1391,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 0 + "value": 1 }, { "endpoint": 0, @@ -1435,9 +1417,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1463,9 +1443,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1491,9 +1469,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1519,9 +1495,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1547,9 +1521,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1575,9 +1547,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1603,9 +1573,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1631,9 +1599,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1659,9 +1625,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1687,9 +1651,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1715,9 +1677,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1743,9 +1703,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1771,9 +1729,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1799,9 +1755,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1827,9 +1781,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1855,9 +1807,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1883,9 +1833,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 1, - "newValue": 1, - "prevValue": 1 + "value": 1 }, { "endpoint": 0, @@ -1911,9 +1859,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, @@ -1939,9 +1885,7 @@ "allowManualEntry": false, "isFromConfig": true }, - "value": 0, - "newValue": 0, - "prevValue": 0 + "value": 0 }, { "endpoint": 0, From 8d0ebdd1f97080bc7fc4a240336f5977bebc935a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 6 Sep 2022 20:13:01 +0200 Subject: [PATCH 081/231] Revert "Add ability to ignore devices for UniFi Protect" (#77916) --- .../components/unifiprotect/__init__.py | 30 ++----- .../components/unifiprotect/config_flow.py | 78 +++++++------------ .../components/unifiprotect/const.py | 1 - homeassistant/components/unifiprotect/data.py | 65 +--------------- .../components/unifiprotect/services.py | 6 +- .../components/unifiprotect/strings.json | 6 +- .../unifiprotect/translations/en.json | 4 - .../components/unifiprotect/utils.py | 46 ++++------- tests/components/unifiprotect/conftest.py | 1 - .../unifiprotect/test_config_flow.py | 45 +---------- tests/components/unifiprotect/test_init.py | 39 +++------- tests/components/unifiprotect/utils.py | 12 +-- 12 files changed, 75 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 60829223e2f..30b1d1ad56d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -26,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( CONF_ALL_UPDATES, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DEFAULT_SCAN_INTERVAL, DEVICES_FOR_SUBSCRIBE, @@ -36,11 +35,11 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData +from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services -from .utils import async_unifi_mac, convert_mac_list +from .utils import _async_unifi_mac_from_hass, async_get_devices from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -107,19 +106,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" - - data: ProtectData = hass.data[DOMAIN][entry.entry_id] - changed = data.async_get_changed_options(entry) - - if len(changed) == 1 and CONF_IGNORED in changed: - new_macs = convert_mac_list(entry.options.get(CONF_IGNORED, "")) - added_macs = new_macs - data.ignored_macs - removed_macs = data.ignored_macs - new_macs - # if only ignored macs are added, we can handle without reloading - if not removed_macs and added_macs: - data.async_add_new_ignored_macs(added_macs) - return - await hass.config_entries.async_reload(entry.entry_id) @@ -139,15 +125,15 @@ async def async_remove_config_entry_device( ) -> bool: """Remove ufp config entry from a device.""" unifi_macs = { - async_unifi_mac(connection[1]) + _async_unifi_mac_from_hass(connection[1]) for connection in device_entry.connections if connection[0] == dr.CONNECTION_NETWORK_MAC } - data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] - if data.api.bootstrap.nvr.mac in unifi_macs: + api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) + assert api is not None + if api.bootstrap.nvr.mac in unifi_macs: return False - for device in data.get_by_types(DEVICES_THAT_ADOPT): + for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT): if device.is_adopted_by_us and device.mac in unifi_macs: - data.async_ignore_mac(device.mac) - break + return False return True diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 1907a201c8d..f07ca923a53 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -35,7 +35,6 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, DEFAULT_MAX_MEDIA, @@ -47,7 +46,7 @@ from .const import ( ) from .data import async_last_update_was_successful from .discovery import async_start_discovery -from .utils import _async_resolve, async_short_mac, async_unifi_mac, convert_mac_list +from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) @@ -121,7 +120,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle integration discovery.""" self._discovered_device = discovery_info - mac = async_unifi_mac(discovery_info["hw_addr"]) + mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"]) await self.async_set_unique_id(mac) source_ip = discovery_info["source_ip"] direct_connect_domain = discovery_info["direct_connect_domain"] @@ -183,7 +182,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = { "name": discovery_info["hostname"] or discovery_info["platform"] - or f"NVR {async_short_mac(discovery_info['hw_addr'])}", + or f"NVR {_async_short_mac(discovery_info['hw_addr'])}", "ip_address": discovery_info["source_ip"], } self.context["title_placeholders"] = placeholders @@ -225,7 +224,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, - CONF_IGNORED: "", }, ) @@ -367,53 +365,33 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - - values = user_input or self.config_entry.options - schema = vol.Schema( - { - vol.Optional( - CONF_DISABLE_RTSP, - description={ - "suggested_value": values.get(CONF_DISABLE_RTSP, False) - }, - ): bool, - vol.Optional( - CONF_ALL_UPDATES, - description={ - "suggested_value": values.get(CONF_ALL_UPDATES, False) - }, - ): bool, - vol.Optional( - CONF_OVERRIDE_CHOST, - description={ - "suggested_value": values.get(CONF_OVERRIDE_CHOST, False) - }, - ): bool, - vol.Optional( - CONF_MAX_MEDIA, - description={ - "suggested_value": values.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) - }, - ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), - vol.Optional( - CONF_IGNORED, - description={"suggested_value": values.get(CONF_IGNORED, "")}, - ): str, - } - ) - errors: dict[str, str] = {} - if user_input is not None: - try: - convert_mac_list(user_input.get(CONF_IGNORED, ""), raise_exception=True) - except vol.Invalid: - errors[CONF_IGNORED] = "invalid_mac_list" - - if not errors: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(title="", data=user_input) return self.async_show_form( step_id="init", - data_schema=schema, - errors=errors, + data_schema=vol.Schema( + { + vol.Optional( + CONF_DISABLE_RTSP, + default=self.config_entry.options.get(CONF_DISABLE_RTSP, False), + ): bool, + vol.Optional( + CONF_ALL_UPDATES, + default=self.config_entry.options.get(CONF_ALL_UPDATES, False), + ): bool, + vol.Optional( + CONF_OVERRIDE_CHOST, + default=self.config_entry.options.get( + CONF_OVERRIDE_CHOST, False + ), + ): bool, + vol.Optional( + CONF_MAX_MEDIA, + default=self.config_entry.options.get( + CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA + ), + ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), + } + ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 080dc41f358..93a0fa5ff74 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -20,7 +20,6 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" -CONF_IGNORED = "ignored_devices" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index c17b99d639f..20b5747a342 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -28,7 +28,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, @@ -37,11 +36,7 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .utils import ( - async_dispatch_id as _ufpd, - async_get_devices_by_type, - convert_mac_list, -) +from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] @@ -72,7 +67,6 @@ class ProtectData: self._hass = hass self._entry = entry - self._existing_options = dict(entry.options) self._hass = hass self._update_interval = update_interval self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} @@ -80,8 +74,6 @@ class ProtectData: self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None self._auth_failures = 0 - self._ignored_macs: set[str] | None = None - self._ignore_update_cancel: Callable[[], None] | None = None self.last_update_success = False self.api = protect @@ -96,47 +88,6 @@ class ProtectData: """Max number of events to load at once.""" return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) - @property - def ignored_macs(self) -> set[str]: - """Set of ignored MAC addresses.""" - - if self._ignored_macs is None: - self._ignored_macs = convert_mac_list( - self._entry.options.get(CONF_IGNORED, "") - ) - - return self._ignored_macs - - @callback - def async_get_changed_options(self, entry: ConfigEntry) -> dict[str, Any]: - """Get changed options for when entry is updated.""" - - return dict( - set(self._entry.options.items()) - set(self._existing_options.items()) - ) - - @callback - def async_ignore_mac(self, mac: str) -> None: - """Ignores a MAC address for a UniFi Protect device.""" - - new_macs = (self._ignored_macs or set()).copy() - new_macs.add(mac) - _LOGGER.debug("Updating ignored_devices option: %s", self.ignored_macs) - options = dict(self._entry.options) - options[CONF_IGNORED] = ",".join(new_macs) - self._hass.config_entries.async_update_entry(self._entry, options=options) - - @callback - def async_add_new_ignored_macs(self, new_macs: set[str]) -> None: - """Add new ignored MAC addresses and ensures the devices are removed.""" - - for mac in new_macs: - device = self.api.bootstrap.get_device_from_mac(mac) - if device is not None: - self._async_remove_device(device) - self._ignored_macs = None - self._existing_options = dict(self._entry.options) - def get_by_types( self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel, None, None]: @@ -148,8 +99,6 @@ class ProtectData: for device in devices: if ignore_unadopted and not device.is_adopted_by_us: continue - if device.mac in self.ignored_macs: - continue yield device async def async_setup(self) -> None: @@ -159,11 +108,6 @@ class ProtectData: ) await self.async_refresh() - for mac in self.ignored_macs: - device = self.api.bootstrap.get_device_from_mac(mac) - if device is not None: - self._async_remove_device(device) - async def async_stop(self, *args: Any) -> None: """Stop processing data.""" if self._unsub_websocket: @@ -228,7 +172,6 @@ class ProtectData: @callback def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: - registry = dr.async_get(self._hass) device_entry = registry.async_get_device( identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} @@ -353,13 +296,13 @@ class ProtectData: @callback -def async_ufp_data_for_config_entry_ids( +def async_ufp_instance_for_config_entry_ids( hass: HomeAssistant, config_entry_ids: set[str] -) -> ProtectData | None: +) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" domain_data = hass.data[DOMAIN] for config_entry_id in config_entry_ids: if config_entry_id in domain_data: protect_data: ProtectData = domain_data[config_entry_id] - return protect_data + return protect_data.api return None diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 914803e9c45..915c51b6c0a 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -25,7 +25,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids from homeassistant.util.read_only_dict import ReadOnlyDict from .const import ATTR_MESSAGE, DOMAIN -from .data import async_ufp_data_for_config_entry_ids +from .data import async_ufp_instance_for_config_entry_ids SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" @@ -70,8 +70,8 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl return _async_get_ufp_instance(hass, device_entry.via_device_id) config_entry_ids = device_entry.config_entries - if ufp_data := async_ufp_data_for_config_entry_ids(hass, config_entry_ids): - return ufp_data.api + if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): + return ufp_instance raise HomeAssistantError(f"No device found for device id: {device_id}") diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d9750d31ae1..d3cfe24abd2 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -50,13 +50,9 @@ "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", - "max_media": "Max number of event to load for Media Browser (increases RAM usage)", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore" + "max_media": "Max number of event to load for Media Browser (increases RAM usage)" } } - }, - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index c6050d05284..5d690e3fd3e 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -42,15 +42,11 @@ } }, "options": { - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" - }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 8c368da1c40..808117aac9e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,9 +1,9 @@ """UniFi Protect Integration utils.""" from __future__ import annotations +from collections.abc import Generator, Iterable import contextlib from enum import Enum -import re import socket from typing import Any @@ -14,16 +14,12 @@ from pyunifiprotect.data import ( LightModeType, ProtectAdoptableDeviceModel, ) -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv from .const import DOMAIN, ModelType -MAC_RE = re.compile(r"[0-9A-F]{12}") - def get_nested_attr(obj: Any, attr: str) -> Any: """Fetch a nested attribute.""" @@ -42,16 +38,15 @@ def get_nested_attr(obj: Any, attr: str) -> Any: @callback -def async_unifi_mac(mac: str) -> str: - """Convert MAC address to format from UniFi Protect.""" +def _async_unifi_mac_from_hass(mac: str) -> str: # MAC addresses in UFP are always caps - return mac.replace(":", "").replace("-", "").replace("_", "").upper() + return mac.replace(":", "").upper() @callback -def async_short_mac(mac: str) -> str: +def _async_short_mac(mac: str) -> str: """Get the short mac address from the full mac.""" - return async_unifi_mac(mac)[-6:] + return _async_unifi_mac_from_hass(mac)[-6:] async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: @@ -82,6 +77,18 @@ def async_get_devices_by_type( return devices +@callback +def async_get_devices( + bootstrap: Bootstrap, model_type: Iterable[ModelType] +) -> Generator[ProtectAdoptableDeviceModel, None, None]: + """Return all device by type.""" + return ( + device + for device_type in model_type + for device in async_get_devices_by_type(bootstrap, device_type).values() + ) + + @callback def async_get_light_motion_current(obj: Light) -> str: """Get light motion mode for Flood Light.""" @@ -99,22 +106,3 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: """Generate entry specific dispatch ID.""" return f"{DOMAIN}.{entry.entry_id}.{dispatch}" - - -@callback -def convert_mac_list(option: str, raise_exception: bool = False) -> set[str]: - """Convert csv list of MAC addresses.""" - - macs = set() - values = cv.ensure_list_csv(option) - for value in values: - if value == "": - continue - value = async_unifi_mac(value) - if not MAC_RE.match(value): - if raise_exception: - raise vol.Invalid("invalid_mac_list") - continue - macs.add(value) - - return macs diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index fa245e8b1cc..b006dfbd004 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -68,7 +68,6 @@ def mock_ufp_config_entry(): "port": 443, "verify_ssl": False, }, - options={"ignored_devices": "FFFFFFFFFFFF,test"}, version=2, ) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 26a9dd73ee8..d0fb0dba9f2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.components import dhcp, ssdp from homeassistant.components.unifiprotect.const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DOMAIN, ) @@ -270,52 +269,10 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "all_updates": True, "disable_rtsp": True, "override_connection_host": True, + "max_media": 1000, } -async def test_form_options_invalid_mac( - hass: HomeAssistant, ufp_client: ProtectApiClient -) -> None: - """Test we handle options flows.""" - mock_config = MockConfigEntry( - domain=DOMAIN, - data={ - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - "id": "UnifiProtect", - "port": 443, - "verify_ssl": False, - "max_media": 1000, - }, - version=2, - unique_id=dr.format_mac(MAC_ADDR), - ) - mock_config.add_to_hass(hass) - - with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.ProtectApiClient" - ) as mock_api: - mock_api.return_value = ufp_client - - await hass.config_entries.async_setup(mock_config.entry_id) - await hass.async_block_till_done() - assert mock_config.state == config_entries.ConfigEntryState.LOADED - - result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] - assert result["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - {CONF_IGNORED: "test,test2"}, - ) - - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {CONF_IGNORED: "invalid_mac_list"} - - @pytest.mark.parametrize( "source, data", [ diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 7a1e590b87d..9392caa30ac 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -7,21 +7,20 @@ from unittest.mock import AsyncMock, patch import aiohttp from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, Doorlock, Light, Sensor +from pyunifiprotect.data import NVR, Bootstrap, Light from homeassistant.components.unifiprotect.const import ( CONF_DISABLE_RTSP, - CONF_IGNORED, DEFAULT_SCAN_INTERVAL, DOMAIN, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .utils import MockUFPFixture, get_device_from_ufp_device, init_entry, time_changed +from .utils import MockUFPFixture, init_entry, time_changed from tests.common import MockConfigEntry @@ -212,38 +211,28 @@ async def test_device_remove_devices( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, - doorlock: Doorlock, - sensor: Sensor, hass_ws_client: Callable[ [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] ], ) -> None: """Test we can only remove a device that no longer exists.""" - sensor.mac = "FFFFFFFFFFFF" - - await init_entry(hass, ufp, [light, doorlock, sensor], regenerate_ids=False) + await init_entry(hass, ufp, [light]) assert await async_setup_component(hass, "config", {}) - + entity_id = "light.test_light" entry_id = ufp.entry.entry_id + + registry: er.EntityRegistry = er.async_get(hass) + entity = registry.async_get(entity_id) + assert entity is not None device_registry = dr.async_get(hass) - light_device = get_device_from_ufp_device(hass, light) - assert light_device is not None + live_device_entry = device_registry.async_get(entity.device_id) assert ( - await remove_device(await hass_ws_client(hass), light_device.id, entry_id) - is True + await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) + is False ) - doorlock_device = get_device_from_ufp_device(hass, doorlock) - assert ( - await remove_device(await hass_ws_client(hass), doorlock_device.id, entry_id) - is True - ) - - sensor_device = get_device_from_ufp_device(hass, sensor) - assert sensor_device is None - dead_device_entry = device_registry.async_get_or_create( config_entry_id=entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")}, @@ -253,10 +242,6 @@ async def test_device_remove_devices( is True ) - await time_changed(hass, 60) - entry = hass.config_entries.async_get_entry(entry_id) - entry.options[CONF_IGNORED] == f"{light.mac},{doorlock.mac}" - async def test_device_remove_devices_nvr( hass: HomeAssistant, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 3376db4ec51..bee479b8e2b 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityDescription import homeassistant.util.dt as dt_util @@ -229,13 +229,3 @@ async def adopt_devices( ufp.ws_msg(mock_msg) await hass.async_block_till_done() - - -def get_device_from_ufp_device( - hass: HomeAssistant, device: ProtectAdoptableDeviceModel -) -> dr.DeviceEntry | None: - """Return all device by type.""" - registry = dr.async_get(hass) - return registry.async_get_device( - identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} - ) From 2eeab820b724427d20b2f0b1ef436573b2cb1f26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 16:43:18 -0500 Subject: [PATCH 082/231] Bump aiohomekit to 1.5.2 (#77927) --- 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 e3526dd870a..08eac050c98 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==1.5.1"], + "requirements": ["aiohomekit==1.5.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 7a945fc9deb..2c26df6547f 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==1.5.1 +aiohomekit==1.5.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a76db50147..3b6adffe4a5 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==1.5.1 +aiohomekit==1.5.2 # homeassistant.components.emulated_hue # homeassistant.components.http From 941a5e382046d4fe7f2b1b12a8a8cc7a160d2b53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 03:22:19 -0500 Subject: [PATCH 083/231] Bump led-ble to 0.7.1 (#77931) --- homeassistant/components/led_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/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 273fbfedc04..1dd289daa4d 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.7.0"], + "requirements": ["led-ble==0.7.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2c26df6547f..1dc6a08de26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.0 +led-ble==0.7.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b6adffe4a5..1e5013eb5f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.0 +led-ble==0.7.1 # homeassistant.components.foscam libpyfoscam==1.0 From a3edbfc6017e80376f1b2c7649e20f0a9a6b8980 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 Sep 2022 18:12:32 -0500 Subject: [PATCH 084/231] Small tweaks to improve performance of bluetooth matching (#77934) * Small tweaks to improve performance of bluetooth matching * Small tweaks to improve performance of bluetooth matching * cleanup --- homeassistant/components/bluetooth/match.py | 68 +++++++++++---------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 813acfc8cda..dd1c9c1fa3c 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -180,12 +180,20 @@ class BluetoothMatcherIndexBase(Generic[_T]): We put them in the bucket that they are most likely to match. """ + # Local name is the cheapest to match since its just a dict lookup if LOCAL_NAME in matcher: self.local_name.setdefault( _local_name_to_index_key(matcher[LOCAL_NAME]), [] ).append(matcher) return + # Manufacturer data is 2nd cheapest since its all ints + if MANUFACTURER_ID in matcher: + self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( + matcher + ) + return + if SERVICE_UUID in matcher: self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) return @@ -196,12 +204,6 @@ class BluetoothMatcherIndexBase(Generic[_T]): ) return - if MANUFACTURER_ID in matcher: - self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( - matcher - ) - return - def remove(self, matcher: _T) -> None: """Remove a matcher from the index. @@ -214,6 +216,10 @@ class BluetoothMatcherIndexBase(Generic[_T]): ) return + if MANUFACTURER_ID in matcher: + self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) + return + if SERVICE_UUID in matcher: self.service_uuid[matcher[SERVICE_UUID]].remove(matcher) return @@ -222,10 +228,6 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher) return - if MANUFACTURER_ID in matcher: - self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) - return - def build(self) -> None: """Rebuild the index sets.""" self.service_uuid_set = set(self.service_uuid) @@ -235,33 +237,36 @@ class BluetoothMatcherIndexBase(Generic[_T]): def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]: """Check for a match.""" matches = [] - if len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH: + if service_info.name and len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH: for matcher in self.local_name.get( service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], [] ): if ble_device_matches(matcher, service_info): matches.append(matcher) - for service_data_uuid in self.service_data_uuid_set.intersection( - service_info.service_data - ): - for matcher in self.service_data_uuid[service_data_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.service_data_uuid_set and service_info.service_data: + for service_data_uuid in self.service_data_uuid_set.intersection( + service_info.service_data + ): + for matcher in self.service_data_uuid[service_data_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) - for manufacturer_id in self.manufacturer_id_set.intersection( - service_info.manufacturer_data - ): - for matcher in self.manufacturer_id[manufacturer_id]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.manufacturer_id_set and service_info.manufacturer_data: + for manufacturer_id in self.manufacturer_id_set.intersection( + service_info.manufacturer_data + ): + for matcher in self.manufacturer_id[manufacturer_id]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) - for service_uuid in self.service_uuid_set.intersection( - service_info.service_uuids - ): - for matcher in self.service_uuid[service_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + if self.service_uuid_set and service_info.service_uuids: + for service_uuid in self.service_uuid_set.intersection( + service_info.service_uuids + ): + for matcher in self.service_uuid[service_uuid]: + if ble_device_matches(matcher, service_info): + matches.append(matcher) return matches @@ -347,8 +352,6 @@ def ble_device_matches( service_info: BluetoothServiceInfoBleak, ) -> bool: """Check if a ble device and advertisement_data matches the matcher.""" - device = service_info.device - # Don't check address here since all callers already # check the address and we don't want to double check # since it would result in an unreachable reject case. @@ -379,7 +382,8 @@ def ble_device_matches( return False if (local_name := matcher.get(LOCAL_NAME)) and ( - (device_name := advertisement_data.local_name or device.name) is None + (device_name := advertisement_data.local_name or service_info.device.name) + is None or not _memorized_fnmatch( device_name, local_name, From 3acc3af38c5b0535ee4be4c5baa2b165157645d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 06:12:17 -0500 Subject: [PATCH 085/231] Bump PySwitchbot to 0.18.25 (#77935) --- homeassistant/components/switchbot/__init__.py | 2 ++ homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 59ed071f325..345190d8933 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -31,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { SupportedModels.BULB.value: [Platform.SENSOR, Platform.LIGHT], SupportedModels.LIGHT_STRIP.value: [Platform.SENSOR, Platform.LIGHT], + SupportedModels.CEILING_LIGHT.value: [Platform.SENSOR, Platform.LIGHT], SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.CURTAIN.value: [ @@ -43,6 +44,7 @@ PLATFORMS_BY_TYPE = { SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { + SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain, SupportedModels.BOT.value: switchbot.Switchbot, SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index aa334120b85..ecd86e1bef5 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -16,6 +16,7 @@ class SupportedModels(StrEnum): BOT = "bot" BULB = "bulb" + CEILING_LIGHT = "ceiling_light" CURTAIN = "curtain" HYGROMETER = "hygrometer" LIGHT_STRIP = "light_strip" @@ -30,6 +31,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.PLUG_MINI: SupportedModels.PLUG, SwitchbotModel.COLOR_BULB: SupportedModels.BULB, SwitchbotModel.LIGHT_STRIP: SupportedModels.LIGHT_STRIP, + SwitchbotModel.CEILING_LIGHT: SupportedModels.CEILING_LIGHT, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 9fb73a62dd6..040e76391bd 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.18.22"], + "requirements": ["PySwitchbot==0.18.25"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 1dc6a08de26..c8723cc6737 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.18.22 +PySwitchbot==0.18.25 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e5013eb5f4..bb8403744b4 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.18.22 +PySwitchbot==0.18.25 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 10f7e2ff8a775400a28384ec7e2d762f670c336b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Sep 2022 09:54:21 -0500 Subject: [PATCH 086/231] Handle stale switchbot advertisement data while connected (#77956) --- homeassistant/components/switchbot/cover.py | 3 +++ homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/switch.py | 13 +++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index c2b6cb1a4c7..df716be6ff3 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any +import switchbot + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, @@ -36,6 +38,7 @@ async def async_setup_entry( class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Representation of a Switchbot.""" + _device: switchbot.SwitchbotCurtain _attr_device_class = CoverDeviceClass.CURTAIN _attr_supported_features = ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 040e76391bd..f322734ba54 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.18.25"], + "requirements": ["PySwitchbot==0.18.27"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 17235135cfa..d524a7100f0 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging from typing import Any +import switchbot + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON @@ -34,6 +36,7 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH + _device: switchbot.Switchbot def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" @@ -69,21 +72,19 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): @property def assumed_state(self) -> bool: """Return true if unable to access real state of entity.""" - if not self.data["data"]["switchMode"]: - return True - return False + return not self._device.switch_mode() @property def is_on(self) -> bool | None: """Return true if device is on.""" - if not self.data["data"]["switchMode"]: + if not self._device.switch_mode(): return self._attr_is_on - return self.data["data"]["isOn"] + return self._device.is_on() @property def extra_state_attributes(self) -> dict: """Return the state attributes.""" return { **super().extra_state_attributes, - "switch_mode": self.data["data"]["switchMode"], + "switch_mode": self._device.switch_mode(), } diff --git a/requirements_all.txt b/requirements_all.txt index c8723cc6737..0119639e3fc 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.18.25 +PySwitchbot==0.18.27 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb8403744b4..0878fbf4a16 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.18.25 +PySwitchbot==0.18.27 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From e69fde68757542abdadbc46230259148f137f3f6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 Sep 2022 17:31:53 +0200 Subject: [PATCH 087/231] Update frontend to 20220907.0 (#77963) --- 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 8abc8fd4e32..07822979683 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==20220906.0"], + "requirements": ["home-assistant-frontend==20220907.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 58a000fbc25..7c11725e460 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 0119639e3fc..a3c604dfc2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -851,7 +851,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0878fbf4a16..d974a7a181f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220906.0 +home-assistant-frontend==20220907.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 4ab5cdcb795845a5dbc83fea72d00edd788986fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 7 Sep 2022 17:46:53 +0200 Subject: [PATCH 088/231] Bumped version to 2022.9.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 156ba1d133d..c3a59a88aae 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b6" +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 1fdc8d87e07..9b05ae89191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0b6" +version = "2022.9.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 996a3477b0a64abb6ac9f3a7c2e746d829ccf263 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Sep 2022 12:53:30 -0400 Subject: [PATCH 089/231] Increase rate limit for zwave_js updates Al provided a new key which bumps the rate limit from 10k per hour to 100k per hour --- homeassistant/components/zwave_js/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index db3da247e7d..ff1a97d6ecc 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -123,5 +123,5 @@ ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" # This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for # your own (https://github.com/zwave-js/firmware-updates/). API_KEY_FIRMWARE_UPDATE_SERVICE = ( - "55eea74f055bef2ad893348112df6a38980600aaf82d2b02011297fc7ba495f830ca2b70" + "2e39d98fc56386389fbb35e5a98fa1b44b9fdd8f971460303587cff408430d4cfcde6134" ) From 3fd887b1f2b52da3642a773e1b27d4027cecc381 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Sep 2022 16:40:55 -0400 Subject: [PATCH 090/231] Show progress for zwave_js.update entity (#77905) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/update.py | 68 ++++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 97c14746dd9..f5604c8280c 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -4,6 +4,7 @@ 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 @@ -11,7 +12,7 @@ from zwave_js_server.client import Client as ZwaveClient 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 FirmwareUpdateInfo +from zwave_js_server.model.firmware import FirmwareUpdateInfo, FirmwareUpdateProgress from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components.update import UpdateDeviceClass, UpdateEntity @@ -63,7 +64,9 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): _attr_entity_category = EntityCategory.CONFIG _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_supported_features = ( - UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.RELEASE_NOTES + | UpdateEntityFeature.PROGRESS ) _attr_has_entity_name = True _attr_should_poll = False @@ -78,6 +81,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._latest_version_firmware: FirmwareUpdateInfo | None = None self._status_unsub: Callable[[], None] | None = None self._poll_unsub: Callable[[], None] | None = None + self._progress_unsub: Callable[[], None] | None = None + self._num_files_installed: int = 0 # Entity class attributes self._attr_name = "Firmware" @@ -93,6 +98,36 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._status_unsub = None self.hass.async_create_task(self._async_update()) + @callback + def _update_progress(self, event: dict[str, Any]) -> None: + """Update install progress on event.""" + 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.async_write_ha_state() + + @callback + def _reset_progress(self) -> None: + """Reset update install progress.""" + if self._progress_unsub: + self._progress_unsub() + self._progress_unsub = None + self._num_files_installed = 0 + self._attr_in_progress = False + self.async_write_ha_state() + async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: """Update the entity.""" self._poll_unsub = None @@ -152,18 +187,29 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - try: - for file in firmware.files: + self._attr_in_progress = 0 + self.async_write_ha_state() + self._progress_unsub = self.node.on( + "firmware update progress", self._update_progress + ) + for file in firmware.files: + try: await self.driver.controller.async_begin_ota_firmware_update( self.node, file ) - except BaseZwaveJSServerError as err: - raise HomeAssistantError(err) from err - else: - self._attr_installed_version = self._attr_latest_version = firmware.version - self._latest_version_firmware = None + except BaseZwaveJSServerError as err: + self._reset_progress() + raise HomeAssistantError(err) from err + self._num_files_installed += 1 + self._attr_in_progress = floor( + 100 * self._num_files_installed / len(firmware.files) + ) self.async_write_ha_state() + self._attr_installed_version = self._attr_latest_version = firmware.version + self._latest_version_firmware = None + self._reset_progress() + async def async_poll_value(self, _: bool) -> None: """Poll a value.""" LOGGER.error( @@ -200,3 +246,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): if self._poll_unsub: self._poll_unsub() self._poll_unsub = None + + if self._progress_unsub: + self._progress_unsub() + self._progress_unsub = None From ce6d337bd51d4f99cbdfabd0ece92afa6bdd9557 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Thu, 8 Sep 2022 08:49:36 +0200 Subject: [PATCH 091/231] Fix `len` method typo for Osram light (#78008) * Fix `len` method typo for Osram light * Fix typo in line 395 --- homeassistant/components/osramlightify/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 9fc54d0352c..e1b204115e6 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -373,7 +373,7 @@ class Luminary(LightEntity): self._max_mireds = color_util.color_temperature_kelvin_to_mired( self._luminary.min_temp() or DEFAULT_KELVIN ) - if len(self._attr_supported_color_modes == 1): + if len(self._attr_supported_color_modes) == 1: # The light supports only a single color mode self._attr_color_mode = list(self._attr_supported_color_modes)[0] @@ -392,7 +392,7 @@ class Luminary(LightEntity): if ColorMode.HS in self._attr_supported_color_modes: self._rgb_color = self._luminary.rgb() - if len(self._attr_supported_color_modes > 1): + if len(self._attr_supported_color_modes) > 1: # The light supports hs + color temp, determine which one it is if self._rgb_color == (0, 0, 0): self._attr_color_mode = ColorMode.COLOR_TEMP From ab9d9d599e6695d3b5dbad7215830cfb57b8ed9d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 8 Sep 2022 03:13:01 -0400 Subject: [PATCH 092/231] Add value ID to zwave_js device diagnostics (#78015) --- homeassistant/components/zwave_js/diagnostics.py | 1 + tests/components/zwave_js/test_diagnostics.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 078bd761b71..33d32e96fe0 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -123,6 +123,7 @@ def get_device_entities( "entity_category": entry.entity_category, "supported_features": entry.supported_features, "unit_of_measurement": entry.unit_of_measurement, + "value_id": value_id, "primary_value": primary_value_data, } entities.append(entity) diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 9f3a7b0884c..41505364111 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -152,6 +152,7 @@ async def test_device_diagnostics_missing_primary_value( x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id ) + assert air_entity["value_id"] == value.value_id assert air_entity["primary_value"] == { "command_class": value.command_class, "command_class_name": value.command_class_name, @@ -189,4 +190,5 @@ async def test_device_diagnostics_missing_primary_value( x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id ) + assert air_entity["value_id"] == value.value_id assert air_entity["primary_value"] is None From 31858ad7796fc42e3f91b97ccc288b476e5eb027 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 Sep 2022 10:59:40 +0200 Subject: [PATCH 093/231] Fix zwave_js default emulate hardware in options flow (#78024) --- .../components/zwave_js/config_flow.py | 4 ++- tests/components/zwave_js/test_config_flow.py | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 3da785cdcf2..c114662888f 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -792,7 +792,9 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL], - CONF_ADDON_EMULATE_HARDWARE: user_input[CONF_EMULATE_HARDWARE], + CONF_ADDON_EMULATE_HARDWARE: user_input.get( + CONF_EMULATE_HARDWARE, False + ), } if new_addon_config != addon_config: diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 6c4b18e8dc3..d4f159f2510 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1951,6 +1951,30 @@ async def different_device_server_version(*args): 0, different_device_server_version, ), + ( + {"config": ADDON_DISCOVERY_INFO}, + {}, + { + "device": "/test", + "network_key": "old123", + "s0_legacy_key": "old123", + "s2_access_control_key": "old456", + "s2_authenticated_key": "old789", + "s2_unauthenticated_key": "old987", + "log_level": "info", + }, + { + "usb_path": "/new", + "s0_legacy_key": "new123", + "s2_access_control_key": "new456", + "s2_authenticated_key": "new789", + "s2_unauthenticated_key": "new987", + "log_level": "info", + "emulate_hardware": False, + }, + 0, + different_device_server_version, + ), ], ) async def test_options_different_device( @@ -2018,14 +2042,16 @@ async def test_options_different_device( result = await hass.config_entries.options.async_configure(result["flow_id"]) await hass.async_block_till_done() + # Default emulate_hardware is False. + addon_options = {"emulate_hardware": False} | old_addon_options # Legacy network key is not reset. - old_addon_options.pop("network_key") + addon_options.pop("network_key") assert set_addon_options.call_count == 2 assert set_addon_options.call_args == call( hass, "core_zwave_js", - {"options": old_addon_options}, + {"options": addon_options}, ) assert result["type"] == "progress" assert result["step_id"] == "start_addon" From 6f3b49601e4306bb0ce12ff4b250e84cf1dfa64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 8 Sep 2022 10:50:14 +0200 Subject: [PATCH 094/231] Extract lametric device from coordinator in notify (#78027) --- homeassistant/components/lametric/notify.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index 4b404840388..c9ae376c496 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator async def async_get_service( @@ -31,8 +32,10 @@ async def async_get_service( """Get the LaMetric notification service.""" if discovery_info is None: return None - lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]] - return LaMetricNotificationService(lametric) + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][ + discovery_info["entry_id"] + ] + return LaMetricNotificationService(coordinator.lametric) class LaMetricNotificationService(BaseNotificationService): From 4009a32fb5f8e8e3013ce0b613c77fbe019b54ad Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 8 Sep 2022 20:28:40 +0200 Subject: [PATCH 095/231] Bump velbus-aio to 2022.9.1 (#78039) Bump velbusaio to 2022.9.1 --- 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 ec0c0f5f2d9..cbc8db0ca9f 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.6.2"], + "requirements": ["velbus-aio==2022.9.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index a3c604dfc2e..ada6fa28325 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2449,7 +2449,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.6.2 +velbus-aio==2022.9.1 # homeassistant.components.venstar venstarcolortouch==0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d974a7a181f..9847410eafc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1677,7 +1677,7 @@ vallox-websocket-api==2.12.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.6.2 +velbus-aio==2022.9.1 # homeassistant.components.venstar venstarcolortouch==0.18 From 3b025b211e64735b9511d8f86f963216dd774407 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 Sep 2022 20:15:27 +0200 Subject: [PATCH 096/231] Fix zwave_js device re-interview (#78046) * Handle stale node and entity info on re-interview * Add test * Unsubscribe on config entry unload --- homeassistant/components/zwave_js/__init__.py | 34 ++++++++--- homeassistant/components/zwave_js/entity.py | 13 ++++- homeassistant/components/zwave_js/update.py | 8 +++ tests/components/zwave_js/test_init.py | 57 +++++++++++++++++++ 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 98219520693..066bc5101ae 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -313,19 +313,24 @@ class ControllerEvents: node, ) + LOGGER.debug("Node added: %s", node.node_id) + + # Listen for ready node events, both new and re-interview. + self.config_entry.async_on_unload( + node.on( + "ready", + lambda event: self.hass.async_create_task( + self.node_events.async_on_node_ready(event["node"]) + ), + ) + ) + # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: await self.node_events.async_on_node_ready(node) return - # if node is not yet ready, register one-time callback for ready state - LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) - node.once( - "ready", - lambda event: self.hass.async_create_task( - self.node_events.async_on_node_ready(event["node"]) - ), - ) + # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added self.register_node_in_dev_reg(node) @@ -414,12 +419,25 @@ class NodeEvents: async def async_on_node_ready(self, node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) + driver = self.controller_events.driver_events.driver # register (or update) node in device registry device = self.controller_events.register_node_in_dev_reg(node) # We only want to create the defaultdict once, even on reinterviews if device.id not in self.controller_events.registered_unique_ids: self.controller_events.registered_unique_ids[device.id] = defaultdict(set) + # Remove any old value ids if this is a reinterview. + self.controller_events.discovered_value_ids.pop(device.id, None) + # Remove stale entities that may exist from a previous interview. + async_dispatcher_send( + self.hass, + ( + f"{DOMAIN}_" + f"{get_valueless_base_unique_id(driver, node)}_" + "remove_entity_on_ready_node" + ), + ) + value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} # run discovery on all node values and create/update entities diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 79dd1d27a4c..65f00b5022a 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN, LOGGER from .discovery import ZwaveDiscoveryInfo -from .helpers import get_device_id, get_unique_id +from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id EVENT_VALUE_UPDATED = "value updated" EVENT_VALUE_REMOVED = "value removed" @@ -96,6 +96,17 @@ class ZWaveBaseEntity(Entity): self.async_on_remove( self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + ( + f"{DOMAIN}_" + f"{get_valueless_base_unique_id(self.driver, self.info.node)}_" + "remove_entity_on_ready_node" + ), + self.async_remove, + ) + ) for status_event in (EVENT_ALIVE, EVENT_DEAD): self.async_on_remove( diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index f5604c8280c..4f25d138aea 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -235,6 +235,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): ) ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self._base_unique_id}_remove_entity_on_ready_node", + self.async_remove, + ) + ) + self.async_on_remove(async_at_start(self.hass, self._async_update)) async def async_will_remove_from_hass(self) -> None: diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index d038949d494..63cbc090e7d 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -3,6 +3,7 @@ from copy import deepcopy from unittest.mock import call, patch import pytest +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 @@ -12,6 +13,7 @@ from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from homeassistant.helpers import ( area_registry as ar, device_registry as dr, @@ -242,6 +244,61 @@ async def test_existing_node_ready(hass, client, multisensor_6, integration): ) +async def test_existing_node_reinterview( + hass: HomeAssistant, + client: Client, + multisensor_6_state: dict, + multisensor_6: Node, + integration: MockConfigEntry, +) -> None: + """Test we handle a node re-interview firing a node ready event.""" + dev_reg = dr.async_get(hass) + node = multisensor_6 + assert client.driver is not None + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + air_temperature_device_id_ext = ( + f"{air_temperature_device_id}-{node.manufacturer_id}:" + f"{node.product_type}:{node.product_id}" + ) + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + + device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id_ext)} + ) + assert device.sw_version == "1.12" + + node_state = deepcopy(multisensor_6_state) + node_state["firmwareVersion"] = "1.13" + event = Event( + type="ready", + data={ + "source": "node", + "event": "ready", + "nodeId": node.node_id, + "nodeState": node_state, + }, + ) + client.driver.receive_event(event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state != STATE_UNAVAILABLE + device = dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) + assert device + assert device == dev_reg.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id_ext)} + ) + assert device.sw_version == "1.13" + + async def test_existing_node_not_ready(hass, zp3111_not_ready, client, integration): """Test we handle a non-ready node that exists during integration setup.""" dev_reg = dr.async_get(hass) From 2ddd1b516caadcce224e76cdfd173bd213883363 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Sep 2022 08:58:53 -0500 Subject: [PATCH 097/231] Bump bluetooth-adapters to 0.3.5 (#78052) --- 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 ca6a76c55ae..aa890a47a41 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.4", + "bluetooth-adapters==0.3.5", "bluetooth-auto-recovery==0.3.1" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7c11725e460..9608cdbfe9e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.4 +bluetooth-adapters==0.3.5 bluetooth-auto-recovery==0.3.1 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ada6fa28325..9c59434c26f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.4 +bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9847410eafc..69b816f2af7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.0 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.4 +bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.1 From cfe8ebdad4bd73ac70da796c72a0119fa80f3b5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Sep 2022 12:15:26 -0500 Subject: [PATCH 098/231] Bump bluetooth-auto-recovery to 0.3.2 (#78063) --- 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 aa890a47a41..3043d6412a4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.3.5", - "bluetooth-auto-recovery==0.3.1" + "bluetooth-auto-recovery==0.3.2" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9608cdbfe9e..1a1fcb9da84 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 bluetooth-adapters==0.3.5 -bluetooth-auto-recovery==0.3.1 +bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 9c59434c26f..34645632890 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.1 +bluetooth-auto-recovery==0.3.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69b816f2af7..f3927d4ed8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.3.5 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.1 +bluetooth-auto-recovery==0.3.2 # homeassistant.components.bond bond-async==0.1.22 From 760853f6155586e7d69347feff3552a406f3ee29 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 8 Sep 2022 14:41:09 -0600 Subject: [PATCH 099/231] Fix bug with 1st gen RainMachine controllers and unknown API calls (#78070) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/rainmachine/__init__.py | 12 ++++++++++-- .../components/rainmachine/binary_sensor.py | 4 +++- homeassistant/components/rainmachine/manifest.json | 2 +- homeassistant/components/rainmachine/sensor.py | 12 +++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 52de2e1c61a..756dc9b958d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -9,7 +9,7 @@ from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller -from regenmaschine.errors import RainMachineError +from regenmaschine.errors import RainMachineError, UnknownAPICallError import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -190,7 +190,9 @@ async def async_update_programs_and_zones( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up RainMachine as config entry.""" websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -244,6 +246,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = await controller.restrictions.universal() else: data = await controller.zones.all(details=True, include_inactive=True) + except UnknownAPICallError: + LOGGER.info( + "Skipping unsupported API call for controller %s: %s", + controller.name, + api_category, + ) except RainMachineError as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 3db64240788..48f11f598c9 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -175,7 +175,9 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FLOW_SENSOR: - self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor") + self._attr_is_on = self.coordinator.data.get("system", {}).get( + "useFlowSensor" + ) class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index b183fc1b24f..a6cc86e5055 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.08.0"], + "requirements": ["regenmaschine==2022.09.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index e2e602b945b..32364e08199 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -273,12 +273,14 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): 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["system"].get( + 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["system"].get("flowSensorWateringClicks") - clicks_per_m3 = self.coordinator.data["system"].get( + clicks = self.coordinator.data.get("system", {}).get( + "flowSensorWateringClicks" + ) + clicks_per_m3 = self.coordinator.data.get("system", {}).get( "flowSensorClicksPerCubicMeter" ) @@ -287,11 +289,11 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): else: self._attr_native_value = None elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX: - self._attr_native_value = self.coordinator.data["system"].get( + 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["system"].get( + self._attr_native_value = self.coordinator.data.get("system", {}).get( "flowSensorWateringClicks" ) diff --git a/requirements_all.txt b/requirements_all.txt index 34645632890..16df31128aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,7 +2118,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.08.0 +regenmaschine==2022.09.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3927d4ed8b..1f94d4041a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.08.0 +regenmaschine==2022.09.0 # homeassistant.components.renault renault-api==0.1.11 From d559b6482ac297c493b6294222e2e80ab27726ae Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 8 Sep 2022 11:13:20 -0600 Subject: [PATCH 100/231] Bump pylitterbot to 2022.9.1 (#78071) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/conftest.py | 8 +++++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index f25b4525877..a4c9f3cd54e 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.8.2"], + "requirements": ["pylitterbot==2022.9.1"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 16df31128aa..e740b8d0017 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1668,7 +1668,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.8.2 +pylitterbot==2022.9.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f94d4041a7..493ff7c4e5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1166,7 +1166,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.8.2 +pylitterbot==2022.9.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index e5d5e730b61..34132ec66d6 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -17,13 +17,13 @@ from tests.common import MockConfigEntry def create_mock_robot( - robot_data: dict | None = None, side_effect: Any | None = None + robot_data: dict | None, account: Account, side_effect: Any | None = None ) -> Robot: """Create a mock Litter-Robot device.""" if not robot_data: robot_data = {} - robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}) + robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}, account=account) robot.start_cleaning = AsyncMock(side_effect=side_effect) robot.set_power_status = AsyncMock(side_effect=side_effect) robot.reset_waste_drawer = AsyncMock(side_effect=side_effect) @@ -44,7 +44,9 @@ def create_mock_account( account = MagicMock(spec=Account) account.connect = AsyncMock() account.refresh_robots = AsyncMock() - account.robots = [] if skip_robots else [create_mock_robot(robot_data, side_effect)] + account.robots = ( + [] if skip_robots else [create_mock_robot(robot_data, account, side_effect)] + ) return account From c873eae79ce621391f6f8890c40477882fd39f61 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 8 Sep 2022 22:49:49 +0200 Subject: [PATCH 101/231] Allow OpenWeatherMap config flow to test using old API to pass (#78074) Co-authored-by: Paulus Schoutsen --- homeassistant/components/openweathermap/config_flow.py | 2 +- tests/components/openweathermap/test_config_flow.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 612965bdb2f..c418231946f 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -130,4 +130,4 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow): async def _is_owm_api_online(hass, api_key, lat, lon): owm = OWM(api_key).weather_manager() - return await hass.async_add_executor_job(owm.one_call, lat, lon) + return await hass.async_add_executor_job(owm.weather_at_coords, lat, lon) diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 12ee849d3d2..40931dc2ce2 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -208,6 +208,8 @@ def _create_mocked_owm(is_api_online: bool): mocked_owm.one_call.return_value = one_call - mocked_owm.weather_manager.return_value.one_call.return_value = is_api_online + mocked_owm.weather_manager.return_value.weather_at_coords.return_value = ( + is_api_online + ) return mocked_owm From a4f398a7509a2afb3fb89f5e2082ca7ad0800a7a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 8 Sep 2022 16:50:47 -0400 Subject: [PATCH 102/231] Bumped version to 2022.9.1 --- 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 c3a59a88aae..38a50aa8fc3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __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 9b05ae89191..10bab1ac2f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.0" +version = "2022.9.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From fee9a303ffdb8cff6fc8c9f8d606040d81f45341 Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Thu, 8 Sep 2022 21:01:43 -0400 Subject: [PATCH 103/231] Fix issue #77920 - ecobee remote sensors not updating (#78035) --- homeassistant/components/ecobee/sensor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index a7d5639ae2c..9d8793efc29 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -29,7 +29,7 @@ from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER class EcobeeSensorEntityDescriptionMixin: """Represent the required ecobee entity description attributes.""" - runtime_key: str + runtime_key: str | None @dataclass @@ -46,7 +46,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - runtime_key="actualTemperature", + runtime_key=None, ), EcobeeSensorEntityDescription( key="humidity", @@ -54,7 +54,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - runtime_key="actualHumidity", + runtime_key=None, ), EcobeeSensorEntityDescription( key="co2PPM", @@ -194,6 +194,11 @@ class EcobeeSensor(SensorEntity): for item in sensor["capability"]: if item["type"] != self.entity_description.key: continue - thermostat = self.data.ecobee.get_thermostat(self.index) - self._state = thermostat["runtime"][self.entity_description.runtime_key] + if self.entity_description.runtime_key is None: + self._state = item["value"] + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + self._state = thermostat["runtime"][ + self.entity_description.runtime_key + ] break From d670df74cb346805a173e8118bf97818396ac6ad Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 2 Sep 2022 18:54:20 +0200 Subject: [PATCH 104/231] Fix reload of MQTT config entries (#76089) * Wait for unsubscribes * Spelling comment * Remove notify_all() during _register_mid() * Update homeassistant/components/mqtt/client.py Co-authored-by: Erik Montnemery * Correct handling reload manual set up MQTT items * Save and restore device trigger subscriptions * Clarify we are storing all remaining subscriptions Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 26 +++++++++++++++++--- homeassistant/components/mqtt/client.py | 10 +++++--- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/mixins.py | 7 ++++++ tests/components/mqtt/test_device_trigger.py | 20 ++++++++++++++- 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1121377a30e..3ec9a7e9d4e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -71,6 +71,7 @@ from .const import ( # noqa: F401 DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_QOS, @@ -315,6 +316,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data[DATA_MQTT] = MQTT(hass, entry, conf) + # Restore saved subscriptions + if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data: + hass.data[DATA_MQTT].subscriptions = hass.data.pop( + DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE + ) entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() @@ -438,6 +444,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_forward_entry_setup_and_setup_discovery(config_entry): """Forward the config entry setup to the platforms and set up discovery.""" + reload_manual_setup: bool = False # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import device_automation, tag @@ -460,8 +467,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded await async_setup_reload_service() + # When the entry is reloaded, also reload manual set up items to enable MQTT + if DATA_MQTT_RELOAD_ENTRY in hass.data: + hass.data.pop(DATA_MQTT_RELOAD_ENTRY) + reload_manual_setup = True + + # When the entry was disabled before, reload manual set up items to enable MQTT again if DATA_MQTT_RELOAD_NEEDED in hass.data: hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + reload_manual_setup = True + + if reload_manual_setup: await async_reload_manual_mqtt_items(hass) await async_forward_entry_setup_and_setup_discovery(entry) @@ -613,8 +629,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mqtt_client.cleanup() # Trigger reload manual MQTT items at entry setup - # Reload the legacy yaml platform - await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: # The entry is disabled reload legacy manual items when the entry is enabled again hass.data[DATA_MQTT_RELOAD_NEEDED] = True @@ -622,7 +636,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # The entry is reloaded: # Trigger re-fetching the yaml config at entry setup hass.data[DATA_MQTT_RELOAD_ENTRY] = True - # Stop the loop + # Reload the legacy yaml platform to make entities unavailable + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + # Wait for all ACKs and stop the loop await mqtt_client.async_disconnect() + # Store remaining subscriptions to be able to restore or reload them + # when the entry is set up again + if mqtt_client.subscriptions: + hass.data[DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE] = mqtt_client.subscriptions return True diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 192de624f17..57f51593ed4 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -309,7 +309,7 @@ class MQTT: def __init__( self, - hass: HomeAssistant, + hass, config_entry, conf, ) -> None: @@ -435,12 +435,13 @@ class MQTT: """Return False if there are unprocessed ACKs.""" return not bool(self._pending_operations) - # wait for ACK-s to be processesed (unsubscribe only) + # wait for ACKs to be processed async with self._pending_operations_condition: await self._pending_operations_condition.wait_for(no_more_acks) # stop the MQTT loop - await self.hass.async_add_executor_job(stop) + async with self._paho_lock: + await self.hass.async_add_executor_job(stop) async def async_subscribe( self, @@ -501,7 +502,8 @@ class MQTT: async with self._paho_lock: mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic) await self._register_mid(mid) - self.hass.async_create_task(self._wait_for_mid(mid)) + + self.hass.async_create_task(self._wait_for_mid(mid)) async def _async_perform_subscriptions( self, subscriptions: Iterable[tuple[str, int]] diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6a5cb912fce..0c711e097d5 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -32,6 +32,7 @@ CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_MQTT = "mqtt" +DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions" DATA_MQTT_CONFIG = "mqtt_config" MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index faef154dd84..75532f75c13 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -65,6 +65,7 @@ from .const import ( DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_DISPATCHERS, + DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, @@ -363,6 +364,12 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Help to set up the platform for manual configured MQTT entities.""" + if DATA_MQTT_RELOAD_ENTRY in hass.data: + _LOGGER.debug( + "MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry", + platform_domain, + ) + return if not (entry_status := mqtt_config_entry_enabled(hass)): _LOGGER.warning( "MQTT integration is %s, skipping setup of manually configured MQTT %s", diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 8363ca34fd7..661075c3cbe 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest +from homeassistant import config as hass_config import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info @@ -1425,7 +1426,24 @@ async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> Non await help_test_unload_config_entry(hass, tmp_path, {}) - # Fake short press 2 + # Rediscover message and fake short press 2 (non impact) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") await hass.async_block_till_done() assert len(calls) == 1 + + mqtt_entry = hass.config_entries.async_entries("mqtt")[0] + + # Load the entry again + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config_file.write_text("") + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await mqtt_entry.async_setup(hass) + + # Rediscover and fake short press 3 + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 2 From 21f6b50f7c0d84cf18e57152d267779ff697e73b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 9 Sep 2022 15:24:26 +0200 Subject: [PATCH 105/231] Clear MQTT discovery topic when a disabled entity is removed (#77757) * Cleanup discovery on entity removal * Add test * Cleanup and test * Test with clearing payload not unique id * Address comments * Tests cover and typing * Just pass hass * reuse code * Follow up comments revert changes to cover tests * Add test unique_id has priority over disabled * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 16 +- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/mixins.py | 51 ++++++- tests/components/mqtt/test_discovery.py | 170 ++++++++++++++++++++++ 4 files changed, 232 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3ec9a7e9d4e..842e5b6405f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -20,7 +20,13 @@ from homeassistant.const import ( CONF_USERNAME, SERVICE_RELOAD, ) -from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + CALLBACK_TYPE, + HassJob, + HomeAssistant, + ServiceCall, + callback, +) from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import ( config_validation as cv, @@ -68,6 +74,7 @@ from .const import ( # noqa: F401 CONFIG_ENTRY_IS_SETUP, DATA_MQTT, DATA_MQTT_CONFIG, + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_RELOAD_NEEDED, @@ -315,6 +322,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Bail out return False + hass.data[DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] = {} hass.data[DATA_MQTT] = MQTT(hass, entry, conf) # Restore saved subscriptions if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data: @@ -638,6 +646,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_MQTT_RELOAD_ENTRY] = True # Reload the legacy yaml platform to make entities unavailable await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + # Cleanup entity registry hooks + registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS + ] + while registry_hooks: + registry_hooks.popitem()[1]() # Wait for all ACKs and stop the loop await mqtt_client.async_disconnect() # Store remaining subscriptions to be able to restore or reload them diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 0c711e097d5..c8af58862e0 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -33,6 +33,7 @@ CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_MQTT = "mqtt" DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions" +DATA_MQTT_DISCOVERY_REGISTRY_HOOKS = "mqtt_discovery_registry_hooks" DATA_MQTT_CONFIG = "mqtt_config" MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 75532f75c13..fddbe838303 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -28,7 +28,13 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import Event, HomeAssistant, async_get_hass, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + async_get_hass, + callback, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -48,6 +54,7 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) 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.typing import ConfigType, DiscoveryInfoType @@ -64,6 +71,7 @@ from .const import ( CONF_TOPIC, DATA_MQTT, DATA_MQTT_CONFIG, + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_UPDATED_CONFIG, @@ -654,6 +662,17 @@ async def async_remove_discovery_payload(hass: HomeAssistant, discovery_data: di await async_publish(hass, discovery_topic, "", retain=True) +async def async_clear_discovery_topic_if_entity_removed( + hass: HomeAssistant, + discovery_data: dict[str, Any], + event: Event, +) -> None: + """Clear the discovery topic if the entity is removed.""" + if event.data["action"] == "remove": + # publish empty payload to config topic to avoid re-adding + await async_remove_discovery_payload(hass, discovery_data) + + class MqttDiscoveryDeviceUpdate: """Add support for auto discovery for platforms without an entity.""" @@ -787,7 +806,8 @@ class MqttDiscoveryUpdate(Entity): def __init__( self, - discovery_data: dict, + hass: HomeAssistant, + discovery_data: dict | None, discovery_update: Callable | None = None, ) -> None: """Initialize the discovery update mixin.""" @@ -795,6 +815,14 @@ class MqttDiscoveryUpdate(Entity): self._discovery_update = discovery_update self._remove_discovery_updated: Callable | None = None self._removed_from_hass = False + if discovery_data is None: + return + self._registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ + DATA_MQTT_DISCOVERY_REGISTRY_HOOKS + ] + discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] + if discovery_hash in self._registry_hooks: + self._registry_hooks.pop(discovery_hash)() async def async_added_to_hass(self) -> None: """Subscribe to discovery updates.""" @@ -857,7 +885,7 @@ class MqttDiscoveryUpdate(Entity): async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" - if not self._removed_from_hass: + if not self._removed_from_hass and self._discovery_data is not None: # Stop subscribing to discovery updates to not trigger when we clear the # discovery topic self._cleanup_discovery_on_remove() @@ -868,7 +896,20 @@ class MqttDiscoveryUpdate(Entity): @callback def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" - if self._discovery_data: + if self._discovery_data is not None: + discovery_hash: tuple = self._discovery_data[ATTR_DISCOVERY_HASH] + if self.registry_entry is not None: + self._registry_hooks[ + discovery_hash + ] = async_track_entity_registry_updated_event( + self.hass, + self.entity_id, + partial( + async_clear_discovery_topic_if_entity_removed, + self.hass, + self._discovery_data, + ), + ) stop_discovery_updates(self.hass, self._discovery_data) send_discovery_done(self.hass, self._discovery_data) super().add_to_platform_abort() @@ -976,7 +1017,7 @@ class MqttEntity( # Initialize mixin classes MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + MqttDiscoveryUpdate.__init__(self, hass, discovery_data, self.discovery_update) MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry) def _init_entity_id(self): diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 29ca1f11743..c625d0a21f9 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,4 +1,5 @@ """The tests for the MQTT discovery.""" +import copy import json from pathlib import Path import re @@ -23,6 +24,8 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( MockConfigEntry, async_capture_events, @@ -1356,3 +1359,170 @@ async def test_mqtt_discovery_unsubscribe_once( await hass.async_block_till_done() await hass.async_block_till_done() mqtt_client_mock.unsubscribe.assert_called_once_with("comp/discovery/#") + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_clear_config_topic_disabled_entity( + hass, mqtt_mock_entry_no_yaml_config, device_reg, caplog +): + """Test the discovery topic is removed when a disabled entity is removed.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + # discover an entity that is not enabled by default + config = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", + "unique_id": "sbfspot_12345", + "enabled_by_default": False, + "device": { + "identifiers": ["sbfspot_12345"], + "name": "sbfspot_12345", + "sw_version": "1.0", + "connections": [["mac", "12:34:56:AB:CD:EF"]], + }, + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345/config", + json.dumps(config), + ) + await hass.async_block_till_done() + # discover an entity that is not unique (part 1), will be added + config_not_unique1 = copy.deepcopy(config) + config_not_unique1["name"] = "sbfspot_12345_1" + config_not_unique1["unique_id"] = "not_unique" + config_not_unique1.pop("enabled_by_default") + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", + json.dumps(config_not_unique1), + ) + # discover an entity that is not unique (part 2), will not be added + config_not_unique2 = copy.deepcopy(config_not_unique1) + config_not_unique2["name"] = "sbfspot_12345_2" + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config", + json.dumps(config_not_unique2), + ) + await hass.async_block_till_done() + assert "Platform mqtt does not generate unique IDs" in caplog.text + + assert hass.states.get("sensor.sbfspot_12345") is None # disabled + assert hass.states.get("sensor.sbfspot_12345_1") is not None # enabled + assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + + # Verify device is created + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + + # Remove the device from the registry + device_reg.async_remove_device(device_entry.id) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Assert all valid discovery topics are cleared + assert mqtt_mock.async_publish.call_count == 2 + assert ( + call("homeassistant/sensor/sbfspot_0/sbfspot_12345/config", "", 0, True) + in mqtt_mock.async_publish.mock_calls + ) + assert ( + call("homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", "", 0, True) + in mqtt_mock.async_publish.mock_calls + ) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_clean_up_registry_monitoring( + hass, mqtt_mock_entry_no_yaml_config, device_reg, tmp_path +): + """Test registry monitoring hook is removed after a reload.""" + await mqtt_mock_entry_no_yaml_config() + hooks: dict = hass.data[mqtt.const.DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] + # discover an entity that is not enabled by default + config1 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", + "unique_id": "sbfspot_12345", + "enabled_by_default": False, + "device": { + "identifiers": ["sbfspot_12345"], + "name": "sbfspot_12345", + "sw_version": "1.0", + "connections": [["mac", "12:34:56:AB:CD:EF"]], + }, + } + # Publish it config + # Since it is not enabled_by_default the sensor will not be loaded + # it should register a hook for monitoring the entiry registry + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345/config", + json.dumps(config1), + ) + await hass.async_block_till_done() + assert len(hooks) == 1 + + # Publish it again no new monitor should be started + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345/config", + json.dumps(config1), + ) + await hass.async_block_till_done() + assert len(hooks) == 1 + + # Verify device is created + device_entry = device_reg.async_get_device(set(), {("mac", "12:34:56:AB:CD:EF")}) + assert device_entry is not None + + # Enload the entry + # The monitoring should be cleared + await help_test_unload_config_entry(hass, tmp_path, {}) + assert len(hooks) == 0 + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_unique_id_collission_has_priority( + hass, mqtt_mock_entry_no_yaml_config, entity_reg +): + """Test tehe unique_id collision detection has priority over registry disabled items.""" + await mqtt_mock_entry_no_yaml_config() + config = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", + "unique_id": "sbfspot_12345", + "enabled_by_default": False, + "device": { + "identifiers": ["sbfspot_12345"], + "name": "sbfspot_12345", + "sw_version": "1.0", + "connections": [["mac", "12:34:56:AB:CD:EF"]], + }, + } + # discover an entity that is not unique and disabled by default (part 1), will be added + config_not_unique1 = copy.deepcopy(config) + config_not_unique1["name"] = "sbfspot_12345_1" + config_not_unique1["unique_id"] = "not_unique" + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_1/config", + json.dumps(config_not_unique1), + ) + # discover an entity that is not unique (part 2), will not be added, and the registry entry is cleared + config_not_unique2 = copy.deepcopy(config_not_unique1) + config_not_unique2["name"] = "sbfspot_12345_2" + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/sbfspot_12345_2/config", + json.dumps(config_not_unique2), + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sbfspot_12345_1") is None # not enabled + assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + + # Verify the first entity is created + assert entity_reg.async_get("sensor.sbfspot_12345_1") is not None + # Verify the second entity is not created because it is not unique + assert entity_reg.async_get("sensor.sbfspot_12345_2") is None From 4c0872b4e4d44ac651f8f3eb74a9a05abab7e6c9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 9 Sep 2022 11:12:09 +0200 Subject: [PATCH 106/231] Improve warning messages on invalid received modes (#77909) --- .../components/mqtt/light/schema_json.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 295b43120d4..14eca9569f8 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -249,7 +249,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid RGB color value received") + _LOGGER.warning( + "Invalid RGB color value received for entity %s", self.entity_id + ) return try: @@ -259,7 +261,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid XY color value received") + _LOGGER.warning( + "Invalid XY color value received for entity %s", self.entity_id + ) return try: @@ -269,12 +273,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid HS color value received") + _LOGGER.warning( + "Invalid HS color value received for entity %s", self.entity_id + ) return else: color_mode = values["color_mode"] if not self._supports_color_mode(color_mode): - _LOGGER.warning("Invalid color mode received") + _LOGGER.warning( + "Invalid color mode received for entity %s", self.entity_id + ) return try: if color_mode == ColorMode.COLOR_TEMP: @@ -314,7 +322,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._color_mode = ColorMode.XY self._xy = (x, y) except (KeyError, ValueError): - _LOGGER.warning("Invalid or incomplete color value received") + _LOGGER.warning( + "Invalid or incomplete color value received for entity %s", + self.entity_id, + ) def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -351,7 +362,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except (TypeError, ValueError): - _LOGGER.warning("Invalid brightness value received") + _LOGGER.warning( + "Invalid brightness value received for entity %s", + self.entity_id, + ) if ( self._supported_features @@ -366,7 +380,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid color temp value received") + _LOGGER.warning( + "Invalid color temp value received for entity %s", + self.entity_id, + ) if self._supported_features and LightEntityFeature.EFFECT: with suppress(KeyError): From f042cc5d7bed73a39d5869c606765a505a0cdc27 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Sep 2022 01:47:33 -0400 Subject: [PATCH 107/231] Handle missing supported brands (#78090) --- homeassistant/components/websocket_api/commands.py | 3 +++ tests/components/websocket_api/test_commands.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 1761323a60d..d4596619241 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -722,6 +722,9 @@ async def handle_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) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index fe748e2c47c..354a4edeb0d 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1760,6 +1760,12 @@ async def test_validate_config_invalid(websocket_client, key, config, 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"}}), @@ -1773,7 +1779,7 @@ async def test_supported_brands(hass, websocket_client): with patch( "homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS", - ("abcd", "test"), + ("abcd", "test", "override_without_brands"), ): await websocket_client.send_json({"id": 7, "type": "supported_brands"}) msg = await websocket_client.receive_json() From dc7c860c6ae5704f1b26ac091c947d76c60e7ddc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 18:13:27 -0500 Subject: [PATCH 108/231] Fix switchbot writing state too frequently (#78094) --- .../components/switchbot/coordinator.py | 5 ++++- .../components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 17 ++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 103e9d67c58..94018c1b46b 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -69,13 +69,16 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): change: bluetooth.BluetoothChange, ) -> None: """Handle a Bluetooth event.""" + self.ble_device = service_info.device if adv := switchbot.parse_advertisement_data( service_info.device, service_info.advertisement ): - self.data = flatten_sensors_data(adv.data) if "modelName" in self.data: self._ready_event.set() _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) + if not self.device.advertisement_changed(adv): + return + self.data = flatten_sensors_data(adv.data) self.device.update_from_advertisement(adv) super()._async_handle_bluetooth_event(service_info, change) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f322734ba54..e311295e52c 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.18.27"], + "requirements": ["PySwitchbot==0.19.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 886da1051b7..9658c1ed9c8 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -71,14 +71,16 @@ async def async_setup_entry( ) -> None: """Set up Switchbot sensor based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ SwitchBotSensor( coordinator, sensor, ) for sensor in coordinator.data["data"] if sensor in SENSOR_TYPES - ) + ] + entities.append(SwitchbotRSSISensor(coordinator, "rssi")) + async_add_entities(entities) class SwitchBotSensor(SwitchbotEntity, SensorEntity): @@ -98,6 +100,15 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): self.entity_description = SENSOR_TYPES[sensor] @property - def native_value(self) -> str: + def native_value(self) -> str | int: """Return the state of the sensor.""" return self.data["data"][self._sensor] + + +class SwitchbotRSSISensor(SwitchBotSensor): + """Representation of a Switchbot RSSI sensor.""" + + @property + def native_value(self) -> str | int: + """Return the state of the sensor.""" + return self.coordinator.ble_device.rssi diff --git a/requirements_all.txt b/requirements_all.txt index e740b8d0017..49fcc1876ec 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.18.27 +PySwitchbot==0.19.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 493ff7c4e5b..8ebf003e88e 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.18.27 +PySwitchbot==0.19.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 3ee62d619ff1ab8765cde735a73b650b0a312cdb Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 9 Sep 2022 14:43:54 +0200 Subject: [PATCH 109/231] Fix LIFX light turning on while fading off (#78095) --- homeassistant/components/lifx/light.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 36d3b480f74..4237fca9be7 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -219,15 +219,10 @@ class LIFXLight(LIFXEntity, LightEntity): elif power_on: await self.set_power(True, duration=fade) else: + if power_on: + await self.set_power(True) if hsbk: await self.set_color(hsbk, kwargs, duration=fade) - # The response from set_color will tell us if the - # bulb is actually on or not, so we don't need to - # call power_on if its already on - if power_on and self.bulb.power_level == 0: - await self.set_power(True) - elif power_on: - await self.set_power(True) if power_off: await self.set_power(False, duration=fade) From 125afb39f0e1ee62b563fba228c941c8e0af6ac0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 9 Sep 2022 16:10:56 -0400 Subject: [PATCH 110/231] Fix zwave_js update entity (#78116) * Test zwave_js update entity progress * Block until firmware update is done * Update homeassistant/components/zwave_js/update.py Co-authored-by: Martin Hjelmare * revert params * unsub finished event listener * fix tests * Add test for returned failure * refactor a little * rename * Remove unnecessary controller logic for mocking * Clear event when resetting * Comments * readability * Fix test * Fix test Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/update.py | 71 +++++-- tests/components/zwave_js/test_update.py | 204 +++++++++++++++++--- 2 files changed, 232 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 4f25d138aea..932ed46a0fc 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -12,7 +12,12 @@ from zwave_js_server.client import Client as ZwaveClient 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 FirmwareUpdateInfo, FirmwareUpdateProgress +from zwave_js_server.model.firmware import ( + FirmwareUpdateFinished, + FirmwareUpdateInfo, + FirmwareUpdateProgress, + FirmwareUpdateStatus, +) from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components.update import UpdateDeviceClass, UpdateEntity @@ -82,7 +87,10 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._status_unsub: Callable[[], None] | None = None 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 # Entity class attributes self._attr_name = "Firmware" @@ -119,18 +127,38 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self.async_write_ha_state() @callback - def _reset_progress(self) -> None: - """Reset update install progress.""" + 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 + self._finished_event.set() + + @callback + def _unsub_firmware_events_and_reset_progress( + self, write_state: bool = False + ) -> None: + """Unsubscribe from firmware events and reset update install progress.""" if self._progress_unsub: self._progress_unsub() self._progress_unsub = None + + if self._finished_unsub: + self._finished_unsub() + self._finished_unsub = None + + self._finished_status = None + self._finished_event.clear() self._num_files_installed = 0 - self._attr_in_progress = False - self.async_write_ha_state() + self._attr_in_progress = 0 + if write_state: + self.async_write_ha_state() async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: """Update the entity.""" self._poll_unsub = None + + # If device is asleep/dead, wait for it to wake up/become alive before + # attempting an update for status, event_name in ( (NodeStatus.ASLEEP, "wake up"), (NodeStatus.DEAD, "alive"), @@ -187,19 +215,40 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._attr_in_progress = 0 - self.async_write_ha_state() + self._unsub_firmware_events_and_reset_progress(True) + self._progress_unsub = self.node.on( "firmware update progress", self._update_progress ) + self._finished_unsub = self.node.once( + "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._reset_progress() + 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._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) @@ -208,7 +257,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None - self._reset_progress() + self._unsub_firmware_events_and_reset_progress() async def async_poll_value(self, _: bool) -> None: """Poll a value.""" @@ -255,6 +304,4 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._poll_unsub() self._poll_unsub = None - if self._progress_unsub: - self._progress_unsub() - self._progress_unsub = None + self._unsub_firmware_events_and_reset_progress() diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 76fecfdee6d..0b567d93106 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -1,9 +1,11 @@ """Test the Z-Wave JS update entities.""" +import asyncio from datetime import timedelta import pytest from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand +from zwave_js_server.model.firmware import FirmwareUpdateStatus from homeassistant.components.update.const import ( ATTR_AUTO_UPDATE, @@ -51,7 +53,7 @@ FIRMWARE_UPDATES = { } -async def test_update_entity_success( +async def test_update_entity_states( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, @@ -60,7 +62,7 @@ async def test_update_entity_success( caplog, hass_ws_client, ): - """Test update entity.""" + """Test update entity states.""" ws_client = await hass_ws_client(hass) await hass.async_block_till_done() @@ -137,39 +139,14 @@ async def test_update_entity_success( client.async_send_command.reset_mock() - # Test successful install call without a version - await hass.services.async_call( - UPDATE_DOMAIN, - SERVICE_INSTALL, - { - ATTR_ENTITY_ID: UPDATE_ENTITY, - }, - blocking=True, - ) - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "controller.begin_ota_firmware_update" - assert ( - args["nodeId"] - == climate_radio_thermostat_ct100_plus_different_endpoints.node_id - ) - assert args["update"] == { - "target": 0, - "url": "https://example2.com", - "integrity": "sha2", - } - - client.async_send_command.reset_mock() - - -async def test_update_entity_install_failure( +async def test_update_entity_install_raises( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, - controller_node, integration, ): - """Test update entity failed install.""" + """Test update entity install raises exception.""" client.async_send_command.return_value = FIRMWARE_UPDATES async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=1)) @@ -287,11 +264,10 @@ async def test_update_entity_ha_not_running( assert args["nodeId"] == zen_31.node_id -async def test_update_entity_failure( +async def test_update_entity_update_failure( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, - controller_node, integration, ): """Test update entity update failed.""" @@ -311,3 +287,169 @@ async def test_update_entity_failure( args["nodeId"] == climate_radio_thermostat_ct100_plus_different_endpoints.node_id ) + + +async def test_update_entity_progress( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test update entity progress.""" + node = climate_radio_thermostat_ct100_plus_different_endpoints + client.async_send_command.return_value = FIRMWARE_UPDATES + + 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) + + event = Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": node.node_id, + "sentFragments": 1, + "totalFragments": 20, + }, + ) + node.receive_event(event) + + # Validate that the progress is updated + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 5 + + event = Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": node.node_id, + "status": FirmwareUpdateStatus.OK_NO_RESTART, + }, + ) + + node.receive_event(event) + await hass.async_block_till_done() + + # 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] is False + 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_install_failed( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, + caplog, +): + """Test update entity install returns error status.""" + node = climate_radio_thermostat_ct100_plus_different_endpoints + client.async_send_command.return_value = FIRMWARE_UPDATES + + 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 + + async def call_install(): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + 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) + + event = Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": node.node_id, + "sentFragments": 1, + "totalFragments": 20, + }, + ) + node.receive_event(event) + + # Validate that the progress is updated + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] == 5 + + event = Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": node.node_id, + "status": FirmwareUpdateStatus.ERROR_TIMEOUT, + }, + ) + + node.receive_event(event) + await hass.async_block_till_done() + + # Validate that progress is reset and entity reflects old version + state = hass.states.get(UPDATE_ENTITY) + assert state + attrs = state.attributes + 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 + + # validate that the install task failed + with pytest.raises(HomeAssistantError): + await install_task From 2b961fd327c7538cea585622467471acedf6f8ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Sep 2022 14:35:23 +0200 Subject: [PATCH 111/231] Improve unique_id collision checks in entity_platform (#78132) --- homeassistant/helpers/entity_platform.py | 82 +++++++++++++++--------- tests/helpers/test_entity_platform.py | 37 +++++++++-- 2 files changed, 83 insertions(+), 36 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index c5c61cc1b0d..81487bbb627 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -454,6 +454,22 @@ class EntityPlatform: self.scan_interval, ) + def _entity_id_already_exists(self, entity_id: str) -> tuple[bool, bool]: + """Check if an entity_id already exists. + + Returns a tuple [already_exists, restored] + """ + already_exists = entity_id in self.entities + restored = False + + if not already_exists and not self.hass.states.async_available(entity_id): + existing = self.hass.states.get(entity_id) + if existing is not None and ATTR_RESTORED in existing.attributes: + restored = True + else: + already_exists = True + return (already_exists, restored) + async def _async_add_entity( # noqa: C901 self, entity: Entity, @@ -480,12 +496,31 @@ class EntityPlatform: entity.add_to_platform_abort() return - requested_entity_id = None suggested_object_id: str | None = None generate_new_entity_id = False # Get entity_id from unique ID registration if entity.unique_id is not None: + registered_entity_id = entity_registry.async_get_entity_id( + self.domain, self.platform_name, entity.unique_id + ) + if registered_entity_id: + already_exists, _ = self._entity_id_already_exists(registered_entity_id) + + if already_exists: + # If there's a collision, the entry belongs to another entity + entity.registry_entry = None + msg = ( + f"Platform {self.platform_name} does not generate unique IDs. " + ) + if entity.entity_id: + msg += f"ID {entity.unique_id} is already used by {registered_entity_id} - ignoring {entity.entity_id}" + else: + msg += f"ID {entity.unique_id} already exists - ignoring {registered_entity_id}" + self.logger.error(msg) + entity.add_to_platform_abort() + return + if self.config_entry is not None: config_entry_id: str | None = self.config_entry.entry_id else: @@ -541,7 +576,6 @@ class EntityPlatform: pass if entity.entity_id is not None: - requested_entity_id = entity.entity_id suggested_object_id = split_entity_id(entity.entity_id)[1] else: if device and entity.has_entity_name: # type: ignore[unreachable] @@ -592,16 +626,6 @@ class EntityPlatform: entity.registry_entry = entry entity.entity_id = entry.entity_id - if entry.disabled: - self.logger.debug( - "Not adding entity %s because it's disabled", - entry.name - or entity.name - or f'"{self.platform_name} {entity.unique_id}"', - ) - entity.add_to_platform_abort() - return - # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID elif entity.entity_id is not None and entity_registry.async_is_registered( @@ -628,28 +652,22 @@ class EntityPlatform: entity.add_to_platform_abort() raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}") - already_exists = entity.entity_id in self.entities - restored = False - - if not already_exists and not self.hass.states.async_available( - entity.entity_id - ): - existing = self.hass.states.get(entity.entity_id) - if existing is not None and ATTR_RESTORED in existing.attributes: - restored = True - else: - already_exists = True + already_exists, restored = self._entity_id_already_exists(entity.entity_id) if already_exists: - if entity.unique_id is not None: - msg = f"Platform {self.platform_name} does not generate unique IDs. " - if requested_entity_id: - msg += f"ID {entity.unique_id} is already used by {entity.entity_id} - ignoring {requested_entity_id}" - else: - msg += f"ID {entity.unique_id} already exists - ignoring {entity.entity_id}" - else: - msg = f"Entity id already exists - ignoring: {entity.entity_id}" - self.logger.error(msg) + self.logger.error( + f"Entity id already exists - ignoring: {entity.entity_id}" + ) + entity.add_to_platform_abort() + return + + if entity.registry_entry and entity.registry_entry.disabled: + self.logger.debug( + "Not adding entity %s because it's disabled", + entry.name + or entity.name + or f'"{self.platform_name} {entity.unique_id}"', + ) entity.add_to_platform_abort() return diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index d7f77eeacda..b2af85ca631 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -438,13 +438,15 @@ async def test_async_remove_with_platform_update_finishes(hass): async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog): - """Test for not adding duplicate entities.""" + """Test for not adding duplicate entities. + + Also test that the entity registry is not updated for duplicates. + """ caplog.set_level(logging.ERROR) component = EntityComponent(_LOGGER, DOMAIN, hass) - await component.async_add_entities( - [MockEntity(name="test1", unique_id="not_very_unique")] - ) + ent1 = MockEntity(name="test1", unique_id="not_very_unique") + await component.async_add_entities([ent1]) assert len(hass.states.async_entity_ids()) == 1 assert not caplog.text @@ -466,6 +468,11 @@ async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog): assert ent2.platform is None assert len(hass.states.async_entity_ids()) == 1 + registry = er.async_get(hass) + # test the entity name was not updated + entry = registry.async_get_or_create(DOMAIN, DOMAIN, "not_very_unique") + assert entry.original_name == "test1" + async def test_using_prescribed_entity_id(hass): """Test for using predefined entity ID.""" @@ -577,6 +584,28 @@ async def test_registry_respect_entity_disabled(hass): assert hass.states.async_entity_ids() == [] +async def test_unique_id_conflict_has_priority_over_disabled_entity(hass, caplog): + """Test that an entity that is not unique has priority over a disabled entity.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity( + name="test1", unique_id="not_very_unique", enabled_by_default=False + ) + entity2 = MockEntity( + name="test2", unique_id="not_very_unique", enabled_by_default=False + ) + await component.async_add_entities([entity1]) + await component.async_add_entities([entity2]) + + assert len(hass.states.async_entity_ids()) == 1 + assert "Platform test_domain does not generate unique IDs." in caplog.text + assert entity1.registry_entry is not None + assert entity2.registry_entry is None + registry = er.async_get(hass) + # test the entity name was not updated + entry = registry.async_get_or_create(DOMAIN, DOMAIN, "not_very_unique") + assert entry.original_name == "test1" + + async def test_entity_registry_updates_name(hass): """Test that updates on the entity registry update platform entities.""" registry = mock_registry( From 27c0a37053c0392b3488f150f8652966e408aea5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 Sep 2022 16:05:14 +0200 Subject: [PATCH 112/231] Allow non-integers in threshold sensor config flow (#78137) --- homeassistant/components/threshold/config_flow.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 1e6236259bd..45ccdcb4a5c 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -30,13 +30,19 @@ OPTIONS_SCHEMA = vol.Schema( vol.Required( CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS ): selector.NumberSelector( - selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, step=1e-3 + ), ), vol.Optional(CONF_LOWER): selector.NumberSelector( - selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, step=1e-3 + ), ), vol.Optional(CONF_UPPER): selector.NumberSelector( - selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX), + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, step=1e-3 + ), ), } ) From 06116f76fa18a072698c33544562fae84e0ad0c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 12:56:21 -0500 Subject: [PATCH 113/231] Bump bluetooth-adapters to 0.3.6 (#78138) --- 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 3043d6412a4..cda4158086f 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.5", + "bluetooth-adapters==0.3.6", "bluetooth-auto-recovery==0.3.2" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1a1fcb9da84..bf93aeba6e5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.5 +bluetooth-adapters==0.3.6 bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 49fcc1876ec..9328cb40a0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.5 +bluetooth-adapters==0.3.6 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ebf003e88e..3de7f120672 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.0 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.5 +bluetooth-adapters==0.3.6 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 From b24f3725d69dc7148aa6441f6f2b45c415d86b3f Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Fri, 9 Sep 2022 16:36:48 +0200 Subject: [PATCH 114/231] Add missing strings for errors in amberelectric config flow (#78140) --- homeassistant/components/amberelectric/strings.json | 5 +++++ homeassistant/components/amberelectric/translations/en.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/amberelectric/strings.json b/homeassistant/components/amberelectric/strings.json index 61d2c061955..5235a8bf325 100644 --- a/homeassistant/components/amberelectric/strings.json +++ b/homeassistant/components/amberelectric/strings.json @@ -15,6 +15,11 @@ }, "description": "Select the NMI of the site you would like to add" } + }, + "error": { + "invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]", + "no_site": "No site provided", + "unknown_error": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/amberelectric/translations/en.json b/homeassistant/components/amberelectric/translations/en.json index b4d30925aa7..0a974298134 100644 --- a/homeassistant/components/amberelectric/translations/en.json +++ b/homeassistant/components/amberelectric/translations/en.json @@ -15,6 +15,11 @@ }, "description": "Go to {api_url} to generate an API key" } + }, + "error": { + "invalid_api_token": "Invalid API key", + "no_site": "No site provided", + "unknown_error": "Unexpected error" } } } \ No newline at end of file From 78802c84804ff7a96f8a92ff2ec0626eb85a8ae0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 10 Sep 2022 04:31:10 +0200 Subject: [PATCH 115/231] Bump aioecowitt to 2022.09.1 (#78142) --- homeassistant/components/ecowitt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 348df17b0cd..080b7d5af72 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -3,7 +3,7 @@ "name": "Ecowitt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", - "requirements": ["aioecowitt==2022.08.3"], + "requirements": ["aioecowitt==2022.09.1"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 9328cb40a0c..00adc14d3ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.08.3 +aioecowitt==2022.09.1 # homeassistant.components.emonitor aioemonitor==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3de7f120672..cf3341f5728 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.08.3 +aioecowitt==2022.09.1 # homeassistant.components.emonitor aioemonitor==1.0.5 From 258791626e897396a11819429cf746ae069da840 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 10 Sep 2022 03:31:51 +0100 Subject: [PATCH 116/231] Add missing moisture sensor to xiaomi_ble (#78160) --- .../components/xiaomi_ble/binary_sensor.py | 3 ++ .../xiaomi_ble/test_binary_sensor.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 448b1f176e5..4de491ab9dd 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -39,6 +39,9 @@ BINARY_SENSOR_DESCRIPTIONS = { key=XiaomiBinarySensorDeviceClass.SMOKE, device_class=BinarySensorDeviceClass.SMOKE, ), + XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOISTURE, + ), } diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 03e3d52d783..390c8d4b579 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -52,3 +52,48 @@ async def test_smoke_sensor(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_moisture(hass): + """Make sure that formldehyde sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1014, payload len is 0x2 and payload is 0xf400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x14\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("binary_sensor.smart_flower_pot_6a3e7a_moisture") + sensor_attr = sensor.attributes + assert sensor.state == "on" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Moisture" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From e4aab6a818840d465e82c408caca59fdbf9bbeb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 21:32:17 -0500 Subject: [PATCH 117/231] Bump pySwitchbot to 0.19.1 (#78168) --- 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 e311295e52c..7c5b0d9bc8d 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.0"], + "requirements": ["PySwitchbot==0.19.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 00adc14d3ca..ea22719551b 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.0 +PySwitchbot==0.19.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf3341f5728..ff7b7c798e4 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.0 +PySwitchbot==0.19.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 57717f13fc5d470f89e94c50b6ace1662170029c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Sep 2022 22:39:13 -0400 Subject: [PATCH 118/231] Bumped version to 2022.9.2 --- 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 38a50aa8fc3..f1b41fd57e0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __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 10bab1ac2f7..d411a1c62bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.1" +version = "2022.9.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1789a8a3852041864b027b686f8134c386bf4d4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Sep 2022 22:20:39 -0500 Subject: [PATCH 119/231] Bump aiohomekit to 1.5.3 (#78170) --- 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 08eac050c98..1055779d7cc 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==1.5.2"], + "requirements": ["aiohomekit==1.5.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 ea22719551b..8f171702ced 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==1.5.2 +aiohomekit==1.5.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff7b7c798e4..4614825f9fe 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==1.5.2 +aiohomekit==1.5.3 # homeassistant.components.emulated_hue # homeassistant.components.http From c731e2f125544898faf2fdf2991e6f57dd945b36 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 9 Sep 2022 23:32:14 -0400 Subject: [PATCH 120/231] Fix ecowitt typing (#78171) --- homeassistant/components/ecowitt/binary_sensor.py | 2 +- homeassistant/components/ecowitt/diagnostics.py | 4 ++-- homeassistant/components/ecowitt/sensor.py | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ecowitt/binary_sensor.py b/homeassistant/components/ecowitt/binary_sensor.py index e487009d74b..fbe2e017339 100644 --- a/homeassistant/components/ecowitt/binary_sensor.py +++ b/homeassistant/components/ecowitt/binary_sensor.py @@ -68,4 +68,4 @@ class EcowittBinarySensorEntity(EcowittEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.ecowitt.value > 0 + return bool(self.ecowitt.value) diff --git a/homeassistant/components/ecowitt/diagnostics.py b/homeassistant/components/ecowitt/diagnostics.py index d02a5dadbcc..96fa020667b 100644 --- a/homeassistant/components/ecowitt/diagnostics.py +++ b/homeassistant/components/ecowitt/diagnostics.py @@ -25,13 +25,13 @@ async def async_get_device_diagnostics( "device": { "name": station.station, "model": station.model, - "frequency": station.frequency, + "frequency": station.frequence, "version": station.version, }, "raw": ecowitt.last_values[station_id], "sensors": { sensor.key: sensor.value - for sensor in station.sensors + for sensor in ecowitt.sensors.values() if sensor.station.key == station_id }, } diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index 843dc700dc0..bb580b6d4b7 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -1,5 +1,8 @@ """Support for Ecowitt Weather Stations.""" +from __future__ import annotations + import dataclasses +from datetime import datetime from typing import Final from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes @@ -242,6 +245,6 @@ class EcowittSensorEntity(EcowittEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.ecowitt.value From cdd5c809bb702cd57fc128974d08ce59447eb40d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Sat, 10 Sep 2022 20:35:26 +0300 Subject: [PATCH 121/231] Fix sending notification to multiple targets in Pushover (#78111) fix sending to mulitple targets --- homeassistant/components/pushover/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index dd711c80aaf..bcf47264108 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -128,7 +128,7 @@ class PushoverNotificationService(BaseNotificationService): self.pushover.send_message( self._user_key, message, - kwargs.get(ATTR_TARGET), + ",".join(kwargs.get(ATTR_TARGET, [])), title, url, url_title, From 795be361b460a945d4c4eea2fa09d244964bfe6d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 10 Sep 2022 17:02:36 +0200 Subject: [PATCH 122/231] Add dependencies to ecowitt (#78187) --- homeassistant/components/ecowitt/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 080b7d5af72..224b4440e36 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -3,6 +3,7 @@ "name": "Ecowitt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", + "dependencies": ["webhook"], "requirements": ["aioecowitt==2022.09.1"], "codeowners": ["@pvizeli"], "iot_class": "local_push" From 5f907601768df20e431ca1353bd14faaa6019f08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 12:30:30 -0500 Subject: [PATCH 123/231] Bump led-ble to 0.8.3 (#78188) * Bump led-ble to 0.8.0 Fixes setup when the previous shutdown was not clean and the device is still connected * bump again * bump again * bump again --- homeassistant/components/led_ble/__init__.py | 6 ++++-- homeassistant/components/led_ble/light.py | 4 ++-- homeassistant/components/led_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py index d885b3eb950..5c454c6df7c 100644 --- a/homeassistant/components/led_ble/__init__.py +++ b/homeassistant/components/led_ble/__init__.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging import async_timeout -from led_ble import BLEAK_EXCEPTIONS, LEDBLE +from led_ble import BLEAK_EXCEPTIONS, LEDBLE, get_device from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher @@ -27,7 +27,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LED BLE from a config entry.""" address: str = entry.data[CONF_ADDRESS] - ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True) + ble_device = bluetooth.async_ble_device_from_address( + hass, address.upper(), True + ) or await get_device(address) if not ble_device: raise ConfigEntryNotReady( f"Could not find LED BLE device with address {address}" diff --git a/homeassistant/components/led_ble/light.py b/homeassistant/components/led_ble/light.py index a18ab812b19..4a8ff3f01af 100644 --- a/homeassistant/components/led_ble/light.py +++ b/homeassistant/components/led_ble/light.py @@ -48,12 +48,12 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity): """Initialize an ledble light.""" super().__init__(coordinator) self._device = device - self._attr_unique_id = device._address + self._attr_unique_id = device.address self._attr_device_info = DeviceInfo( name=name, model=hex(device.model_num), sw_version=hex(device.version_num), - connections={(dr.CONNECTION_BLUETOOTH, device._address)}, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, ) self._async_update_attrs() diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 1dd289daa4d..261d27726e5 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.7.1"], + "requirements": ["led-ble==0.8.3"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8f171702ced..2bc648329ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.1 +led-ble==0.8.3 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4614825f9fe..bc8692ee18c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.7.1 +led-ble==0.8.3 # homeassistant.components.foscam libpyfoscam==1.0 From a969ce273aa778c9ee3e4cb4935132b1ca186548 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 12:32:38 -0500 Subject: [PATCH 124/231] Fix switchbot not setting up when already connected at startup (#78198) --- homeassistant/components/switchbot/__init__.py | 2 +- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 345190d8933..d1f68047a83 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -84,7 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address: str = entry.data[CONF_ADDRESS] ble_device = bluetooth.async_ble_device_from_address( hass, address.upper(), connectable - ) + ) or await switchbot.get_device(address) if not ble_device: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}" diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 7c5b0d9bc8d..f35f3cddf72 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.1"], + "requirements": ["PySwitchbot==0.19.2"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2bc648329ab..785232fdb20 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.1 +PySwitchbot==0.19.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc8692ee18c..7b741c06781 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.1 +PySwitchbot==0.19.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 95c20df367b63b021ae143802ab55782cbc7f335 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 12:32:29 -0500 Subject: [PATCH 125/231] Fix Yale Access Bluetooth not setting up when already connected at startup (#78199) --- 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 de0034c755f..da4bf1cf6d2 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.6.4"], + "requirements": ["yalexs-ble==1.7.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 785232fdb20..599a606ee74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2542,7 +2542,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.4 +yalexs-ble==1.7.1 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b741c06781..3ccb693bd23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1746,7 +1746,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.4 +yalexs-ble==1.7.1 # homeassistant.components.august yalexs==1.2.1 From 183c61b6cac4dd805ae5c5f41cd261008b5c3e88 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 10 Sep 2022 13:56:01 -0400 Subject: [PATCH 126/231] Bump ZHA dependencies (#78201) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/test_config_flow.py | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 2e35427a70c..419e1c1452b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -8,8 +8,8 @@ "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.79", - "zigpy-deconz==0.18.0", - "zigpy==0.50.2", + "zigpy-deconz==0.18.1", + "zigpy==0.50.3", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.2", "zigpy-znp==0.8.2" diff --git a/requirements_all.txt b/requirements_all.txt index 599a606ee74..dd909ca8205 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2578,7 +2578,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.18.0 +zigpy-deconz==0.18.1 # homeassistant.components.zha zigpy-xbee==0.15.0 @@ -2590,7 +2590,7 @@ zigpy-zigate==0.9.2 zigpy-znp==0.8.2 # homeassistant.components.zha -zigpy==0.50.2 +zigpy==0.50.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ccb693bd23..4450a6a26e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1767,7 +1767,7 @@ zeroconf==0.39.1 zha-quirks==0.0.79 # homeassistant.components.zha -zigpy-deconz==0.18.0 +zigpy-deconz==0.18.1 # homeassistant.components.zha zigpy-xbee==0.15.0 @@ -1779,7 +1779,7 @@ zigpy-zigate==0.9.2 zigpy-znp==0.8.2 # homeassistant.components.zha -zigpy==0.50.2 +zigpy==0.50.3 # homeassistant.components.zwave_js zwave-js-server-python==0.41.1 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index d65732a6ab8..5fc4b232634 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -2,11 +2,12 @@ import copy import json -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch import uuid import pytest import serial.tools.list_ports +from zigpy.backups import BackupManager import zigpy.config from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkNotFormed @@ -49,6 +50,7 @@ def disable_platform_only(): def mock_app(): """Mock zigpy app interface.""" mock_app = AsyncMock() + mock_app.backups = create_autospec(BackupManager, instance=True) mock_app.backups.backups = [] with patch( From c7cb0d1a07f5a4d4ae6ca7f1550e7f5866fd1d80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 13:21:10 -0500 Subject: [PATCH 127/231] Close stale switchbot connections at setup time (#78202) --- homeassistant/components/switchbot/__init__.py | 2 ++ homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index d1f68047a83..7307187bf54 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -89,6 +89,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}" ) + + await switchbot.close_stale_connections(ble_device) cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) device = cls( device=ble_device, diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f35f3cddf72..c83e575bd49 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.2"], + "requirements": ["PySwitchbot==0.19.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index dd909ca8205..3bd9e23e5f9 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.2 +PySwitchbot==0.19.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4450a6a26e6..3b858b0344a 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.2 +PySwitchbot==0.19.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From a626ab4f1a40e27a3681d4bf1da69d16698cefa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Sep 2022 13:36:44 -0500 Subject: [PATCH 128/231] Bump aiohomekit to 1.5.4 to handle stale ble connections at startup (#78203) --- 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 1055779d7cc..b71156c9bc7 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==1.5.3"], + "requirements": ["aiohomekit==1.5.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 3bd9e23e5f9..0f10e099606 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==1.5.3 +aiohomekit==1.5.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b858b0344a..361b7212cf5 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==1.5.3 +aiohomekit==1.5.4 # homeassistant.components.emulated_hue # homeassistant.components.http From 6e88b8d3d551ddf598e20afe6736c1f3c74e331b Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Sun, 11 Sep 2022 18:18:01 +0200 Subject: [PATCH 129/231] Landis+Gyr integration: increase timeout and add debug logging (#78025) --- homeassistant/components/landisgyr_heat_meter/__init__.py | 4 +--- .../components/landisgyr_heat_meter/config_flow.py | 6 ++++-- homeassistant/components/landisgyr_heat_meter/const.py | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 4321f53bed6..3ef235ff8af 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -31,9 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.info("Polling on %s", entry.data[CONF_DEVICE]) return await hass.async_add_executor_job(api.read) - # No automatic polling and no initial refresh of data is being done at this point, - # to prevent battery drain. The user will have to do it manually. - + # Polling is only daily to prevent battery drain. coordinator = DataUpdateCoordinator( hass, _LOGGER, diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index e3dbbb7433b..2e244a9a65f 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_DEVICE from homeassistant.exceptions import HomeAssistantError -from .const import DOMAIN +from .const import DOMAIN, ULTRAHEAT_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -43,6 +43,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): dev_path = await self.hass.async_add_executor_job( get_serial_by_id, user_input[CONF_DEVICE] ) + _LOGGER.debug("Using this path : %s", dev_path) try: return await self.validate_and_create_entry(dev_path) @@ -76,6 +77,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Try to connect to the device path and return an entry.""" model, device_number = await self.validate_ultraheat(dev_path) + _LOGGER.debug("Got model %s and device_number %s", model, device_number) await self.async_set_unique_id(device_number) self._abort_if_unique_id_configured() data = { @@ -94,7 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): reader = UltraheatReader(port) heat_meter = HeatMeterService(reader) try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(ULTRAHEAT_TIMEOUT): # validate and retrieve the model and device number for a unique id data = await self.hass.async_add_executor_job(heat_meter.read) _LOGGER.debug("Got data from Ultraheat API: %s", data) diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 70008890d1f..55a6c65892c 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity import EntityCategory DOMAIN = "landisgyr_heat_meter" GJ_TO_MWH = 0.277778 # conversion factor +ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time HEAT_METER_SENSOR_TYPES = ( SensorEntityDescription( From 1e8f461270aefbf2cd8e3f9398a6cc25b05c8f3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 03:06:13 -0500 Subject: [PATCH 130/231] Bump bluetooth-adapters to 0.4.1 (#78205) Switches to dbus-fast which fixes a file descriptor leak --- 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 cda4158086f..3c9ef7a85b7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.16.0", - "bluetooth-adapters==0.3.6", + "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.2" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bf93aeba6e5..4f41518361f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 -bluetooth-adapters==0.3.6 +bluetooth-adapters==0.4.1 bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0f10e099606..5122e4aeb5d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.6 +bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 361b7212cf5..b94fb9eb73f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ blinkpy==0.19.0 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.3.6 +bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.2 From 4b79e82e317e7c6cf1ba0f328e43ef01d6b3e07d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 11 Sep 2022 02:17:36 -0600 Subject: [PATCH 131/231] Bump `regenmaschine` to 2022.09.1 (#78210) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index a6cc86e5055..6a87110f6f6 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.0"], + "requirements": ["regenmaschine==2022.09.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 5122e4aeb5d..fd9db0d274a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,7 +2118,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.09.0 +regenmaschine==2022.09.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b94fb9eb73f..3c96e15c671 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.09.0 +regenmaschine==2022.09.1 # homeassistant.components.renault renault-api==0.1.11 From 4d4a87ba05c5f2247a58dc6c027face195c1ecbc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 03:04:05 -0500 Subject: [PATCH 132/231] Bump led_ble to 0.8.5 (#78215) * Bump led_ble to 0.8.4 Changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v0.8.3...v0.8.4 * bump again --- homeassistant/components/led_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/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 261d27726e5..9bcb3f860e1 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.8.3"], + "requirements": ["led-ble==0.8.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index fd9db0d274a..d8594b03594 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.3 +led-ble==0.8.5 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c96e15c671..f453226f93c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.3 +led-ble==0.8.5 # homeassistant.components.foscam libpyfoscam==1.0 From de8b066a1d714ea3e43ca035c7a94abc064c4c12 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 Sep 2022 18:18:38 +0200 Subject: [PATCH 133/231] Bump pysensibo to 1.0.20 (#78222) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index a2a7cbe3bd0..4f89148a21c 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.19"], + "requirements": ["pysensibo==1.0.20"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index d8594b03594..0c4ffbb8c78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.19 +pysensibo==1.0.20 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f453226f93c..5e125ce07a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1285,7 +1285,7 @@ pyruckus==0.16 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.19 +pysensibo==1.0.20 # homeassistant.components.serial # homeassistant.components.zha From e7986a54a5403d692dc40256b49423aadd879083 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:19:31 -0500 Subject: [PATCH 134/231] Bump PySwitchbot to 0.19.5 (#78224) --- 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 c83e575bd49..e1e832f854a 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.3"], + "requirements": ["PySwitchbot==0.19.5"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0c4ffbb8c78..333809983d4 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.3 +PySwitchbot==0.19.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e125ce07a3..9bb916a069e 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.3 +PySwitchbot==0.19.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 1b7a06912a6ed34389fda29ee552e18ed819cde6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:18:52 -0500 Subject: [PATCH 135/231] Bump yalexs-ble to 1.8.1 (#78225) --- 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 da4bf1cf6d2..a685d750077 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.7.1"], + "requirements": ["yalexs-ble==1.8.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 333809983d4..eede3512e9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2542,7 +2542,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.7.1 +yalexs-ble==1.8.1 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bb916a069e..892f5fbb9f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1746,7 +1746,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.7.1 +yalexs-ble==1.8.1 # homeassistant.components.august yalexs==1.2.1 From a277664187abd7c5dd9519a8cd13c8b2b6c3ef23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:19:01 -0500 Subject: [PATCH 136/231] Bump led-ble to 0.9.1 (#78226) --- homeassistant/components/led_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/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 9bcb3f860e1..1f27837b89e 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.8.5"], + "requirements": ["led-ble==0.9.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index eede3512e9c..aadcbd9cdd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.5 +led-ble==0.9.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 892f5fbb9f2..0edfe2c7e8d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.8.5 +led-ble==0.9.1 # homeassistant.components.foscam libpyfoscam==1.0 From 296db8b2afa2794c67b0c5a5ecc6af19cf15c225 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 11:19:12 -0500 Subject: [PATCH 137/231] Bump aiohomekit to 1.5.6 (#78228) --- 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 b71156c9bc7..a4cbbc227d9 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==1.5.4"], + "requirements": ["aiohomekit==1.5.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 aadcbd9cdd9..4038e1863f4 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==1.5.4 +aiohomekit==1.5.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0edfe2c7e8d..3f072512375 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==1.5.4 +aiohomekit==1.5.6 # homeassistant.components.emulated_hue # homeassistant.components.http From f18ab504a552a93c8e59e65c2cb6cba350576701 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 2 Sep 2022 03:33:55 +0200 Subject: [PATCH 138/231] Move up setup of service to make it more robust when running multiple instances of deCONZ (#77621) --- homeassistant/components/deconz/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index cb4715f3c28..4750c40fab2 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -43,6 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err + if not hass.data[DOMAIN]: + async_setup_services(hass) + gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway( hass, config_entry, api ) @@ -53,9 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await async_setup_events(gateway) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - if len(hass.data[DOMAIN]) == 1: - async_setup_services(hass) - api.start() config_entry.async_on_unload( From 184a1c95f0aa312fd64b29ddd9af25eb9567fe9c Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Tue, 13 Sep 2022 16:46:31 +0200 Subject: [PATCH 139/231] Bump blinkpy to 0.19.2 (#78097) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index c302f2dab6c..aa3cad317ac 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,7 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.19.0"], + "requirements": ["blinkpy==0.19.2"], "codeowners": ["@fronzbot"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 4038e1863f4..0e1af72f066 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ bleak==0.16.0 blebox_uniapi==2.0.2 # homeassistant.components.blink -blinkpy==0.19.0 +blinkpy==0.19.2 # homeassistant.components.blinksticklight blinkstick==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f072512375..3cded1bf00c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ bleak==0.16.0 blebox_uniapi==2.0.2 # homeassistant.components.blink -blinkpy==0.19.0 +blinkpy==0.19.2 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.0 From 3129114d07af35800d3b77806859f582f93d6f0c Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 13 Sep 2022 13:01:13 +0200 Subject: [PATCH 140/231] Bump PyViCare==2.17.0 (#78232) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 575ca35729b..c770f1b2382 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.16.2"], + "requirements": ["PyViCare==2.17.0"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0e1af72f066..09448308a01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cded1bf00c..6d3e863708a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From 9640553b529442594a5850d83c8442abc94543d9 Mon Sep 17 00:00:00 2001 From: d-walsh Date: Mon, 12 Sep 2022 19:03:07 -0400 Subject: [PATCH 141/231] Fix missing dependency for dbus_next (#78235) --- homeassistant/components/bluetooth/manifest.json | 3 ++- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3c9ef7a85b7..55a7e610e76 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,8 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.4.1", - "bluetooth-auto-recovery==0.3.2" + "bluetooth-auto-recovery==0.3.2", + "dbus_next==0.2.3" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4f41518361f..1e4a0975a1a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,6 +16,7 @@ bluetooth-auto-recovery==0.3.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 +dbus_next==0.2.3 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 09448308a01..6e49b75c0dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -534,6 +534,9 @@ datadog==0.15.0 # homeassistant.components.metoffice datapoint==0.9.8 +# homeassistant.components.bluetooth +dbus_next==0.2.3 + # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d3e863708a..cd195516a73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -411,6 +411,9 @@ datadog==0.15.0 # homeassistant.components.metoffice datapoint==0.9.8 +# homeassistant.components.bluetooth +dbus_next==0.2.3 + # homeassistant.components.debugpy debugpy==1.6.3 From f6d26476b571c7996b2aa4537916b54fb0b7c866 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 19:16:46 -0500 Subject: [PATCH 142/231] Bump bluetooth-auto-recovery to 0.3.3 (#78245) Downgrades a few more loggers to debug since the only reason we check them is to give a better error message when the bluetooth adapter is blocked by rfkill. https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/compare/v0.3.2...v0.3.3 closes #78211 --- 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 55a7e610e76..434b4c1127e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.16.0", "bluetooth-adapters==0.4.1", - "bluetooth-auto-recovery==0.3.2", + "bluetooth-auto-recovery==0.3.3", "dbus_next==0.2.3" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1e4a0975a1a..f9c92c8fdea 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.8.0 bcrypt==3.1.7 bleak==0.16.0 bluetooth-adapters==0.4.1 -bluetooth-auto-recovery==0.3.2 +bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 6e49b75c0dd..1ac6009d871 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.2 +bluetooth-auto-recovery==0.3.3 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd195516a73..6215cd68cb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ bluemaestro-ble==0.2.0 bluetooth-adapters==0.4.1 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.3.2 +bluetooth-auto-recovery==0.3.3 # homeassistant.components.bond bond-async==0.1.22 From 95a89448e043c54269213ca60d44c300b4d1cd5e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 05:19:45 -0500 Subject: [PATCH 143/231] Bump aiodiscover to 1.4.13 (#78253) --- homeassistant/components/dhcp/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/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index fea7772f72d..f3f44f6dc9b 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.11"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.13"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f9c92c8fdea..dbbfda9d6db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==2.4.0 PyNaCl==1.5.0 -aiodiscover==1.4.11 +aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index 1ac6009d871..6d00afceb36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -134,7 +134,7 @@ aiobafi6==0.7.2 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.11 +aiodiscover==1.4.13 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6215cd68cb5..2780b516ce4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -121,7 +121,7 @@ aiobafi6==0.7.2 aiobotocore==2.1.0 # homeassistant.components.dhcp -aiodiscover==1.4.11 +aiodiscover==1.4.13 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From d6bf1a8caa10b3af52ee2cbf5df34b82be508619 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 14:18:26 -0500 Subject: [PATCH 144/231] Bump pySwitchbot to 0.19.6 (#78304) No longer swallows exceptions from bleak connection errors which was hiding the root cause of problems. This was the original behavior from a long time ago which does not make sense anymore since we retry a few times anyways https://github.com/Danielhiversen/pySwitchbot/compare/0.19.5...0.19.6 --- 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 e1e832f854a..b2a5b68deae 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.5"], + "requirements": ["PySwitchbot==0.19.6"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6d00afceb36..d2b6f6ef853 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.5 +PySwitchbot==0.19.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2780b516ce4..b1069836313 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.5 +PySwitchbot==0.19.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From a90b6d37bf34c9738cc11c578d6e4a81eb92a3c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 05:38:01 -0500 Subject: [PATCH 145/231] Make yalexs_ble matcher more specific (#78307) --- homeassistant/components/yalexs_ble/manifest.json | 7 ++++++- homeassistant/generated/bluetooth.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index a685d750077..f3a9cc798e6 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -6,7 +6,12 @@ "requirements": ["yalexs-ble==1.8.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], - "bluetooth": [{ "manufacturer_id": 465 }], + "bluetooth": [ + { + "manufacturer_id": 465, + "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb" + } + ], "iot_class": "local_push", "supported_brands": { "august_ble": "August Bluetooth" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index b2400f733da..59f10767e0d 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -281,6 +281,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ }, { "domain": "yalexs_ble", - "manufacturer_id": 465 + "manufacturer_id": 465, + "service_uuid": "0000fe24-0000-1000-8000-00805f9b34fb" } ] From 0aa2685e0c4657492fbaede5786b4769b45df989 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 12 Sep 2022 15:37:11 -0400 Subject: [PATCH 146/231] Fix sengled bulbs in ZHA (#78315) * Fix sengled bulbs in ZHA * fix tests * update discovery data --- homeassistant/components/zha/light.py | 2 +- tests/components/zha/test_light.py | 2 +- tests/components/zha/zha_devices_list.py | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 9858c6803f9..5b2df6d11cf 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -84,7 +84,7 @@ PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" -DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} +DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} SUPPORT_GROUP_LIGHT = ( diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 16678393f52..bc907e4252f 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -178,7 +178,7 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE2, - manufacturer="Sengled", + manufacturer="sengled", nwk=0xC79E, ) color_cluster = zigpy_device.endpoints[1].light_color diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 4cea8eb8f66..2d15f9335db 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -5493,7 +5493,7 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sengled_e11_g13_identifybutton", - "light.sengled_e11_g13_light", + "light.sengled_e11_g13_mintransitionlight", "sensor.sengled_e11_g13_smartenergymetering", "sensor.sengled_e11_g13_smartenergysummation", "sensor.sengled_e11_g13_rssi", @@ -5502,8 +5502,8 @@ DEVICES = [ DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_light", + DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", + DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_mintransitionlight", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], @@ -5549,7 +5549,7 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sengled_e12_n14_identifybutton", - "light.sengled_e12_n14_light", + "light.sengled_e12_n14_mintransitionlight", "sensor.sengled_e12_n14_smartenergymetering", "sensor.sengled_e12_n14_smartenergysummation", "sensor.sengled_e12_n14_rssi", @@ -5558,8 +5558,8 @@ DEVICES = [ DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_light", + DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", + DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_mintransitionlight", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], @@ -5605,7 +5605,7 @@ DEVICES = [ DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ "button.sengled_z01_a19nae26_identifybutton", - "light.sengled_z01_a19nae26_light", + "light.sengled_z01_a19nae26_mintransitionlight", "sensor.sengled_z01_a19nae26_smartenergymetering", "sensor.sengled_z01_a19nae26_smartenergysummation", "sensor.sengled_z01_a19nae26_rssi", @@ -5614,8 +5614,8 @@ DEVICES = [ DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_light", + DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", + DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_mintransitionlight", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], From 9acf74d78313a95d6902ca7c57099286dfc1a0e9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 03:24:49 +0200 Subject: [PATCH 147/231] Fix calculating gas cost for gas measured in ft3 (#78327) --- homeassistant/components/energy/sensor.py | 3 ++- tests/components/energy/test_sensor.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 4002f6c416d..602fc09f602 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) from homeassistant.core import ( @@ -44,7 +45,7 @@ SUPPORTED_STATE_CLASSES = [ SensorStateClass.TOTAL_INCREASING, ] VALID_ENERGY_UNITS = [ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR] -VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS +VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 997f60a8899..9fa82ead2a1 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, STATE_UNKNOWN, + VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) from homeassistant.helpers import entity_registry as er @@ -841,10 +842,13 @@ async def test_cost_sensor_handle_price_units( assert state.state == "20.0" -async def test_cost_sensor_handle_gas(hass, hass_storage, setup_integration) -> None: +@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +async def test_cost_sensor_handle_gas( + hass, hass_storage, setup_integration, unit +) -> None: """Test gas cost price from sensor entity.""" energy_attributes = { - ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_UNIT_OF_MEASUREMENT: unit, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() From 47f6be77cc53053bc23b905c0fac3df7cdd0f5b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 14:31:34 +0200 Subject: [PATCH 148/231] Bump bleak to 0.17.0 (#78333) --- homeassistant/components/bluetooth/manifest.json | 5 +++-- homeassistant/components/bluetooth/scanner.py | 2 +- homeassistant/package_constraints.txt | 5 +++-- requirements_all.txt | 7 +++++-- requirements_test_all.txt | 7 +++++-- tests/components/bluetooth/test_scanner.py | 2 +- 6 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 434b4c1127e..acc8a6977e3 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -5,10 +5,11 @@ "dependencies": ["usb"], "quality_scale": "internal", "requirements": [ - "bleak==0.16.0", + "bleak==0.17.0", + "bleak-retry-connector==1.15.1", "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.3", - "dbus_next==0.2.3" + "dbus-fast==1.4.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 78979198e5c..184e8775f07 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -17,7 +17,7 @@ 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 dbus_next import InvalidMessageError +from dbus_fast import InvalidMessageError from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dbbfda9d6db..b511248a602 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,13 +10,14 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.8.0 bcrypt==3.1.7 -bleak==0.16.0 +bleak-retry-connector==1.15.1 +bleak==0.17.0 bluetooth-adapters==0.4.1 bluetooth-auto-recovery==0.3.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 -dbus_next==0.2.3 +dbus-fast==1.4.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index d2b6f6ef853..85cea15f0e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,10 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.16.0 +bleak-retry-connector==1.15.1 + +# homeassistant.components.bluetooth +bleak==0.17.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -535,7 +538,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus_next==0.2.3 +dbus-fast==1.4.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1069836313..df34b95ff56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,10 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak==0.16.0 +bleak-retry-connector==1.15.1 + +# homeassistant.components.bluetooth +bleak==0.17.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 @@ -412,7 +415,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus_next==0.2.3 +dbus-fast==1.4.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 26e949ad2e3..2c1810d8338 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -7,7 +7,7 @@ from bleak.backends.scanner import ( AdvertisementDataCallback, BLEDevice, ) -from dbus_next import InvalidMessageError +from dbus_fast import InvalidMessageError import pytest from homeassistant.components import bluetooth From 12edfb39293884f0830700e8c6aeb6fc227bdf04 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 14:39:05 +0200 Subject: [PATCH 149/231] Drop initial when loading input_number from storage (#78354) --- .../components/input_number/__init__.py | 16 ++++++++++++++++ tests/components/input_number/test_init.py | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index ff01bd124b6..0e56c1df15b 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -196,6 +196,22 @@ class NumberStorageCollection(collection.StorageCollection): """Suggest an ID based on the config.""" return info[CONF_NAME] + async def _async_load_data(self) -> dict | None: + """Load the data. + + A past bug caused frontend to add initial value to all input numbers. + This drops that. + """ + data = await super()._async_load_data() + + if data is None: + return data + + for number in data["items"]: + number.pop(CONF_INITIAL, None) + + return data + async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 4149627720b..bec05d3f344 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -416,7 +416,7 @@ async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() state = hass.states.get(f"{DOMAIN}.from_storage") - assert float(state.state) == 10 + assert float(state.state) == 0 # initial is not supported when loading from storage assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" assert state.attributes.get(ATTR_EDITABLE) @@ -438,7 +438,7 @@ async def test_editable_state_attribute(hass, storage_setup): ) state = hass.states.get(f"{DOMAIN}.from_storage") - assert float(state.state) == 10 + assert float(state.state) == 0 assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" assert state.attributes.get(ATTR_EDITABLE) From ad396f0538bd6f15340d0eb626e8d713f8737dca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:54:52 +0200 Subject: [PATCH 150/231] Don't allow partial update of input_number settings (#78356) --- .../components/input_number/__init__.py | 24 ++++---------- tests/components/input_number/test_init.py | 32 +++++++++++-------- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 0e56c1df15b..00aeca2ab34 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -65,7 +65,7 @@ def _cv_input_number(cfg): return cfg -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_MIN): vol.Coerce(float), vol.Required(CONF_MAX): vol.Coerce(float), @@ -76,17 +76,6 @@ CREATE_FIELDS = { vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]), } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN): vol.Coerce(float), - vol.Optional(CONF_MAX): vol.Coerce(float), - vol.Optional(CONF_INITIAL): vol.Coerce(float), - vol.Optional(CONF_STEP): vol.All(vol.Coerce(float), vol.Range(min=1e-9)), - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_MODE): vol.In([MODE_BOX, MODE_SLIDER]), -} - CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( @@ -148,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -184,12 +173,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class NumberStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_number)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_number)) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -214,8 +202,8 @@ class NumberStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return _cv_input_number({**data, **update_data}) + update_data = self.SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputNumber(RestoreEntity): diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index bec05d3f344..7ba7489f644 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -507,16 +507,14 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): async def test_update_min_max(hass, hass_ws_client, storage_setup): """Test updating min/max updates the state.""" - items = [ - { - "id": "from_storage", - "name": "from storage", - "max": 100, - "min": 0, - "step": 1, - "mode": "slider", - } - ] + settings = { + "name": "from storage", + "max": 100, + "min": 0, + "step": 1, + "mode": "slider", + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -530,26 +528,34 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = settings | {"min": 9} await client.send_json( - {"id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", "min": 9} + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + **updated_settings, + } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert float(state.state) == 9 + updated_settings = settings | {"max": 5} await client.send_json( { "id": 7, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "max": 5, - "min": 0, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert float(state.state) == 5 From bfcb9402ef4d2f36465d572b6fce46b075e26995 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:11:44 +0200 Subject: [PATCH 151/231] Bump PySwitchbot to 0.19.8 (#78361) * Bump PySwitchbot to 0.19.7 Changes for bleak 0.17 https://github.com/Danielhiversen/pySwitchbot/compare/0.19.6...0.19.7 * bump again to fix some more stale state bugs --- 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 b2a5b68deae..41b3f5aa61b 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.6"], + "requirements": ["PySwitchbot==0.19.8"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 85cea15f0e9..5da2446c36b 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.6 +PySwitchbot==0.19.8 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df34b95ff56..f9ee981b1a9 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.6 +PySwitchbot==0.19.8 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From b21a37cad51ed0c53795f0e3830f72a9e9eebeda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:14:53 +0200 Subject: [PATCH 152/231] Bump yalexs-ble to 1.9.0 (#78362) --- 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 f3a9cc798e6..673521f9e06 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.8.1"], + "requirements": ["yalexs-ble==1.9.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5da2446c36b..a46bc7c3193 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2548,7 +2548,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.8.1 +yalexs-ble==1.9.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9ee981b1a9..82508fd7cb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1752,7 +1752,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.8.1 +yalexs-ble==1.9.0 # homeassistant.components.august yalexs==1.2.1 From cd7f65bb6aefc45066e7b16833b1a7fcde5b997e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 16:54:20 -0500 Subject: [PATCH 153/231] Bump xiaomi-ble to 0.9.3 (#78301) --- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_binary_sensor.py | 8 +- .../components/xiaomi_ble/test_config_flow.py | 38 ++++---- tests/components/xiaomi_ble/test_sensor.py | 95 ++++++++----------- 6 files changed, 66 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index c01f0846234..de8e61ad8ce 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -9,7 +9,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.9.0"], + "requirements": ["xiaomi-ble==0.9.3"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a46bc7c3193..6ba52832cba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2528,7 +2528,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.0 +xiaomi-ble==0.9.3 # homeassistant.components.knx xknx==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82508fd7cb3..f5452a9140c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1735,7 +1735,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.0 +xiaomi-ble==0.9.3 # homeassistant.components.knx xknx==1.0.2 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 390c8d4b579..dd49b4d181d 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -45,10 +45,10 @@ async def test_smoke_sensor(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - smoke_sensor = hass.states.get("binary_sensor.thermometer_e39cbc_smoke") + smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") smoke_sensor_attribtes = smoke_sensor.attributes assert smoke_sensor.state == "on" - assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer E39CBC Smoke" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -90,10 +90,10 @@ async def test_moisture(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("binary_sensor.smart_flower_pot_6a3e7a_moisture") + sensor = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture") sensor_attr = sensor.attributes assert sensor.state == "on" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Moisture" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 6a1c9c8e435..a884e5cbefa 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -39,7 +39,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" + assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -65,7 +65,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" + assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -123,7 +123,7 @@ async def test_async_step_bluetooth_during_onboarding(hass): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" + assert result["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result["data"] == {} assert result["result"].unique_id == "00:81:F9:DD:6F:C1" assert len(mock_setup_entry.mock_calls) == 1 @@ -148,7 +148,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -180,7 +180,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key(has user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -214,7 +214,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -238,7 +238,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -272,7 +272,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -306,7 +306,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -370,7 +370,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "58:2D:34:35:93:21"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Temperature/Humidity Sensor 359321 (LYWSDCGQ)" + assert result2["title"] == "Temperature/Humidity Sensor 9321 (LYWSDCGQ)" assert result2["data"] == {} assert result2["result"].unique_id == "58:2D:34:35:93:21" @@ -405,7 +405,7 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={} ) assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" + assert result3["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result3["data"] == {} assert result3["result"].unique_id == "A4:C1:38:56:53:84" @@ -453,7 +453,7 @@ async def test_async_step_user_short_payload_then_full(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" + assert result2["title"] == "Temperature/Humidity Sensor 5384 (LYWSD03MMC)" assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} @@ -486,7 +486,7 @@ async def test_async_step_user_with_found_devices_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -532,7 +532,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -580,7 +580,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" + assert result2["title"] == "Thermometer 9CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -613,7 +613,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -658,7 +658,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -703,7 +703,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" + assert result2["title"] == "Dimmer Switch 988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -822,7 +822,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "00:81:F9:DD:6F:C1"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" + assert result2["title"] == "Baby Thermometer 6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index c4052d36bf4..25c118bed49 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -42,12 +42,11 @@ async def test_sensors(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - temp_sensor = hass.states.get("sensor.baby_thermometer_dd6fc1_temperature") + temp_sensor = hass.states.get("sensor.baby_thermometer_6fc1_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "36.8719980616822" assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] - == "Baby Thermometer DD6FC1 Temperature" + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Baby Thermometer 6FC1 Temperature" ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -92,10 +91,10 @@ async def test_xiaomi_formaldeyhde(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_formaldehyde") + sensor = hass.states.get("sensor.smart_flower_pot_3e7a_formaldehyde") sensor_attr = sensor.attributes assert sensor.state == "2.44" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Formaldehyde" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Formaldehyde" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -139,10 +138,10 @@ async def test_xiaomi_consumable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_consumable") + sensor = hass.states.get("sensor.smart_flower_pot_3e7a_consumable") sensor_attr = sensor.attributes assert sensor.state == "96" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Consumable" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Consumable" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -186,17 +185,17 @@ async def test_xiaomi_battery_voltage(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - volt_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_voltage") + volt_sensor = hass.states.get("sensor.smart_flower_pot_3e7a_voltage") volt_sensor_attr = volt_sensor.attributes assert volt_sensor.state == "3.1" - assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Voltage" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Voltage" assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" - bat_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_battery") + bat_sensor = hass.states.get("sensor.smart_flower_pot_3e7a_battery") bat_sensor_attr = bat_sensor.attributes assert bat_sensor.state == "100" - assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Battery" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Battery" assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -254,42 +253,38 @@ async def test_xiaomi_HHCCJCY01(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 - illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert ( - cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" - ) + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" - ) + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - batt_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_battery") + batt_sensor = hass.states.get("sensor.plant_sensor_3e7a_battery") batt_sensor_attribtes = batt_sensor.attributes assert batt_sensor.state == "5" - assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Battery" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Battery" assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -355,35 +350,31 @@ async def test_xiaomi_HHCCJCY01_not_connectable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 4 - illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert ( - cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" - ) + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" - ) + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -438,42 +429,38 @@ async def test_xiaomi_HHCCJCY01_only_some_sources_connectable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 - illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert ( - cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" - ) + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert ( - temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" - ) + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - batt_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_battery") + batt_sensor = hass.states.get("sensor.plant_sensor_3e7a_battery") batt_sensor_attribtes = batt_sensor.attributes assert batt_sensor.state == "5" - assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Battery" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Battery" assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -515,14 +502,12 @@ async def test_xiaomi_CGDK2(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - temp_sensor = hass.states.get( - "sensor.temperature_humidity_sensor_122089_temperature" - ) + temp_sensor = hass.states.get("sensor.temperature_humidity_sensor_2089_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "22.6" assert ( temp_sensor_attribtes[ATTR_FRIENDLY_NAME] - == "Temperature/Humidity Sensor 122089 Temperature" + == "Temperature/Humidity Sensor 2089 Temperature" ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From 7cdac3ee8c6ccc320a4f37ee00f5d180f74f533d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 21:06:17 +0200 Subject: [PATCH 154/231] Bump xiaomi-ble to 0.10.0 (#78365) --- homeassistant/components/xiaomi_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/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index de8e61ad8ce..56efd9e966a 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -9,7 +9,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.9.3"], + "requirements": ["xiaomi-ble==0.10.0"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 6ba52832cba..855c924b0f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2528,7 +2528,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.3 +xiaomi-ble==0.10.0 # homeassistant.components.knx xknx==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5452a9140c..eafa9d219d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1735,7 +1735,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.9.3 +xiaomi-ble==0.10.0 # homeassistant.components.knx xknx==1.0.2 From dd007cd765de3023d8553ec8a5a984990072035f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:15:30 +0200 Subject: [PATCH 155/231] Bump led-ble to 0.10.0 (#78367) --- homeassistant/components/led_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/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 1f27837b89e..89b4fdb26af 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.9.1"], + "requirements": ["led-ble==0.10.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 855c924b0f9..5ebb0a5a56c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.9.1 +led-ble==0.10.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eafa9d219d0..3aaa5cb2fea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -712,7 +712,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.9.1 +led-ble==0.10.0 # homeassistant.components.foscam libpyfoscam==1.0 From f0753f7a97af808664fa1a6b26c64a7db65b61ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 21:00:45 +0200 Subject: [PATCH 156/231] Bump aiohomekit to 1.5.7 (#78369) --- 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 a4cbbc227d9..0b67f80bac5 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==1.5.6"], + "requirements": ["aiohomekit==1.5.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 5ebb0a5a56c..b7ab218a461 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==1.5.6 +aiohomekit==1.5.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3aaa5cb2fea..e52ccf0a359 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==1.5.6 +aiohomekit==1.5.7 # homeassistant.components.emulated_hue # homeassistant.components.http From 3beed135865bb6f43dac3d7e11fa2f41cd14fa7f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:55:06 +0200 Subject: [PATCH 157/231] Don't allow partial update of counter settings (#78371) --- homeassistant/components/counter/__init__.py | 23 ++++--------- tests/components/counter/test_init.py | 36 ++++++++++---------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index a16d891af54..83d307f69d3 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -47,7 +47,7 @@ SERVICE_CONFIGURE = "configure" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Required(CONF_NAME): vol.All(cv.string, vol.Length(min=1)), @@ -57,16 +57,6 @@ CREATE_FIELDS = { vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, } -UPDATE_FIELDS = { - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_INITIAL): cv.positive_int, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MAXIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(CONF_MINIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(CONF_RESTORE): cv.boolean, - vol.Optional(CONF_STEP): cv.positive_int, -} - def _none_to_empty_dict(value): if value is None: @@ -128,7 +118,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") @@ -152,12 +142,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class CounterStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -166,8 +155,8 @@ class CounterStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class Counter(RestoreEntity): diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 107dd97924d..90885be770d 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -591,17 +591,15 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): async def test_update_min_max(hass, hass_ws_client, storage_setup): """Test updating min/max updates the state.""" - items = [ - { - "id": "from_storage", - "initial": 15, - "name": "from storage", - "maximum": 100, - "minimum": 10, - "step": 3, - "restore": True, - } - ] + settings = { + "initial": 15, + "name": "from storage", + "maximum": 100, + "minimum": 10, + "step": 3, + "restore": True, + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -618,16 +616,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = settings | {"minimum": 19} await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "minimum": 19, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert int(state.state) == 19 @@ -635,18 +635,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): assert state.attributes[ATTR_MAXIMUM] == 100 assert state.attributes[ATTR_STEP] == 3 + updated_settings = settings | {"maximum": 5, "minimum": 2, "step": 5} await client.send_json( { "id": 7, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "maximum": 5, - "minimum": 2, - "step": 5, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert int(state.state) == 5 @@ -654,18 +654,18 @@ async def test_update_min_max(hass, hass_ws_client, storage_setup): assert state.attributes[ATTR_MAXIMUM] == 5 assert state.attributes[ATTR_STEP] == 5 + updated_settings = settings | {"maximum": None, "minimum": None, "step": 6} await client.send_json( { "id": 8, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "maximum": None, - "minimum": None, - "step": 6, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert int(state.state) == 5 From ee07ca8caa9ef03a2d449da2068994ef7ff8d3e5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:55:24 +0200 Subject: [PATCH 158/231] Don't allow partial update of input_boolean settings (#78372) --- .../components/input_boolean/__init__.py | 32 ++++--- tests/components/input_boolean/test_init.py | 89 ++++++++++++++++++- 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 7dee3614ad5..a43b132a0e2 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -37,20 +37,25 @@ _LOGGER = logging.getLogger(__name__) CONF_INITIAL = "initial" -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_INITIAL): cv.boolean, vol.Optional(CONF_ICON): cv.icon, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, -} - CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))}, + { + DOMAIN: cv.schema_with_slug_keys( + vol.Any( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + }, + None, + ) + ) + }, extra=vol.ALLOW_EXTRA, ) @@ -62,12 +67,11 @@ STORAGE_VERSION = 1 class InputBooleanStorageCollection(collection.StorageCollection): """Input boolean collection stored in storage.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -76,8 +80,8 @@ class InputBooleanStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data @bind_hass @@ -118,7 +122,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 2b7a1f88ef1..2e044c7a90f 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -40,7 +40,11 @@ def storage_setup(hass, hass_storage): "data": {"items": [{"id": "from_storage", "name": "from storage"}]}, } else: - hass_storage[DOMAIN] = items + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } if config is None: config = {DOMAIN: {}} return await async_setup_component(hass, DOMAIN, config) @@ -332,6 +336,89 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None +async def test_ws_update(hass, hass_ws_client, storage_setup): + """Test update WS.""" + + settings = { + "name": "from storage", + } + items = [{"id": "from_storage"} | settings] + assert await storage_setup(items) + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = er.async_get(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert state.state + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + updated_settings = settings | {"name": "new_name", "icon": "mdi:blah"} + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + **updated_settings, + } + ) + resp = await client.receive_json() + assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings + + state = hass.states.get(input_entity_id) + assert state.attributes["icon"] == "mdi:blah" + assert state.attributes["friendly_name"] == "new_name" + + updated_settings = settings | {"name": "new_name_2"} + await client.send_json( + { + "id": 7, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + **updated_settings, + } + ) + resp = await client.receive_json() + assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings + + state = hass.states.get(input_entity_id) + assert "icon" not in state.attributes + assert state.attributes["friendly_name"] == "new_name_2" + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = er.async_get(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state + + async def test_setup_no_config(hass, hass_admin_user): """Test component setup with no config.""" count_start = len(hass.states.async_entity_ids()) From 9459af30b0e6392219fac410c9fa176f434a3ca3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:56:40 +0200 Subject: [PATCH 159/231] Don't allow partial update of input_datetime settings (#78373) --- .../components/input_datetime/__init__.py | 20 ++++++------------- tests/components/input_datetime/test_init.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index bda5572081c..daedfd251b0 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -61,20 +61,13 @@ def validate_set_datetime_attrs(config): STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_INITIAL): cv.string, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HAS_DATE): cv.boolean, - vol.Optional(CONF_HAS_TIME): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_INITIAL): cv.string, -} def has_date_or_time(conf): @@ -167,7 +160,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -213,12 +206,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class DateTimeStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, has_date_or_time)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, has_date_or_time)) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -227,8 +219,8 @@ class DateTimeStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return has_date_or_time({**data, **update_data}) + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputDatetime(RestoreEntity): diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 28ca2ab02bd..9e694488797 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -583,17 +583,23 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = { + CONF_NAME: "even newer name", + CONF_HAS_DATE: False, + CONF_HAS_TIME: True, + CONF_INITIAL: INITIAL_DATETIME, + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - ATTR_NAME: "even newer name", - CONF_HAS_DATE: False, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert state.state == INITIAL_TIME From 336179df6df97c7b68f04b18be12883e67edfbcc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:56:46 +0200 Subject: [PATCH 160/231] Don't allow partial update of input_button settings (#78374) --- .../components/input_button/__init__.py | 30 +++++++++++-------- tests/components/input_button/test_init.py | 1 + 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index 3182e36d5fc..a47e96d635d 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -30,18 +30,23 @@ DOMAIN = "input_button" _LOGGER = logging.getLogger(__name__) -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_ICON): cv.icon, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, -} - CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))}, + { + DOMAIN: cv.schema_with_slug_keys( + vol.Any( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, + }, + None, + ) + ) + }, extra=vol.ALLOW_EXTRA, ) @@ -53,12 +58,11 @@ STORAGE_VERSION = 1 class InputButtonStorageCollection(collection.StorageCollection): """Input button collection stored in storage.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> vol.Schema: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -67,8 +71,8 @@ class InputButtonStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return {**data, **update_data} + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -103,7 +107,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: diff --git a/tests/components/input_button/test_init.py b/tests/components/input_button/test_init.py index 33342455147..eb27f277884 100644 --- a/tests/components/input_button/test_init.py +++ b/tests/components/input_button/test_init.py @@ -305,6 +305,7 @@ async def test_ws_create_update(hass, hass_ws_client, storage_setup): ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "new", "name": "newer"} state = hass.states.get(f"{DOMAIN}.new") assert state is not None From 6f9a311cec77507696590e27ac4c53f0c8c95e55 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:56:59 +0200 Subject: [PATCH 161/231] Don't allow partial update of input_select settings (#78376) --- .../components/input_select/__init__.py | 21 +++------ tests/components/input_select/test_init.py | 45 +++++++++++-------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 83d6684a366..5d0e356b8ee 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -56,7 +56,7 @@ def _unique(options: Any) -> Any: raise HomeAssistantError("Duplicate options are not allowed") from exc -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_OPTIONS): vol.All( cv.ensure_list, vol.Length(min=1), _unique, [cv.string] @@ -64,14 +64,6 @@ CREATE_FIELDS = { vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_ICON): cv.icon, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OPTIONS): vol.All( - cv.ensure_list, vol.Length(min=1), _unique, [cv.string] - ), - vol.Optional(CONF_INITIAL): cv.string, - vol.Optional(CONF_ICON): cv.icon, -} def _remove_duplicates(options: list[str], name: str | None) -> list[str]: @@ -172,7 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -238,12 +230,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class InputSelectStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_select)) async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: """Validate the config is valid.""" - return cast(dict[str, Any], self.CREATE_SCHEMA(data)) + return cast(dict[str, Any], self.CREATE_UPDATE_SCHEMA(data)) @callback def _get_suggested_id(self, info: dict[str, Any]) -> str: @@ -254,8 +245,8 @@ class InputSelectStorageCollection(collection.StorageCollection): self, data: dict[str, Any], update_data: dict[str, Any] ) -> dict[str, Any]: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return _cv_input_select({**data, **update_data}) + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputSelect(SelectEntity, RestoreEntity): diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index d65140dcbf9..1a1618d7805 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -628,13 +628,11 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): async def test_update(hass, hass_ws_client, storage_setup): """Test updating options updates the state.""" - items = [ - { - "id": "from_storage", - "name": "from storage", - "options": ["yaml update 1", "yaml update 2"], - } - ] + settings = { + "name": "from storage", + "options": ["yaml update 1", "yaml update 2"], + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -647,28 +645,36 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = settings | { + "options": ["new option", "newer option"], + CONF_INITIAL: "newer option", + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "options": ["new option", "newer option"], - CONF_INITIAL: "newer option", + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert state.attributes[ATTR_OPTIONS] == ["new option", "newer option"] # Should fail because the initial state is now invalid + updated_settings = settings | { + "options": ["new option", "no newer option"], + CONF_INITIAL: "newer option", + } await client.send_json( { "id": 7, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "options": ["new option", "no newer option"], + **updated_settings, } ) resp = await client.receive_json() @@ -678,13 +684,11 @@ async def test_update(hass, hass_ws_client, storage_setup): async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog): """Test updating options updates the state.""" - items = [ - { - "id": "from_storage", - "name": "from storage", - "options": ["yaml update 1", "yaml update 2"], - } - ] + settings = { + "name": "from storage", + "options": ["yaml update 1", "yaml update 2"], + } + items = [{"id": "from_storage"} | settings] assert await storage_setup(items) input_id = "from_storage" @@ -697,13 +701,16 @@ async def test_update_duplicates(hass, hass_ws_client, storage_setup, caplog): client = await hass_ws_client(hass) + updated_settings = settings | { + "options": ["new option", "newer option", "newer option"], + CONF_INITIAL: "newer option", + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - "options": ["new option", "newer option", "newer option"], - CONF_INITIAL: "newer option", + **updated_settings, } ) resp = await client.receive_json() From 04d6bb085b1d8bf6ae5591077c886edc38907920 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:57:14 +0200 Subject: [PATCH 162/231] Don't allow partial update of input_text settings (#78377) --- .../components/input_text/__init__.py | 23 +++++-------------- tests/components/input_text/test_init.py | 13 +++++++---- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 38d74f57931..211d9843996 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -51,7 +51,7 @@ SERVICE_SET_VALUE = "set_value" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), @@ -61,16 +61,6 @@ CREATE_FIELDS = { vol.Optional(CONF_PATTERN): cv.string, vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]), } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN): vol.Coerce(int), - vol.Optional(CONF_MAX): vol.Coerce(int), - vol.Optional(CONF_INITIAL): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_PATTERN): cv.string, - vol.Optional(CONF_MODE): vol.In([MODE_TEXT, MODE_PASSWORD]), -} def _cv_input_text(cfg): @@ -147,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -177,12 +167,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class InputTextStorageCollection(collection.StorageCollection): """Input storage based collection.""" - CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_text)) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_text)) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return self.CREATE_UPDATE_SCHEMA(data) @callback def _get_suggested_id(self, info: dict) -> str: @@ -191,8 +180,8 @@ class InputTextStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - update_data = self.UPDATE_SCHEMA(update_data) - return _cv_input_text({**data, **update_data}) + update_data = self.CREATE_UPDATE_SCHEMA(update_data) + return {CONF_ID: data[CONF_ID]} | update_data class InputText(RestoreEntity): diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 48f9551a65b..8256d9d351f 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -432,19 +432,24 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = { + ATTR_NAME: "even newer name", + CONF_INITIAL: "newer option", + ATTR_MAX: TEST_VAL_MAX, + ATTR_MIN: 6, + ATTR_MODE: "password", + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", - ATTR_NAME: "even newer name", - CONF_INITIAL: "newer option", - ATTR_MIN: 6, - ATTR_MODE: "password", + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == {"id": "from_storage"} | updated_settings state = hass.states.get(input_entity_id) assert state.state == "loaded from storage" From 859947288040aae5a64de2b596be3fda5d4bc737 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 Sep 2022 20:58:12 +0200 Subject: [PATCH 163/231] Don't allow partial update of timer settings (#78378) --- homeassistant/components/timer/__init__.py | 17 +++++------------ tests/components/timer/test_init.py | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index e5564736c74..fea814261fe 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -61,18 +61,12 @@ SERVICE_FINISH = "finish" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -CREATE_FIELDS = { +STORAGE_FIELDS = { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period, vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean, } -UPDATE_FIELDS = { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_DURATION): cv.time_period, - vol.Optional(CONF_RESTORE): cv.boolean, -} def _format_timedelta(delta: timedelta): @@ -137,7 +131,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS ).async_setup(hass) async def reload_service_handler(service_call: ServiceCall) -> None: @@ -171,12 +165,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class TimerStorageCollection(collection.StorageCollection): """Timer storage based collection.""" - CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) - UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - data = self.CREATE_SCHEMA(data) + data = self.CREATE_UPDATE_SCHEMA(data) # make duration JSON serializeable data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION]) return data @@ -188,7 +181,7 @@ class TimerStorageCollection(collection.StorageCollection): async def _update_data(self, data: dict, update_data: dict) -> dict: """Return a new updated data object.""" - data = {**data, **self.UPDATE_SCHEMA(update_data)} + data = {CONF_ID: data[CONF_ID]} | self.CREATE_UPDATE_SCHEMA(update_data) # make duration JSON serializeable if CONF_DURATION in update_data: data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION]) diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 8f605d2de9f..2e9ef785b32 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -585,17 +585,27 @@ async def test_update(hass, hass_ws_client, storage_setup): client = await hass_ws_client(hass) + updated_settings = { + CONF_NAME: "timer from storage", + CONF_DURATION: 33, + CONF_RESTORE: True, + } await client.send_json( { "id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{timer_id}", - CONF_DURATION: 33, - CONF_RESTORE: True, + **updated_settings, } ) resp = await client.receive_json() assert resp["success"] + assert resp["result"] == { + "id": "from_storage", + CONF_DURATION: "0:00:33", + CONF_NAME: "timer from storage", + CONF_RESTORE: True, + } state = hass.states.get(timer_entity_id) assert state.attributes[ATTR_DURATION] == _format_timedelta(cv.time_period(33)) From d98ed5c6f6dbc71eda371a3f7b9b40865c9611d1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 13 Sep 2022 20:51:04 +0200 Subject: [PATCH 164/231] Unregister EcoWitt webhook at unload (#78388) --- homeassistant/components/ecowitt/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/ecowitt/__init__.py b/homeassistant/components/ecowitt/__init__.py index ebd861c1377..567e21b4d87 100644 --- a/homeassistant/components/ecowitt/__init__.py +++ b/homeassistant/components/ecowitt/__init__.py @@ -44,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) From 843d5f101adb3855375985c8c4731e7c034cddcd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:10:50 +0200 Subject: [PATCH 165/231] Fix flapping system log test (#78391) Since we run tests with asyncio debug on, there is a chance we will get an asyncio log message instead of the one we want Fixes https://github.com/home-assistant/core/actions/runs/3045080236/jobs/4906717578 --- tests/components/system_log/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 6304e0ea7cf..96e5480acb5 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -41,7 +41,7 @@ def find_log(logs, level): if not isinstance(level, tuple): level = (level,) log = next( - (log for log in logs if log["level"] in level), + (log for log in logs if log["level"] in level and log["name"] != "asyncio"), None, ) assert log is not None From f07e1bc5005517870cbf64cf28d252bead87ea3d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 13 Sep 2022 21:01:46 +0200 Subject: [PATCH 166/231] Fix CI workflow caching (#78398) --- .github/workflows/ci.yaml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 75a200402d6..80c6cbac34b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -169,7 +169,6 @@ jobs: uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - cache: "pip" - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.8 @@ -484,7 +483,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ matrix.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' @@ -492,10 +491,10 @@ jobs: with: path: ${{ env.PIP_CACHE }} key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.generate-pip-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -542,7 +541,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -574,7 +573,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -607,7 +606,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -651,7 +650,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -699,7 +698,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ matrix.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -752,7 +751,7 @@ jobs: uses: actions/cache@v3.0.8 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' From 0b5953038e90327b40aec6a261e316f3bd3c06c1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 13 Sep 2022 22:15:45 +0200 Subject: [PATCH 167/231] Update frontend to 20220907.1 (#78404) --- 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 07822979683..42d7dd4b0fa 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==20220907.0"], + "requirements": ["home-assistant-frontend==20220907.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b511248a602..ef5ff53c4a4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.4.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220907.0 +home-assistant-frontend==20220907.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index b7ab218a461..ecbfdf8c255 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -857,7 +857,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.0 +home-assistant-frontend==20220907.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e52ccf0a359..097881c4b39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -634,7 +634,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.0 +home-assistant-frontend==20220907.1 # homeassistant.components.home_connect homeconnect==0.7.2 From 29caf06439855e78af25b73471bc1319ce6af81b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Sep 2022 22:17:09 +0200 Subject: [PATCH 168/231] Bump govee-ble to 0.17.3 (#78405) --- 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 2ce68498968..537ae9c7ed5 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -17,6 +17,11 @@ "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 57391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", @@ -53,7 +58,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.2"], + "requirements": ["govee-ble==0.17.3"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 59f10767e0d..22bb93d1bda 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -62,6 +62,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", "connectable": False }, + { + "domain": "govee_ble", + "manufacturer_id": 57391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": False + }, { "domain": "govee_ble", "manufacturer_id": 18994, diff --git a/requirements_all.txt b/requirements_all.txt index ecbfdf8c255..fc3fd2a0f1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -778,7 +778,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.2 +govee-ble==0.17.3 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 097881c4b39..ece00d7d630 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -579,7 +579,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.2 +govee-ble==0.17.3 # homeassistant.components.gree greeclimate==1.3.0 From 1955ff9e0de7b63b6100cc3d8c27c121bcf20756 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 13 Sep 2022 16:31:51 -0400 Subject: [PATCH 169/231] Bumped version to 2022.9.3 --- 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 f1b41fd57e0..8b899cf0f40 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __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 d411a1c62bb..d926dd5bbc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.2" +version = "2022.9.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6a197332c711d2f904ed6e0c6ce5bb945e7dbb1b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 13 Sep 2022 16:16:21 -0600 Subject: [PATCH 170/231] Fix bug with RainMachine update entity (#78411) * Fix bug with RainMachine update entity * Comment --- homeassistant/components/rainmachine/update.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index b191f2695a0..a811894a0c2 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -99,4 +99,11 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity): UpdateStates.UPGRADING, UpdateStates.REBOOT, ) - self._attr_latest_version = data["packageDetails"]["newVersion"] + + # The RainMachine API docs say that multiple "packages" can be updated, but + # don't give details on what types exist (which makes it impossible to have + # update entities per update type); so, we use the first one (with the idea that + # after it succeeds, the entity will show the next update): + package_details = data["packageDetails"][0] + self._attr_latest_version = package_details["newVersion"] + self._attr_title = package_details["packageName"] From 7b83807baaa0334ee423457ba00a8cbd1dc6d0b6 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 14 Sep 2022 02:00:59 +0100 Subject: [PATCH 171/231] Retry on unavailable IPMA api (#78332) Co-authored-by: J. Nick Koston --- homeassistant/components/ipma/__init__.py | 50 +++++++++++++++++++-- homeassistant/components/ipma/const.py | 3 ++ homeassistant/components/ipma/manifest.json | 2 +- homeassistant/components/ipma/weather.py | 33 ++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ipma/test_config_flow.py | 2 +- tests/components/ipma/test_weather.py | 13 +++--- 8 files changed, 65 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 4d675e8cc1d..315362247a2 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,19 +1,61 @@ """Component for the Portuguese weather service - IPMA.""" +import logging + +import async_timeout +from pyipma import IPMAException +from pyipma.api import IPMA_API +from pyipma.location import Location + from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .config_flow import IpmaFlowHandler # noqa: F401 -from .const import DOMAIN # noqa: F401 +from .const import DATA_API, DATA_LOCATION, DOMAIN DEFAULT_NAME = "ipma" PLATFORMS = [Platform.WEATHER] +_LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_get_api(hass): + """Get the pyipma api object.""" + websession = async_get_clientsession(hass) + return IPMA_API(websession) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up IPMA station as config entry.""" - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + api = await async_get_api(hass) + try: + async with async_timeout.timeout(30): + location = await Location.get(api, float(latitude), float(longitude)) + + _LOGGER.debug( + "Initializing for coordinates %s, %s -> station %s (%d, %d)", + latitude, + longitude, + location.station, + location.id_station, + location.global_id_local, + ) + except IPMAException as err: + raise ConfigEntryNotReady( + f"Could not get location for ({latitude},{longitude})" + ) from err + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = {DATA_API: api, DATA_LOCATION: location} + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index 47434d7f76b..60c8115a5c4 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -6,3 +6,6 @@ DOMAIN = "ipma" HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}" + +DATA_LOCATION = "location" +DATA_API = "api" diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index a391b24e3b4..23558600373 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==3.0.2"], + "requirements": ["pyipma==3.0.4"], "codeowners": ["@dgomes", "@abmantis"], "iot_class": "cloud_polling", "loggers": ["geopy", "pyipma"] diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index d20e5cb2f21..a0fe5b235b3 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -48,11 +48,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import Throttle +from .const import DATA_API, DATA_LOCATION, DOMAIN + _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Instituto Português do Mar e Atmosfera" @@ -95,13 +96,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add a weather entity from a config_entry.""" - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] + api = hass.data[DOMAIN][config_entry.entry_id][DATA_API] + location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION] mode = config_entry.data[CONF_MODE] - api = await async_get_api(hass) - location = await async_get_location(hass, api, latitude, longitude) - # Migrate old unique_id @callback def _async_migrator(entity_entry: entity_registry.RegistryEntry): @@ -127,29 +125,6 @@ async def async_setup_entry( async_add_entities([IPMAWeather(location, api, config_entry.data)], True) -async def async_get_api(hass): - """Get the pyipma api object.""" - websession = async_get_clientsession(hass) - return IPMA_API(websession) - - -async def async_get_location(hass, api, latitude, longitude): - """Retrieve pyipma location, location name to be used as the entity name.""" - async with async_timeout.timeout(30): - location = await Location.get(api, float(latitude), float(longitude)) - - _LOGGER.debug( - "Initializing for coordinates %s, %s -> station %s (%d, %d)", - latitude, - longitude, - location.station, - location.id_station, - location.global_id_local, - ) - - return location - - class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" diff --git a/requirements_all.txt b/requirements_all.txt index fc3fd2a0f1f..8a87ad1cc6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1614,7 +1614,7 @@ pyinsteon==1.2.0 pyintesishome==1.8.0 # homeassistant.components.ipma -pyipma==3.0.2 +pyipma==3.0.4 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ece00d7d630..e83a56b8a28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1127,7 +1127,7 @@ pyicloud==1.0.0 pyinsteon==1.2.0 # homeassistant.components.ipma -pyipma==3.0.2 +pyipma==3.0.4 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index c8d53f95a4a..0c17cbe7f4b 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -168,7 +168,7 @@ async def test_config_entry_migration(hass): ) with patch( - "homeassistant.components.ipma.weather.async_get_location", + "pyipma.location.Location.get", return_value=MockLocation(), ): assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 942b9654895..e129216730d 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -19,7 +19,6 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - DOMAIN as WEATHER_DOMAIN, ) from homeassistant.const import STATE_UNKNOWN @@ -181,7 +180,8 @@ async def test_setup_config_flow(hass): return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") @@ -203,7 +203,8 @@ async def test_daily_forecast(hass): return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") @@ -227,7 +228,8 @@ async def test_hourly_forecast(hass): return_value=MockLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG_HOURLY) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") @@ -248,7 +250,8 @@ async def test_failed_get_observation_forecast(hass): return_value=MockBadLocation(), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get("weather.hometown") From e711758cfd3690d4f8559658c7a7a7a77580da8c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 14 Sep 2022 09:54:51 +0200 Subject: [PATCH 172/231] Update frontend to 20220907.2 (#78431) --- 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 42d7dd4b0fa..69fdc6c95a1 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==20220907.1"], + "requirements": ["home-assistant-frontend==20220907.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ef5ff53c4a4..a86c2b07bf7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.4.0 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220907.1 +home-assistant-frontend==20220907.2 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 8a87ad1cc6a..ecba6634ed6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -857,7 +857,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.1 +home-assistant-frontend==20220907.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e83a56b8a28..e3588ff16c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -634,7 +634,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220907.1 +home-assistant-frontend==20220907.2 # homeassistant.components.home_connect homeconnect==0.7.2 From e839849456730da35ce6f19050359e30fccc30dc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 14 Sep 2022 10:03:04 +0200 Subject: [PATCH 173/231] Bumped version to 2022.9.4 --- 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 8b899cf0f40..5b17e764b8a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __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 d926dd5bbc1..316c0eedda6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.3" +version = "2022.9.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8229e241f12ef32f407a9204b92020ae78971c9b Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 18 Sep 2022 05:45:31 +0200 Subject: [PATCH 174/231] Fix fan speed regression for some xiaomi fans (#78406) Co-authored-by: Martin Hjelmare --- homeassistant/components/xiaomi_miio/const.py | 9 -- homeassistant/components/xiaomi_miio/fan.py | 82 +++++++++++++------ 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 11922956c25..c0711a02a36 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -112,15 +112,6 @@ MODELS_FAN_MIOT = [ MODEL_FAN_ZA5, ] -# number of speed levels each fan has -SPEEDS_FAN_MIOT = { - MODEL_FAN_1C: 3, - MODEL_FAN_P10: 4, - MODEL_FAN_P11: 4, - MODEL_FAN_P9: 4, - MODEL_FAN_ZA5: 4, -} - MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3C, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 901211d1d2d..ddbd45bff08 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -85,7 +85,6 @@ from .const import ( MODELS_PURIFIER_MIOT, SERVICE_RESET_FILTER, SERVICE_SET_EXTRA_FEATURES, - SPEEDS_FAN_MIOT, ) from .device import XiaomiCoordinatedMiioEntity @@ -235,13 +234,11 @@ async def async_setup_entry( elif model in MODELS_FAN_MIIO: entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - speed_count = SPEEDS_FAN_MIOT[model] - entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator, speed_count) + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) + elif model == MODEL_FAN_1C: + entity = XiaomiFan1C(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIOT: - speed_count = SPEEDS_FAN_MIOT[model] - entity = XiaomiFanMiot( - device, config_entry, unique_id, coordinator, speed_count - ) + entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) else: return @@ -1049,11 +1046,6 @@ class XiaomiFanP5(XiaomiGenericFan): class XiaomiFanMiot(XiaomiGenericFan): """Representation of a Xiaomi Fan Miot.""" - def __init__(self, device, entry, unique_id, coordinator, speed_count): - """Initialize MIOT fan with speed count.""" - super().__init__(device, entry, unique_id, coordinator) - self._speed_count = speed_count - @property def operation_mode_class(self): """Hold operation mode class.""" @@ -1071,9 +1063,7 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: - self._percentage = ranged_value_to_percentage( - (1, self._speed_count), self.coordinator.data.speed - ) + self._percentage = self.coordinator.data.speed else: self._percentage = 0 @@ -1092,6 +1082,59 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = preset_mode self.async_write_ha_state() + async def async_set_percentage(self, percentage: int) -> None: + """Set the percentage of the fan.""" + if percentage == 0: + self._percentage = 0 + await self.async_turn_off() + return + + result = await self._try_command( + "Setting fan speed percentage of the miio device failed.", + self._device.set_speed, + percentage, + ) + if result: + self._percentage = percentage + + if not self.is_on: + await self.async_turn_on() + elif result: + self.async_write_ha_state() + + +class XiaomiFanZA5(XiaomiFanMiot): + """Representation of a Xiaomi Fan ZA5.""" + + @property + def operation_mode_class(self): + """Hold operation mode class.""" + return FanZA5OperationMode + + +class XiaomiFan1C(XiaomiFanMiot): + """Representation of a Xiaomi Fan 1C (Standing Fan 2 Lite).""" + + def __init__(self, device, entry, unique_id, coordinator): + """Initialize MIOT fan with speed count.""" + super().__init__(device, entry, unique_id, coordinator) + self._speed_count = 3 + + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._state = self.coordinator.data.is_on + self._preset_mode = self.coordinator.data.mode.name + self._oscillating = self.coordinator.data.oscillate + if self.coordinator.data.is_on: + self._percentage = ranged_value_to_percentage( + (1, self._speed_count), self.coordinator.data.speed + ) + else: + self._percentage = 0 + + self.async_write_ha_state() + async def async_set_percentage(self, percentage: int) -> None: """Set the percentage of the fan.""" if percentage == 0: @@ -1116,12 +1159,3 @@ class XiaomiFanMiot(XiaomiGenericFan): if result: self._percentage = ranged_value_to_percentage((1, self._speed_count), speed) self.async_write_ha_state() - - -class XiaomiFanZA5(XiaomiFanMiot): - """Representation of a Xiaomi Fan ZA5.""" - - @property - def operation_mode_class(self): - """Hold operation mode class.""" - return FanZA5OperationMode From a4749178f199c49af81b58ab6fad8ae0f5ca255f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 16 Sep 2022 17:04:55 -0400 Subject: [PATCH 175/231] Only redact zwave_js values that are worth redacting (#78420) * Only redact zwave_js values that are worth redacting * Tweak test * Use redacted fixture for test --- .../components/zwave_js/diagnostics.py | 3 + tests/components/zwave_js/conftest.py | 6 + .../config_entry_diagnostics_redacted.json | 1936 +++++++++++++++++ tests/components/zwave_js/test_diagnostics.py | 18 +- 4 files changed, 1951 insertions(+), 12 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 33d32e96fe0..c915d2a3e63 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -51,6 +51,9 @@ VALUES_TO_REDACT = ( def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: """Redact value of a Z-Wave value.""" + # If the value has no value, there is nothing to redact + 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: diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 04a9c5671f9..075ac76ede8 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -264,6 +264,12 @@ def config_entry_diagnostics_fixture(): return json.loads(load_fixture("zwave_js/config_entry_diagnostics.json")) +@pytest.fixture(name="config_entry_diagnostics_redacted", scope="session") +def config_entry_diagnostics_redacted_fixture(): + """Load the redacted config entry diagnostics fixture data.""" + return json.loads(load_fixture("zwave_js/config_entry_diagnostics_redacted.json")) + + @pytest.fixture(name="multisensor_6_state", scope="session") def multisensor_6_state_fixture(): """Load the multisensor 6 node state fixture data.""" diff --git a/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json new file mode 100644 index 00000000000..1e68d82d586 --- /dev/null +++ b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json @@ -0,0 +1,1936 @@ +[ + { + "type": "version", + "driverVersion": "8.11.6", + "serverVersion": "1.15.0", + "homeId": "**REDACTED**", + "minSchemaVersion": 0, + "maxSchemaVersion": 15 + }, + { + "type": "result", + "success": true, + "messageId": "api-schema-id", + "result": {} + }, + { + "type": "result", + "success": true, + "messageId": "listen-id", + "result": { + "state": { + "driver": { + "logConfig": { + "enabled": true, + "level": "info", + "logToFile": false, + "filename": "/data/store/zwavejs_%DATE%.log", + "forceConsole": true + }, + "statisticsEnabled": true + }, + "controller": { + "libraryVersion": "Z-Wave 6.07", + "type": 1, + "homeId": "**REDACTED**", + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.2", + "manufacturerId": 134, + "productType": 1, + "productId": 90, + "supportedFunctionTypes": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 28, + 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, 56, 57, + 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, + 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, 99, 102, 103, + 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, 184, 185, 186, + 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 + ], + "sucNodeId": 1, + "supportsTimers": false, + "isHealNetworkActive": false, + "statistics": { + "messagesTX": 10, + "messagesRX": 734, + "messagesDroppedRX": 0, + "NAK": 0, + "CAN": 0, + "timeoutACK": 0, + "timeoutResponse": 0, + "timeoutCallback": 0, + "messagesDroppedTX": 0 + }, + "inclusionState": 0 + }, + "nodes": [ + { + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z‐Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "label": "ZW090", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "isControllerNode": true, + "keepAwake": false + }, + { + "nodeId": 29, + "index": 0, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "firmwareVersion": "113.22", + "name": "Front Door Lock", + "location": "**REDACTED**", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 29, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (1)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (2)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (3)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (4)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (5)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (6)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (7)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (8)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (9)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (10)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (11)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (12)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (13)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (14)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (15)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (16)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (17)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (18)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (19)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (20)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (21)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (22)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (23)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (24)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (25)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (26)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (28)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (29)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (30)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Lock state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "11": "Lock jammed" + } + }, + "value": 11 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Keypad state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + } + }, + "value": 16 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 89 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.42" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["113.22"] + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "statistics": { + "commandsTX": 25, + "commandsRX": 42, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": 7, + "isControllerNode": false, + "keepAwake": false + } + ] + } + } + } +] diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 41505364111..64f27805243 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from zwave_js_server.event import Event -from homeassistant.components.diagnostics.const import REDACTED from homeassistant.components.zwave_js.diagnostics import ( ZwaveValueMatcher, async_get_device_diagnostics, @@ -26,7 +25,11 @@ from tests.components.diagnostics import ( async def test_config_entry_diagnostics( - hass, hass_client, integration, config_entry_diagnostics + hass, + hass_client, + integration, + config_entry_diagnostics, + config_entry_diagnostics_redacted, ): """Test the config entry level diagnostics data dump.""" with patch( @@ -36,16 +39,7 @@ async def test_config_entry_diagnostics( diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, integration ) - assert len(diagnostics) == 3 - assert diagnostics[0]["homeId"] == REDACTED - nodes = diagnostics[2]["result"]["state"]["nodes"] - for node in nodes: - assert "location" not in node or node["location"] == REDACTED - for value in node["values"]: - if value["commandClass"] == 99 and value["property"] == "userCode": - assert value["value"] == REDACTED - else: - assert value.get("value") != REDACTED + assert diagnostics == config_entry_diagnostics_redacted async def test_device_diagnostics( From 40c5689507a1c95a2a00f8c1b047226fa7b7273e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Sep 2022 16:47:08 +0200 Subject: [PATCH 176/231] Prevent deleting blueprints which are in use (#78444) --- .../components/automation/__init__.py | 26 ++++- .../components/automation/helpers.py | 9 +- .../components/blueprint/__init__.py | 2 +- homeassistant/components/blueprint/errors.py | 8 ++ homeassistant/components/blueprint/models.py | 6 + homeassistant/components/script/__init__.py | 25 ++++- homeassistant/components/script/helpers.py | 9 +- tests/components/blueprint/test_models.py | 4 +- .../blueprint/test_websocket_api.py | 105 +++++++++++++++++- .../blueprints/script/test_service.yaml | 8 ++ 10 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 tests/testing_config/blueprints/script/test_service.yaml diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 154c443e799..685edc3b6d7 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import blueprint +from homeassistant.components.blueprint import CONF_USE_BLUEPRINT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -20,6 +21,7 @@ from homeassistant.const import ( CONF_EVENT_DATA, CONF_ID, CONF_MODE, + CONF_PATH, CONF_PLATFORM, CONF_VARIABLES, CONF_ZONE, @@ -224,6 +226,21 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: return list(automation_entity.referenced_areas) +@callback +def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: + """Return all automations that reference the blueprint.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + automation_entity.entity_id + for automation_entity in component.entities + if automation_entity.referenced_blueprint == blueprint_path + ] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all automations.""" hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) @@ -346,7 +363,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return self.action_script.referenced_areas @property - def referenced_devices(self): + def referenced_blueprint(self) -> str | None: + """Return referenced blueprint or None.""" + if self._blueprint_inputs is None: + return None + return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]) + + @property + def referenced_devices(self) -> set[str]: """Return a set of referenced devices.""" if self._referenced_devices is not None: return self._referenced_devices diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index 3be11afe18b..7c2efc17bf4 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER DATA_BLUEPRINTS = "automation_blueprints" +def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: + """Return True if any automation references the blueprint.""" + from . import automations_with_blueprint # pylint: disable=import-outside-toplevel + + return len(automations_with_blueprint(hass, blueprint_path)) > 0 + + @singleton(DATA_BLUEPRINTS) @callback def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get automation blueprints.""" - return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) + return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use) diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 23ab6398333..3087309f36a 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -3,7 +3,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from . import websocket_api -from .const import DOMAIN # noqa: F401 +from .const import CONF_USE_BLUEPRINT, DOMAIN # noqa: F401 from .errors import ( # noqa: F401 BlueprintException, BlueprintWithNameException, diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index aceca533d23..fe714542e0f 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -91,3 +91,11 @@ class FileAlreadyExists(BlueprintWithNameException): def __init__(self, domain: str, blueprint_name: str) -> None: """Initialize blueprint exception.""" super().__init__(domain, blueprint_name, "Blueprint already exists") + + +class BlueprintInUse(BlueprintWithNameException): + """Error when a blueprint is in use.""" + + def __init__(self, domain: str, blueprint_name: str) -> None: + """Initialize blueprint exception.""" + super().__init__(domain, blueprint_name, "Blueprint in use") diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 0d90c663b4f..f77a2bed9a4 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging import pathlib import shutil @@ -35,6 +36,7 @@ from .const import ( ) from .errors import ( BlueprintException, + BlueprintInUse, FailedToLoad, FileAlreadyExists, InvalidBlueprint, @@ -183,11 +185,13 @@ class DomainBlueprints: hass: HomeAssistant, domain: str, logger: logging.Logger, + blueprint_in_use: Callable[[HomeAssistant, str], bool], ) -> None: """Initialize a domain blueprints instance.""" self.hass = hass self.domain = domain self.logger = logger + self._blueprint_in_use = blueprint_in_use self._blueprints: dict[str, Blueprint | None] = {} self._load_lock = asyncio.Lock() @@ -302,6 +306,8 @@ class DomainBlueprints: async def async_remove_blueprint(self, blueprint_path: str) -> None: """Remove a blueprint file.""" + if self._blueprint_in_use(self.hass, blueprint_path): + raise BlueprintInUse(self.domain, blueprint_path) path = self.blueprint_folder / blueprint_path await self.hass.async_add_executor_job(path.unlink) self._blueprints[blueprint_path] = None diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index efad242fbd0..53bd256c624 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -8,7 +8,7 @@ from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.components.blueprint import BlueprintInputs +from homeassistant.components.blueprint import CONF_USE_BLUEPRINT, BlueprintInputs from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_ICON, CONF_MODE, CONF_NAME, + CONF_PATH, CONF_SEQUENCE, CONF_VARIABLES, SERVICE_RELOAD, @@ -165,6 +166,21 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: return list(script_entity.script.referenced_areas) +@callback +def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: + """Return all scripts that reference the blueprint.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + return [ + script_entity.entity_id + for script_entity in component.entities + if script_entity.referenced_blueprint == blueprint_path + ] + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Load the scripts from the configuration.""" hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) @@ -372,6 +388,13 @@ class ScriptEntity(ToggleEntity, RestoreEntity): """Return true if script is on.""" return self.script.is_running + @property + def referenced_blueprint(self): + """Return referenced blueprint or None.""" + if self._blueprint_inputs is None: + return None + return self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH] + @callback def async_change_listener(self): """Update state.""" diff --git a/homeassistant/components/script/helpers.py b/homeassistant/components/script/helpers.py index 3c78138a4ec..9f0d4399d3d 100644 --- a/homeassistant/components/script/helpers.py +++ b/homeassistant/components/script/helpers.py @@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER DATA_BLUEPRINTS = "script_blueprints" +def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: + """Return True if any script references the blueprint.""" + from . import scripts_with_blueprint # pylint: disable=import-outside-toplevel + + return len(scripts_with_blueprint(hass, blueprint_path)) > 0 + + @singleton(DATA_BLUEPRINTS) @callback def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints: """Get script blueprints.""" - return DomainBlueprints(hass, DOMAIN, LOGGER) + return DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 497e8b36e99..02ed94709db 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -47,7 +47,9 @@ def blueprint_2(): @pytest.fixture def domain_bps(hass): """Domain blueprints fixture.""" - return models.DomainBlueprints(hass, "automation", logging.getLogger(__name__)) + return models.DomainBlueprints( + hass, "automation", logging.getLogger(__name__), None + ) def test_blueprint_model_init(): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index eb2d12f5081..05c0e4adc4c 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -8,13 +8,26 @@ from homeassistant.setup import async_setup_component from homeassistant.util.yaml import parse_yaml +@pytest.fixture +def automation_config(): + """Automation config.""" + return {} + + +@pytest.fixture +def script_config(): + """Script config.""" + return {} + + @pytest.fixture(autouse=True) -async def setup_bp(hass): +async def setup_bp(hass, automation_config, script_config): """Fixture to set up the blueprint component.""" assert await async_setup_component(hass, "blueprint", {}) - # Trigger registration of automation blueprints - await async_setup_component(hass, "automation", {}) + # Trigger registration of automation and script blueprints + await async_setup_component(hass, "automation", automation_config) + await async_setup_component(hass, "script", script_config) async def test_list_blueprints(hass, hass_ws_client): @@ -251,3 +264,89 @@ async def test_delete_non_exist_file_blueprint(hass, aioclient_mock, hass_ws_cli assert msg["id"] == 9 assert not msg["success"] + + +@pytest.mark.parametrize( + "automation_config", + ( + { + "automation": { + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + } + } + }, + ), +) +async def test_delete_blueprint_in_use_by_automation( + hass, aioclient_mock, hass_ws_client +): + """Test deleting a blueprint which is in use.""" + + with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "test_event_service.yaml", + "domain": "automation", + } + ) + + msg = await client.receive_json() + + assert not unlink_mock.mock_calls + assert msg["id"] == 9 + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Blueprint in use", + } + + +@pytest.mark.parametrize( + "script_config", + ( + { + "script": { + "test_script": { + "use_blueprint": { + "path": "test_service.yaml", + "input": { + "service_to_call": "test.automation", + }, + } + } + } + }, + ), +) +async def test_delete_blueprint_in_use_by_script(hass, aioclient_mock, hass_ws_client): + """Test deleting a blueprint which is in use.""" + + with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock: + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "blueprint/delete", + "path": "test_service.yaml", + "domain": "script", + } + ) + + msg = await client.receive_json() + + assert not unlink_mock.mock_calls + assert msg["id"] == 9 + assert not msg["success"] + assert msg["error"] == { + "code": "unknown_error", + "message": "Blueprint in use", + } diff --git a/tests/testing_config/blueprints/script/test_service.yaml b/tests/testing_config/blueprints/script/test_service.yaml new file mode 100644 index 00000000000..4de991e90dc --- /dev/null +++ b/tests/testing_config/blueprints/script/test_service.yaml @@ -0,0 +1,8 @@ +blueprint: + name: "Call service" + domain: script + input: + service_to_call: +sequence: + service: !input service_to_call + entity_id: light.kitchen From d31d4e29161b9447f2071153caa1c6922ee10960 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 10:40:52 +0200 Subject: [PATCH 177/231] Bump bleak-retry-connector to 0.17.1 (#78474) --- 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 acc8a6977e3..1b1ec016e82 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.17.0", - "bleak-retry-connector==1.15.1", + "bleak-retry-connector==1.17.1", "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.3", "dbus-fast==1.4.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a86c2b07bf7..3ee61afca23 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.8.0 bcrypt==3.1.7 -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 bleak==0.17.0 bluetooth-adapters==0.4.1 bluetooth-auto-recovery==0.3.3 diff --git a/requirements_all.txt b/requirements_all.txt index ecba6634ed6..e87110fcf84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ bimmer_connected==0.10.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 # homeassistant.components.bluetooth bleak==0.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3588ff16c2..3c1475c6fd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ bellows==0.33.1 bimmer_connected==0.10.2 # homeassistant.components.bluetooth -bleak-retry-connector==1.15.1 +bleak-retry-connector==1.17.1 # homeassistant.components.bluetooth bleak==0.17.0 From 0525a1cd973e34e3c63291d0841edc9fec3a53fc Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 14 Sep 2022 21:54:01 +0200 Subject: [PATCH 178/231] Bump python-songpal to 0.15.1 (#78481) --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 2aa58b16a7e..1fb61547445 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Songpal", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.15"], + "requirements": ["python-songpal==0.15.1"], "codeowners": ["@rytilahti", "@shenxn"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e87110fcf84..49a5fa82903 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2005,7 +2005,7 @@ python-ripple-api==0.0.3 python-smarttub==0.0.33 # homeassistant.components.songpal -python-songpal==0.15 +python-songpal==0.15.1 # homeassistant.components.tado python-tado==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c1475c6fd6..b26f8721fcb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1377,7 +1377,7 @@ python-picnic-api==1.1.0 python-smarttub==0.0.33 # homeassistant.components.songpal -python-songpal==0.15 +python-songpal==0.15.1 # homeassistant.components.tado python-tado==0.12.0 From 318ae7750add53adcbda226be4296b7cf635622b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:25 +0200 Subject: [PATCH 179/231] Bump PySwitchbot to 0.19.9 (#78504) --- 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 41b3f5aa61b..a321c964edc 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.8"], + "requirements": ["PySwitchbot==0.19.9"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 49a5fa82903..222692c6d48 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.8 +PySwitchbot==0.19.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b26f8721fcb..f25f37d166c 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.8 +PySwitchbot==0.19.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From ed6575fefb6bc3f9de0844154418d781b6ce3a5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:12 +0200 Subject: [PATCH 180/231] Bump yalexs_ble to 1.9.2 (#78508) --- 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 673521f9e06..c70669f7cc6 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.0"], + "requirements": ["yalexs-ble==1.9.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 222692c6d48..4a4637494ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2548,7 +2548,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.0 +yalexs-ble==1.9.2 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f25f37d166c..598cb144a71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1752,7 +1752,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.0 +yalexs-ble==1.9.2 # homeassistant.components.august yalexs==1.2.1 From e0b63ac488bb6970cfdcdef649f4ef8958435d79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:40 +0200 Subject: [PATCH 181/231] Bump led-ble to 0.10.1 (#78511) --- homeassistant/components/led_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/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 89b4fdb26af..65725ed482a 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.0"], + "requirements": ["led-ble==0.10.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4a4637494ab..057167f0bb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ lakeside==0.12 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.10.0 +led-ble==0.10.1 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 598cb144a71..dc733f04a56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -712,7 +712,7 @@ lacrosse-view==0.0.9 laundrify_aio==1.1.2 # homeassistant.components.led_ble -led-ble==0.10.0 +led-ble==0.10.1 # homeassistant.components.foscam libpyfoscam==1.0 From 050c09df6228a68ecf393db30f250442834bc1a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Sep 2022 13:25:50 +0200 Subject: [PATCH 182/231] Bump aiohomekit to 1.5.8 (#78515) --- 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 0b67f80bac5..338015d8c40 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==1.5.7"], + "requirements": ["aiohomekit==1.5.8"], "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 057167f0bb7..03b67659a25 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==1.5.7 +aiohomekit==1.5.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc733f04a56..d97afa95063 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==1.5.7 +aiohomekit==1.5.8 # homeassistant.components.emulated_hue # homeassistant.components.http From c1809681b69a0016e059811f5e7638696f0f3c88 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 17 Sep 2022 13:43:35 -0400 Subject: [PATCH 183/231] Fix zwave_js update entity startup state (#78563) * Fix update entity startup state * Only write state if there is a change * Add test to show that when an existing entity gets recreated, skipped version does not reset * Remove unused blocks * Update homeassistant/components/zwave_js/update.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/update.py | 34 ++++++----- tests/components/zwave_js/test_update.py | 63 ++++++++++++++++++++- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 932ed46a0fc..1358258b892 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -96,7 +96,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_name = "Firmware" self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.firmware_update" - self._attr_installed_version = self._attr_latest_version = node.firmware_version + self._attr_installed_version = node.firmware_version # device may not be precreated in main handler yet self._attr_device_info = get_device_info(driver, node) @@ -184,20 +184,26 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): err, ) else: - if available_firmware_updates: - self._latest_version_firmware = latest_firmware = max( - available_firmware_updates, - key=lambda x: AwesomeVersion(x.version), + # If we have an available firmware update that is a higher version than + # what's on the node, we should advertise it, otherwise the installed + # version is the latest. + if ( + available_firmware_updates + and ( + latest_firmware := max( + available_firmware_updates, + key=lambda x: AwesomeVersion(x.version), + ) ) - - # If we have an available firmware update that is a higher version than - # what's on the node, we should advertise it, otherwise there is - # nothing to do. - new_version = latest_firmware.version - current_version = self.node.firmware_version - if AwesomeVersion(new_version) > AwesomeVersion(current_version): - self._attr_latest_version = new_version - self.async_write_ha_state() + and AwesomeVersion(latest_firmware.version) + > AwesomeVersion(self.node.firmware_version) + ): + self._latest_version_firmware = latest_firmware + self._attr_latest_version = latest_firmware.version + self.async_write_ha_state() + elif self._attr_latest_version != self._attr_installed_version: + self._attr_latest_version = self._attr_installed_version + self.async_write_ha_state() finally: self._poll_unsub = async_call_later( self.hass, timedelta(days=1), self._async_update diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 0b567d93106..a5b3059e705 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -13,8 +13,10 @@ from homeassistant.components.update.const import ( ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, ATTR_RELEASE_URL, + ATTR_SKIPPED_VERSION, DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, + SERVICE_SKIP, ) from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id @@ -64,7 +66,6 @@ async def test_update_entity_states( ): """Test update entity states.""" ws_client = await hass_ws_client(hass) - await hass.async_block_till_done() assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF @@ -453,3 +454,63 @@ async def test_update_entity_install_failed( # validate that the install task failed with pytest.raises(HomeAssistantError): await install_task + + +async def test_update_entity_reload( + hass, + client, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test update entity maintains state after reload.""" + assert hass.states.get(UPDATE_ENTITY).state == STATE_OFF + + client.async_send_command.return_value = {"updates": []} + + 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_OFF + + client.async_send_command.return_value = FIRMWARE_UPDATES + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=2)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_ON + attrs = state.attributes + assert not attrs[ATTR_AUTO_UPDATE] + assert attrs[ATTR_INSTALLED_VERSION] == "10.7" + assert not attrs[ATTR_IN_PROGRESS] + assert attrs[ATTR_LATEST_VERSION] == "11.2.4" + assert attrs[ATTR_RELEASE_URL] is None + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_SKIP, + { + ATTR_ENTITY_ID: UPDATE_ENTITY, + }, + blocking=True, + ) + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Trigger another update and make sure the skipped version is still skipped + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(days=4)) + await hass.async_block_till_done() + + state = hass.states.get(UPDATE_ENTITY) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_SKIPPED_VERSION] == "11.2.4" From ef66d8e70513b44ef3c4be6d5fb952a0923e89ae Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 16 Sep 2022 23:29:08 +0300 Subject: [PATCH 184/231] Bump pyrisco to v0.5.5 (#78566) Upgrade to pyrisco 0.5.5 --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 9703b5775bc..bd8bbfd715f 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.4"], + "requirements": ["pyrisco==0.5.5"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 03b67659a25..b7cd187739d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1826,7 +1826,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.4 +pyrisco==0.5.5 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d97afa95063..5322db36a46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1279,7 +1279,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.4 +pyrisco==0.5.5 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 3a89a49d4a6491457db3f9338b5ccb57de6847ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Sep 2022 11:18:00 +0200 Subject: [PATCH 185/231] Fix WebSocket condition testing (#78570) --- .../components/websocket_api/commands.py | 3 +- .../components/websocket_api/test_commands.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index d4596619241..c42d48a604e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -616,8 +616,7 @@ async def handle_test_condition( from homeassistant.helpers import condition # Do static + dynamic validation of the condition - config = cv.CONDITION_SCHEMA(msg["condition"]) - config = await condition.async_validate_condition_config(hass, config) + config = await condition.async_validate_condition_config(hass, msg["condition"]) # Test the condition check_condition = await condition.async_from_config(hass, config) connection.send_result( diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 354a4edeb0d..a5905a809a9 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1603,6 +1603,42 @@ async def test_test_condition(hass, websocket_client): assert msg["success"] assert msg["result"]["result"] is True + await websocket_client.send_json( + { + "id": 6, + "type": "test_condition", + "condition": { + "condition": "template", + "value_template": "{{ is_state('hello.world', 'paulus') }}", + }, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"]["result"] is True + + await websocket_client.send_json( + { + "id": 7, + "type": "test_condition", + "condition": { + "condition": "template", + "value_template": "{{ is_state('hello.world', 'frenck') }}", + }, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"]["result"] is False + async def test_execute_script(hass, websocket_client): """Test testing a condition.""" From 39dee6d426d7fced31c40a0ddcef8774761f094d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 22:45:04 -0500 Subject: [PATCH 186/231] Fix switchbot not accepting the first advertisement (#78610) --- homeassistant/components/switchbot/__init__.py | 2 +- homeassistant/components/switchbot/coordinator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 7307187bf54..58e77dfe1bf 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): - raise ConfigEntryNotReady(f"Switchbot {sensor_type} with {address} not ready") + raise ConfigEntryNotReady(f"{address} is not advertising state") entry.async_on_unload(entry.add_update_listener(_async_update_listener)) await hass.config_entries.async_forward_entry_setups( diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 94018c1b46b..ee93c74af37 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -73,7 +73,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): if adv := switchbot.parse_advertisement_data( service_info.device, service_info.advertisement ): - if "modelName" in self.data: + if "modelName" in adv.data: self._ready_event.set() _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) if not self.device.advertisement_changed(adv): From 6e62080cd9e97ef673da45d6d03a8c43a171673f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 05:13:33 -0500 Subject: [PATCH 187/231] Fix reconnect race in HomeKit Controller (#78629) --- homeassistant/components/homekit_controller/connection.py | 2 +- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index b4aaab5acf0..8afbe6a70e4 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -423,7 +423,7 @@ class HKDevice: if self._polling_interval_remover: self._polling_interval_remover() - await self.pairing.close() + await self.pairing.shutdown() await self.hass.config_entries.async_unload_platforms( self.config_entry, self.platforms diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 338015d8c40..d9e5bfb854b 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==1.5.8"], + "requirements": ["aiohomekit==1.5.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 b7cd187739d..7c79fedd32d 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==1.5.8 +aiohomekit==1.5.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5322db36a46..8bd8ce95d4d 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==1.5.8 +aiohomekit==1.5.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 7eb98ffbd1ba0d94dd93e330e19dc936d7f1db36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Sat, 17 Sep 2022 13:59:29 +0200 Subject: [PATCH 188/231] Bump qingping-ble to 0.7.0 (#78630) --- CODEOWNERS | 4 ++-- homeassistant/components/qingping/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4609e6330d6..c4706cee1b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -867,8 +867,8 @@ build.json @home-assistant/supervisor /homeassistant/components/pvpc_hourly_pricing/ @azogue /tests/components/pvpc_hourly_pricing/ @azogue /homeassistant/components/qbittorrent/ @geoffreylagaisse -/homeassistant/components/qingping/ @bdraco -/tests/components/qingping/ @bdraco +/homeassistant/components/qingping/ @bdraco @skgsergio +/tests/components/qingping/ @bdraco @skgsergio /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap_qsw/ @Noltari diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 4e1189f3782..85df751bfc7 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -11,8 +11,8 @@ "connectable": false } ], - "requirements": ["qingping-ble==0.6.0"], + "requirements": ["qingping-ble==0.7.0"], "dependencies": ["bluetooth"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco", "@skgsergio"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 7c79fedd32d..1c2adb758df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.6.0 +qingping-ble==0.7.0 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bd8ce95d4d..b90412968ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1445,7 +1445,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.6.0 +qingping-ble==0.7.0 # homeassistant.components.rachio rachiopy==1.0.3 From a2aa0e608ddfd68f91de9a965078d5480dda728d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 Sep 2022 12:52:28 -0500 Subject: [PATCH 189/231] Add a helpful message to the config_entries.OperationNotAllowed exception (#78631) We only expect this exception to be raised as a result of an implementation problem. When it is raised during production it is currently hard to trace down why its happening See #75835 --- homeassistant/config_entries.py | 10 ++++++++-- tests/test_config_entries.py | 16 +++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f37efb6c627..7e9f3b1248d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1024,7 +1024,10 @@ class ConfigEntries: raise UnknownEntry if entry.state is not ConfigEntryState.NOT_LOADED: - raise OperationNotAllowed + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id {entry.entry_id}" + f" cannot be setup because is already loaded in the {entry.state} state" + ) # Setup Component if not set up yet if entry.domain in self.hass.config.components: @@ -1046,7 +1049,10 @@ class ConfigEntries: raise UnknownEntry if not entry.state.recoverable: - raise OperationNotAllowed + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id " + f"{entry.entry_id} cannot be unloaded because it is not in a recoverable state ({entry.state})" + ) return await entry.async_unload(self.hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b923e37b636..d2d4ffe1134 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1141,7 +1141,7 @@ async def test_entry_setup_invalid_state(hass, manager, state): MockModule("comp", async_setup=mock_setup, async_setup_entry=mock_setup_entry), ) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_setup(entry.entry_id) assert len(mock_setup.mock_calls) == 0 @@ -1201,7 +1201,7 @@ async def test_entry_unload_invalid_state(hass, manager, state): mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 @@ -1296,7 +1296,7 @@ async def test_entry_reload_error(hass, manager, state): ), ) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_reload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 @@ -1370,7 +1370,10 @@ async def test_entry_disable_without_reload_support(hass, manager): assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD # Enable - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises( + config_entries.OperationNotAllowed, + match=str(config_entries.ConfigEntryState.FAILED_UNLOAD), + ): await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 @@ -3270,7 +3273,10 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): ) entry.add_to_hass(hass) - with pytest.raises(config_entries.OperationNotAllowed): + with pytest.raises( + config_entries.OperationNotAllowed, + match=str(config_entries.ConfigEntryState.SETUP_IN_PROGRESS), + ): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS From c8d16175da0444df0e16e848e8b8306b3e6915e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 17 Sep 2022 17:15:11 +0200 Subject: [PATCH 190/231] Update demetriek to 0.2.4 (#78646) --- 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 735c05e659c..9fb39f9fb47 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.2"], + "requirements": ["demetriek==0.2.4"], "codeowners": ["@robbiet480", "@frenck"], "iot_class": "local_polling", "dependencies": ["application_credentials"], diff --git a/requirements_all.txt b/requirements_all.txt index 1c2adb758df..0549afcf2bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -558,7 +558,7 @@ defusedxml==0.7.1 deluge-client==1.7.1 # homeassistant.components.lametric -demetriek==0.2.2 +demetriek==0.2.4 # homeassistant.components.denonavr denonavr==0.10.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b90412968ad..243772e9b49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ defusedxml==0.7.1 deluge-client==1.7.1 # homeassistant.components.lametric -demetriek==0.2.2 +demetriek==0.2.4 # homeassistant.components.denonavr denonavr==0.10.11 From ebeebeaec143ee6c902eb7a2c041ba612371860c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 18 Sep 2022 08:56:46 -0400 Subject: [PATCH 191/231] Handle multiple files properly in zwave_js update entity (#78658) * Handle multiple files properly in zwave_js update entity * Until we have progress, set in progress to true. And fix when we write state * fix tests * Assert we set in progress to true before we get progress * Fix tests * Comment --- homeassistant/components/zwave_js/update.py | 16 +- tests/components/zwave_js/test_update.py | 159 +++++++++++++++++++- 2 files changed, 168 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 1358258b892..cb52259b654 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -135,7 +135,7 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): @callback def _unsub_firmware_events_and_reset_progress( - self, write_state: bool = False + self, write_state: bool = True ) -> None: """Unsubscribe from firmware events and reset update install progress.""" if self._progress_unsub: @@ -221,12 +221,14 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): """Install an update.""" firmware = self._latest_version_firmware assert firmware - self._unsub_firmware_events_and_reset_progress(True) + self._unsub_firmware_events_and_reset_progress(False) + self._attr_in_progress = True + self.async_write_ha_state() self._progress_unsub = self.node.on( "firmware update progress", self._update_progress ) - self._finished_unsub = self.node.once( + self._finished_unsub = self.node.on( "firmware update finished", self._update_finished ) @@ -241,6 +243,8 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): # 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 @@ -259,8 +263,12 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._attr_in_progress = floor( 100 * self._num_files_installed / len(firmware.files) ) + + # Clear the status so we can get a new one + self._finished_status = None self.async_write_ha_state() + # If we get here, all files were installed successfully self._attr_installed_version = self._attr_latest_version = firmware.version self._latest_version_firmware = None self._unsub_firmware_events_and_reset_progress() @@ -310,4 +318,4 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): self._poll_unsub() self._poll_unsub = None - self._unsub_firmware_events_and_reset_progress() + self._unsub_firmware_events_and_reset_progress(False) diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index a5b3059e705..b2517c3dd34 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -7,7 +7,7 @@ from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand from zwave_js_server.model.firmware import FirmwareUpdateStatus -from homeassistant.components.update.const import ( +from homeassistant.components.update import ( ATTR_AUTO_UPDATE, ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, @@ -54,6 +54,19 @@ FIRMWARE_UPDATES = { ] } +FIRMWARE_UPDATE_MULTIPLE_FILES = { + "updates": [ + { + "version": "11.2.4", + "changelog": "blah 2", + "files": [ + {"target": 0, "url": "https://example2.com", "integrity": "sha2"}, + {"target": 1, "url": "https://example4.com", "integrity": "sha4"}, + ], + }, + ] +} + async def test_update_entity_states( hass, @@ -328,6 +341,11 @@ async def test_update_entity_progress( # 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 + event = Event( type="firmware update progress", data={ @@ -363,7 +381,142 @@ async def test_update_entity_progress( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is False + 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_INSTALLED_VERSION] == "11.2.4" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_OFF @@ -446,7 +599,7 @@ async def test_update_entity_install_failed( state = hass.states.get(UPDATE_ENTITY) assert state attrs = state.attributes - assert attrs[ATTR_IN_PROGRESS] is False + assert attrs[ATTR_IN_PROGRESS] == 0 assert attrs[ATTR_INSTALLED_VERSION] == "10.7" assert attrs[ATTR_LATEST_VERSION] == "11.2.4" assert state.state == STATE_ON From cce4496ad6bf10a8df2cc4764690b7ff079d77c2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 18 Sep 2022 12:17:28 +0200 Subject: [PATCH 192/231] Remove mDNS iteration from Plugwise unique ID (#78680) * Remove mDNS iteration from Plugwise unique ID * Add iteration to tests --- homeassistant/components/plugwise/config_flow.py | 2 +- tests/components/plugwise/test_config_flow.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 3fee1445758..8cf2456c0b4 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -91,7 +91,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = discovery_info _properties = discovery_info.properties - unique_id = discovery_info.hostname.split(".")[0] + unique_id = discovery_info.hostname.split(".")[0].split("-")[0] if config_entry := await self.async_set_unique_id(unique_id): try: await validate_gw_input( diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 4dbe1d2615f..84d2335b16f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -36,7 +36,8 @@ TEST_USERNAME2 = "stretch" TEST_DISCOVERY = ZeroconfServiceInfo( host=TEST_HOST, addresses=[TEST_HOST], - hostname=f"{TEST_HOSTNAME}.local.", + # The added `-2` is to simulate mDNS collision + hostname=f"{TEST_HOSTNAME}-2.local.", name="mock_name", port=DEFAULT_PORT, properties={ From 454675d86b08deb09baad811ce1bcd17c1e3aed2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 10:22:54 -0500 Subject: [PATCH 193/231] Fix bluetooth callback matchers when only matching on connectable (#78687) --- homeassistant/components/bluetooth/manager.py | 4 +- homeassistant/components/bluetooth/match.py | 53 ++++++++++++------ tests/components/bluetooth/test_init.py | 55 +++++++++++++++++++ 3 files changed, 93 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 80817deb2a1..014da818e12 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -384,11 +384,11 @@ class BluetoothManager: callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True) connectable = callback_matcher[CONNECTABLE] - self._callback_index.add_with_address(callback_matcher) + self._callback_index.add_callback_matcher(callback_matcher) @hass_callback def _async_remove_callback() -> None: - self._callback_index.remove_with_address(callback_matcher) + self._callback_index.remove_callback_matcher(callback_matcher) # If we have history for the subscriber, we can trigger the callback # immediately with the last packet so the subscriber can see the diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index dd1c9c1fa3c..1a59ee6fe4c 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -173,7 +173,7 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.service_data_uuid_set: set[str] = set() self.manufacturer_id_set: set[int] = set() - def add(self, matcher: _T) -> None: + def add(self, matcher: _T) -> bool: """Add a matcher to the index. Matchers must end up only in one bucket. @@ -185,26 +185,28 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.local_name.setdefault( _local_name_to_index_key(matcher[LOCAL_NAME]), [] ).append(matcher) - return + return True # Manufacturer data is 2nd cheapest since its all ints if MANUFACTURER_ID in matcher: self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append( matcher ) - return + return True if SERVICE_UUID in matcher: self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher) - return + return True if SERVICE_DATA_UUID in matcher: self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append( matcher ) - return + return True - def remove(self, matcher: _T) -> None: + return False + + def remove(self, matcher: _T) -> bool: """Remove a matcher from the index. Matchers only end up in one bucket, so once we have @@ -214,19 +216,21 @@ class BluetoothMatcherIndexBase(Generic[_T]): self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove( matcher ) - return + return True if MANUFACTURER_ID in matcher: self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher) - return + return True if SERVICE_UUID in matcher: self.service_uuid[matcher[SERVICE_UUID]].remove(matcher) - return + return True if SERVICE_DATA_UUID in matcher: self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher) - return + return True + + return False def build(self) -> None: """Rebuild the index sets.""" @@ -284,8 +288,11 @@ class BluetoothCallbackMatcherIndex( """Initialize the matcher index.""" super().__init__() self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {} + self.connectable: list[BluetoothCallbackMatcherWithCallback] = [] - def add_with_address(self, matcher: BluetoothCallbackMatcherWithCallback) -> None: + def add_callback_matcher( + self, matcher: BluetoothCallbackMatcherWithCallback + ) -> None: """Add a matcher to the index. Matchers must end up only in one bucket. @@ -296,10 +303,15 @@ class BluetoothCallbackMatcherIndex( self.address.setdefault(matcher[ADDRESS], []).append(matcher) return - super().add(matcher) - self.build() + if super().add(matcher): + self.build() + return - def remove_with_address( + if CONNECTABLE in matcher: + self.connectable.append(matcher) + return + + def remove_callback_matcher( self, matcher: BluetoothCallbackMatcherWithCallback ) -> None: """Remove a matcher from the index. @@ -311,8 +323,13 @@ class BluetoothCallbackMatcherIndex( self.address[matcher[ADDRESS]].remove(matcher) return - super().remove(matcher) - self.build() + if super().remove(matcher): + self.build() + return + + if CONNECTABLE in matcher: + self.connectable.remove(matcher) + return def match_callbacks( self, service_info: BluetoothServiceInfoBleak @@ -322,6 +339,9 @@ class BluetoothCallbackMatcherIndex( for matcher in self.address.get(service_info.address, []): if ble_device_matches(matcher, service_info): matches.append(matcher) + for matcher in self.connectable: + if ble_device_matches(matcher, service_info): + matches.append(matcher) return matches @@ -355,7 +375,6 @@ def ble_device_matches( # Don't check address here since all callers already # check the address and we don't want to double check # since it would result in an unreachable reject case. - if matcher.get(CONNECTABLE, True) and not service_info.connectable: return False diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e4b84b943b4..dcbf51a1dbd 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1321,6 +1321,61 @@ async def test_register_callback_by_manufacturer_id( assert service_info.manufacturer_id == 21 +async def test_register_callback_by_connectable( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by connectable.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + await async_setup_with_default_adapter(hass) + + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {CONNECTABLE: False}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + apple_device = BLEDevice("44:44:33:11:23:45", "rtx") + apple_adv = AdvertisementData( + local_name="rtx", + manufacturer_data={7676: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, apple_device, apple_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + inject_advertisement(hass, empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + assert len(callbacks) == 2 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "rtx" + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "empty" + + async def test_filtering_noisy_apple_devices( hass, mock_bleak_scanner_start, enable_bluetooth ): From f5c30ab10a01fcc065414fecea893ef3eb73159a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 11:47:13 -0500 Subject: [PATCH 194/231] Bump thermobeacon-ble to 0.3.2 (#78693) --- homeassistant/components/thermobeacon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index eb13b68a7e2..639d2362026 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -24,7 +24,7 @@ }, { "local_name": "ThermoBeacon", "connectable": false } ], - "requirements": ["thermobeacon-ble==0.3.1"], + "requirements": ["thermobeacon-ble==0.3.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 0549afcf2bf..91a8c24ad1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2369,7 +2369,7 @@ tesla-wall-connector==1.0.2 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.1 +thermobeacon-ble==0.3.2 # homeassistant.components.thermopro thermopro-ble==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 243772e9b49..8c0e0039851 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1618,7 +1618,7 @@ tesla-powerwall==0.3.18 tesla-wall-connector==1.0.2 # homeassistant.components.thermobeacon -thermobeacon-ble==0.3.1 +thermobeacon-ble==0.3.2 # homeassistant.components.thermopro thermopro-ble==0.4.3 From da81dbe6acb5d5be0e8d086764c25a91325966ba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 18 Sep 2022 13:13:54 -0400 Subject: [PATCH 195/231] Bumped version to 2022.9.5 --- 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 5b17e764b8a..c11f9a9e054 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __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 316c0eedda6..2d1213ea199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.4" +version = "2022.9.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 933dde1d1e4aa41ad7e06351dfb9336a86416851 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 20:10:05 -0500 Subject: [PATCH 196/231] Handle Modalias missing from the bluetooth adapter details on older BlueZ (#78716) --- homeassistant/components/bluetooth/const.py | 2 +- homeassistant/components/bluetooth/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 3174603f08e..891e6d8be82 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -58,7 +58,7 @@ class AdapterDetails(TypedDict, total=False): address: str sw_version: str - hw_version: str + hw_version: str | None passive_scan: bool diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 3f6c862e53d..19efab7a15c 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -46,7 +46,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: adapters[adapter] = AdapterDetails( address=adapter1["Address"], sw_version=adapter1["Name"], # This is actually the BlueZ version - hw_version=adapter1["Modalias"], + hw_version=adapter1.get("Modalias"), passive_scan="org.bluez.AdvertisementMonitorManager1" in details, ) return adapters From b3a48389789549b3cb1aabd042310137baccc9b9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 Sep 2022 11:02:15 +0200 Subject: [PATCH 197/231] Refactor MQTT tests to use modern platform schema part 1 (#77387) * Tests alarm_control_panel * Tests binary_sensor * Tests button * Tests camera * Tests Climate + corrections default config * Tests cover * Tests device_tracker * Tests fan * Tests humidifier * Fix test_supported_features test fan * Tests init * Tests legacy vacuum * Derive DEFAULT_CONFIG_LEGACY from DEFAULT_CONFIG * Commit suggestion comment changes --- .../mqtt/test_alarm_control_panel.py | 265 +-- tests/components/mqtt/test_binary_sensor.py | 341 ++-- tests/components/mqtt/test_button.py | 169 +- tests/components/mqtt/test_camera.py | 131 +- tests/components/mqtt/test_climate.py | 290 +-- tests/components/mqtt/test_cover.py | 1566 +++++++++-------- tests/components/mqtt/test_device_tracker.py | 122 +- .../mqtt/test_device_tracker_discovery.py | 39 +- tests/components/mqtt/test_fan.py | 954 +++++----- tests/components/mqtt/test_humidifier.py | 659 +++---- tests/components/mqtt/test_init.py | 35 +- tests/components/mqtt/test_legacy_vacuum.py | 284 +-- 12 files changed, 2659 insertions(+), 2196 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 7d127902f3d..e51ed9aeae9 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import alarm_control_panel +from homeassistant.components import alarm_control_panel, mqtt from homeassistant.components.mqtt.alarm_control_panel import ( MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -65,54 +65,65 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import assert_setup_component, async_fire_mqtt_message +from tests.common import async_fire_mqtt_message from tests.components.alarm_control_panel import common CODE_NUMBER = "1234" CODE_TEXT = "HELLO_CODE" DEFAULT_CONFIG = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + } } } DEFAULT_CONFIG_CODE = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", - "code": "0123", - "code_arm_required": True, + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "0123", + "code_arm_required": True, + } } } DEFAULT_CONFIG_REMOTE_CODE = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", - "code": "REMOTE_CODE", - "code_arm_required": True, + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "REMOTE_CODE", + "code_arm_required": True, + } } } DEFAULT_CONFIG_REMOTE_CODE_TEXT = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", - "code": "REMOTE_CODE_TEXT", - "code_arm_required": True, + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "REMOTE_CODE_TEXT", + "code_arm_required": True, + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN +DEFAULT_CONFIG_CODE_LEGACY = copy.deepcopy(DEFAULT_CONFIG_CODE[mqtt.DOMAIN]) +DEFAULT_CONFIG_CODE_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def alarm_control_panel_platform_only(): @@ -123,47 +134,62 @@ def alarm_control_panel_platform_only(): yield -async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_no_yaml_config): - """Test for failing with no state topic.""" - with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: - assert await async_setup_component( - hass, - alarm_control_panel.DOMAIN, +@pytest.mark.parametrize( + "config,valid", + [ + ( { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "command_topic": "alarm/command", + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "command_topic": "alarm/command", + } } }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert not config[alarm_control_panel.DOMAIN] - - -async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_no_yaml_config): - """Test failing with no command topic.""" - with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: - assert await async_setup_component( - hass, - alarm_control_panel.DOMAIN, + False, + ), + ( { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "state_topic": "alarm/state", + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + } } }, + False, + ), + ( + { + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "command_topic": "alarm/command", + "state_topic": "alarm/state", + } + } + }, + True, + ), + ], +) +async def test_fail_setup_without_state_or_command_topic(hass, config, valid): + """Test for failing setup with no state or command topic.""" + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + config, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert not config[alarm_control_panel.DOMAIN] + is valid + ) async def test_update_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test updating with via state topic.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -195,7 +221,7 @@ async def test_ignore_update_state_if_unknown_via_state_topic( """Test ignoring updates via state topic.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -227,7 +253,7 @@ async def test_publish_mqtt_no_code( """Test publishing of MQTT messages when no code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -261,7 +287,7 @@ async def test_publish_mqtt_with_code( """Test publishing of MQTT messages when code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG_CODE, ) await hass.async_block_till_done() @@ -314,7 +340,7 @@ async def test_publish_mqtt_with_remote_code( """Test publishing of MQTT messages when remode code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG_REMOTE_CODE, ) await hass.async_block_till_done() @@ -358,7 +384,7 @@ async def test_publish_mqtt_with_remote_code_text( """Test publishing of MQTT messages when remote text code is configured.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, DEFAULT_CONFIG_REMOTE_CODE_TEXT, ) await hass.async_block_till_done() @@ -405,10 +431,10 @@ async def test_publish_mqtt_with_code_required_false( code_trigger_required = False """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) - config[alarm_control_panel.DOMAIN][disable_code] = False + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN][disable_code] = False assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -453,13 +479,13 @@ async def test_disarm_publishes_mqtt_with_template( When command_template set to output json """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) - config[alarm_control_panel.DOMAIN]["code"] = "0123" - config[alarm_control_panel.DOMAIN][ + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = "0123" + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN][ "command_template" ] = '{"action":"{{ action }}","code":"{{ code }}"}' assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -477,19 +503,20 @@ async def test_update_state_via_state_topic_template( """Test updating with template_value via state topic.""" assert await async_setup_component( hass, - alarm_control_panel.DOMAIN, + mqtt.DOMAIN, { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "state_topic": "test-topic", - "value_template": "\ + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "command_topic": "test-topic", + "state_topic": "test-topic", + "value_template": "\ {% if (value | int) == 100 %}\ armed_away\ {% else %}\ disarmed\ {% endif %}", + } } }, ) @@ -508,9 +535,9 @@ async def test_update_state_via_state_topic_template( async def test_attributes_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) - config[alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -524,9 +551,9 @@ async def test_attributes_code_number(hass, mqtt_mock_entry_with_yaml_config): async def test_attributes_remote_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG_REMOTE_CODE) - config[alarm_control_panel.DOMAIN]["code"] = "REMOTE_CODE" + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = "REMOTE_CODE" - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -540,9 +567,9 @@ async def test_attributes_remote_code_number(hass, mqtt_mock_entry_with_yaml_con async def test_attributes_code_text(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) - config[alarm_control_panel.DOMAIN]["code"] = CODE_TEXT + config[mqtt.DOMAIN][alarm_control_panel.DOMAIN]["code"] = CODE_TEXT - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -561,7 +588,7 @@ async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -571,7 +598,7 @@ async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -581,7 +608,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -591,7 +618,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE, + DEFAULT_CONFIG_CODE_LEGACY, ) @@ -603,7 +630,7 @@ async def test_setting_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -615,7 +642,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -626,7 +653,7 @@ async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_c hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -639,7 +666,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -652,7 +679,7 @@ async def test_update_with_json_attrs_bad_JSON( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -663,7 +690,7 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -694,7 +721,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_alarm(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered alarm_control_panel.""" - data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, data ) @@ -704,8 +731,8 @@ async def test_discovery_update_alarm_topic_and_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -739,8 +766,8 @@ async def test_discovery_update_alarm_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -772,7 +799,7 @@ async def test_discovery_update_unchanged_alarm( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -824,7 +851,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG[alarm_control_panel.DOMAIN], + DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN], topic, value, ) @@ -836,7 +863,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -846,21 +873,27 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) 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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -870,14 +903,17 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) 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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -887,7 +923,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, alarm_control_panel.SERVICE_ALARM_DISARM, command_payload="DISARM", ) @@ -930,7 +966,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -951,7 +987,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -960,14 +996,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = alarm_control_panel.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -977,7 +1013,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = alarm_control_panel.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 7d4edace988..bbe8b978707 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import binary_sensor +from homeassistant.components import binary_sensor, mqtt from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, @@ -50,20 +50,25 @@ from .test_common import ( ) from tests.common import ( - assert_setup_component, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, ) DEFAULT_CONFIG = { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def binary_sensor_platform_only(): @@ -78,15 +83,16 @@ async def test_setting_sensor_value_expires_availability_topic( """Test the expiration of the value.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "expire_after": 4, - "force_update": True, - "availability_topic": "availability-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + "availability_topic": "availability-topic", + } } }, ) @@ -270,14 +276,15 @@ async def test_setting_sensor_value_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + } } }, ) @@ -307,14 +314,15 @@ async def test_invalid_sensor_value_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + } } }, ) @@ -348,16 +356,17 @@ async def test_setting_sensor_value_via_mqtt_message_and_template( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": '{%if is_state(entity_id,"on")-%}OFF' - "{%-else-%}ON{%-endif%}", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": '{%if is_state(entity_id,"on")-%}OFF' + "{%-else-%}ON{%-endif%}", + } } }, ) @@ -382,15 +391,16 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": "{{value | upper}}", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": "{{value | upper}}", + } } }, ) @@ -420,16 +430,17 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ """Test processing a raw value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "encoding": "", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": "{%if value|unpack('b')-%}ON{%else%}OFF{%-endif-%}", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "encoding": "", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": "{%if value|unpack('b')-%}ON{%else%}OFF{%-endif-%}", + } } }, ) @@ -454,15 +465,16 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "value_template": '{%if value == "ABC"%}ON{%endif%}', + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "value_template": '{%if value == "ABC"%}ON{%endif%}', + } } }, ) @@ -486,13 +498,14 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid sensor class.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "motion", - "state_topic": "test-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "device_class": "motion", + "state_topic": "test-topic", + } } }, ) @@ -503,25 +516,22 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("device_class") == "motion" -async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): +async def test_invalid_device_class(hass, caplog): """Test the setting of an invalid sensor class.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "abc123", - "state_topic": "test-topic", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "device_class": "abc123", + "state_topic": "test-topic", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("binary_sensor.test") - assert state is None + assert "Invalid config for [mqtt]: expected BinarySensorDeviceClass" in caplog.text async def test_availability_when_connection_lost( @@ -529,28 +539,40 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -558,14 +580,15 @@ async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + } } }, ) @@ -594,15 +617,16 @@ async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "force_update": True, + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "force_update": True, + } } }, ) @@ -631,16 +655,17 @@ async def test_off_delay(hass, mqtt_mock_entry_with_yaml_config): """Test off_delay option.""" assert await async_setup_component( hass, - binary_sensor.DOMAIN, + mqtt.DOMAIN, { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "payload_on": "ON", - "payload_off": "OFF", - "off_delay": 30, - "force_update": True, + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "off_delay": 30, + "force_update": True, + } } }, ) @@ -680,14 +705,20 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -700,7 +731,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -713,7 +744,7 @@ async def test_update_with_json_attrs_bad_JSON( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -724,7 +755,7 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -755,7 +786,7 @@ async def test_discovery_removal_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test removal of discovered binary_sensor.""" - data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, data ) @@ -765,8 +796,8 @@ async def test_discovery_update_binary_sensor_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -802,8 +833,8 @@ async def test_discovery_update_binary_sensor_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -861,7 +892,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG[binary_sensor.DOMAIN], + DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN], topic, value, attribute, @@ -873,7 +904,7 @@ async def test_discovery_update_unchanged_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -908,42 +939,60 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -953,7 +1002,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, None, ) @@ -961,7 +1010,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -970,7 +1019,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -991,11 +1040,11 @@ async def test_cleanup_triggers_and_restoring_state( ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN - config1 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config1["name"] = "test1" config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" - config2 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" @@ -1053,7 +1102,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( freezer.move_to("2022-02-02 12:02:00+01:00") domain = binary_sensor.DOMAIN - config3 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config3 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config3["name"] = "test3" config3["expire_after"] = 10 config3["state_topic"] = "test-topic3" @@ -1065,17 +1114,18 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ) mock_restore_cache(hass, (fake_state,)) - with assert_setup_component(1, domain): - assert await async_setup_component(hass, domain, {domain: config3}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {domain: config3}} + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = binary_sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1085,7 +1135,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = binary_sensor.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 68db846c91c..1274c700800 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import button +from homeassistant.components import button, mqtt from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -43,9 +43,14 @@ from .test_common import ( ) DEFAULT_CONFIG = { - button.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {button.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[button.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def button_platform_only(): @@ -59,15 +64,16 @@ async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" assert await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: { - "command_topic": "command-topic", - "name": "test", - "object_id": "test_button", - "payload_press": "beer press", - "platform": "mqtt", - "qos": "2", + mqtt.DOMAIN: { + button.DOMAIN: { + "command_topic": "command-topic", + "name": "test", + "object_id": "test_button", + "payload_press": "beer press", + "qos": "2", + } } }, ) @@ -97,14 +103,15 @@ async def test_command_template(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of MQTT commands through a command template.""" assert await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: { - "command_topic": "command-topic", - "command_template": '{ "{{ value }}": "{{ entity_id }}" }', - "name": "test", - "payload_press": "milky_way_press", - "platform": "mqtt", + mqtt.DOMAIN: { + button.DOMAIN: { + "command_topic": "command-topic", + "command_template": '{ "{{ value }}": "{{ entity_id }}" }', + "name": "test", + "payload_press": "milky_way_press", + } } }, ) @@ -133,14 +140,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -193,7 +200,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -202,14 +209,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY, None ) 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, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -218,7 +225,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, button.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + button.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -227,14 +238,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, button.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + button.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, button.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + button.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -271,8 +290,8 @@ async def test_discovery_removal_button(hass, mqtt_mock_entry_no_yaml_config, ca async def test_discovery_update_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered button.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" @@ -321,35 +340,35 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -359,59 +378,54 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, button.SERVICE_PRESS, command_payload="PRESS", state_topic=None, ) -async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): +async def test_invalid_device_class(hass): """Test device_class option with invalid value.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "device_class": "foobarnotreal", + mqtt.DOMAIN: { + button.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "device_class": "foobarnotreal", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("button.test") - assert state is None async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, - button.DOMAIN, + mqtt.DOMAIN, { - button.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "test-topic", - "device_class": "update", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "test-topic", - "device_class": "restart", - }, - { - "platform": "mqtt", - "name": "Test 3", - "command_topic": "test-topic", - }, - ] + mqtt.DOMAIN: { + button.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "test-topic", + "device_class": "update", + }, + { + "name": "Test 2", + "command_topic": "test-topic", + "device_class": "restart", + }, + { + "name": "Test 3", + "command_topic": "test-topic", + }, + ] + } }, ) await hass.async_block_till_done() @@ -443,7 +457,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -462,7 +476,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -471,14 +485,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = button.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -488,7 +502,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = button.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = button.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index a76025a608a..4d0b2fbfca0 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import camera +from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.camera import MQTT_CAMERA_ATTRIBUTES_BLOCKED from homeassistant.const import Platform from homeassistant.setup import async_setup_component @@ -43,9 +43,12 @@ from .test_common import ( from tests.common import async_fire_mqtt_message -DEFAULT_CONFIG = { - camera.DOMAIN: {"platform": "mqtt", "name": "test", "topic": "test_topic"} -} +DEFAULT_CONFIG = {mqtt.DOMAIN: {camera.DOMAIN: {"name": "test", "topic": "test_topic"}}} + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[camera.DOMAIN]["platform"] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -62,8 +65,8 @@ async def test_run_camera_setup( topic = "test/camera" await async_setup_component( hass, - "camera", - {"camera": {"platform": "mqtt", "topic": topic, "name": "Test Camera"}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {camera.DOMAIN: {"topic": topic, "name": "Test Camera"}}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -86,13 +89,14 @@ async def test_run_camera_b64_encoded( topic = "test/camera" await async_setup_component( hass, - "camera", + mqtt.DOMAIN, { - "camera": { - "platform": "mqtt", - "topic": topic, - "name": "Test Camera", - "encoding": "b64", + mqtt.DOMAIN: { + camera.DOMAIN: { + "topic": topic, + "name": "Test Camera", + "encoding": "b64", + } } }, ) @@ -110,7 +114,7 @@ async def test_run_camera_b64_encoded( assert body == "grass" -# Using CONF_ENCODING to set b64 encoding for images is deprecated Home Assistant 2022.9, use CONF_IMAGE_ENCODING instead +# Using CONF_ENCODING to set b64 encoding for images is deprecated in Home Assistant 2022.9, use CONF_IMAGE_ENCODING instead async def test_legacy_camera_b64_encoded_with_availability( hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config ): @@ -119,14 +123,15 @@ async def test_legacy_camera_b64_encoded_with_availability( topic_availability = "test/camera_availability" await async_setup_component( hass, - "camera", + mqtt.DOMAIN, { - "camera": { - "platform": "mqtt", - "topic": topic, - "name": "Test Camera", - "encoding": "b64", - "availability": {"topic": topic_availability}, + mqtt.DOMAIN: { + camera.DOMAIN: { + "topic": topic, + "name": "Test Camera", + "encoding": "b64", + "availability": {"topic": topic_availability}, + } } }, ) @@ -155,15 +160,16 @@ async def test_camera_b64_encoded_with_availability( topic_availability = "test/camera_availability" await async_setup_component( hass, - "camera", + mqtt.DOMAIN, { - "camera": { - "platform": "mqtt", - "topic": topic, - "name": "Test Camera", - "encoding": "utf-8", - "image_encoding": "b64", - "availability": {"topic": topic_availability}, + mqtt.DOMAIN: { + "camera": { + "topic": topic, + "name": "Test Camera", + "encoding": "utf-8", + "image_encoding": "b64", + "availability": {"topic": topic_availability}, + } } }, ) @@ -189,28 +195,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -219,7 +225,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -231,7 +237,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_CAMERA_ATTRIBUTES_BLOCKED, ) @@ -239,7 +245,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -248,7 +254,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, camera.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + camera.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -257,14 +267,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, camera.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + camera.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, camera.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + camera.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -293,7 +311,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered camera.""" - data = json.dumps(DEFAULT_CONFIG[camera.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[camera.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data ) @@ -341,28 +359,28 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -372,7 +390,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ["test_topic"], ) @@ -380,7 +398,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co 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, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -390,7 +408,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, None, state_topic="test_topic", state_payload=b"ON", @@ -400,7 +418,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -409,14 +427,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = camera.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -426,7 +444,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = camera.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index d5164e85718..14bd9084abe 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -6,7 +6,7 @@ from unittest.mock import call, patch import pytest import voluptuous as vol -from homeassistant.components import climate +from homeassistant.components import climate, mqtt from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, @@ -16,10 +16,9 @@ from homeassistant.components.climate.const import ( ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_ACTIONS, - DOMAIN as CLIMATE_DOMAIN, PRESET_ECO, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.components.mqtt.climate import MQTT_CLIMATE_ATTRIBUTES_BLOCKED @@ -62,30 +61,37 @@ from tests.components.climate import common ENTITY_CLIMATE = "climate.test" + DEFAULT_CONFIG = { - CLIMATE_DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_command_topic": "mode-topic", - "temperature_command_topic": "temperature-topic", - "temperature_low_command_topic": "temperature-low-topic", - "temperature_high_command_topic": "temperature-high-topic", - "fan_mode_command_topic": "fan-mode-topic", - "swing_mode_command_topic": "swing-mode-topic", - "aux_command_topic": "aux-topic", - "preset_mode_command_topic": "preset-mode-topic", - "preset_modes": [ - "eco", - "away", - "boost", - "comfort", - "home", - "sleep", - "activity", - ], + mqtt.DOMAIN: { + climate.DOMAIN: { + "name": "test", + "mode_command_topic": "mode-topic", + "temperature_command_topic": "temperature-topic", + "temperature_low_command_topic": "temperature-low-topic", + "temperature_high_command_topic": "temperature-high-topic", + "fan_mode_command_topic": "fan-mode-topic", + "swing_mode_command_topic": "swing-mode-topic", + "aux_command_topic": "aux-topic", + "preset_mode_command_topic": "preset-mode-topic", + "preset_modes": [ + "eco", + "away", + "boost", + "comfort", + "home", + "sleep", + "activity", + ], + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[climate.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def climate_platform_only(): @@ -96,7 +102,7 @@ def climate_platform_only(): async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): """Test the initial parameters.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -109,18 +115,14 @@ async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP -async def test_preset_none_in_preset_modes( - hass, mqtt_mock_entry_no_yaml_config, caplog -): +async def test_preset_none_in_preset_modes(hass, caplog): """Test the preset mode payload reset configuration.""" - config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) config["preset_modes"].append("none") - assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert "Invalid config for [climate.mqtt]: not a valid value" in caplog.text - state = hass.states.get(ENTITY_CLIMATE) - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {climate.DOMAIN: config}} + ) + assert "Invalid config for [mqtt]: not a valid value" in caplog.text # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 @@ -136,22 +138,19 @@ async def test_preset_none_in_preset_modes( ("hold_mode_state_template", "{{ value_json }}"), ], ) -async def test_preset_modes_deprecation_guard( - hass, mqtt_mock_entry_no_yaml_config, caplog, parameter, config_value -): +async def test_preset_modes_deprecation_guard(hass, caplog, parameter, config_value): """Test the configuration for invalid legacy parameters.""" - config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) config[parameter] = config_value - assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - state = hass.states.get(ENTITY_CLIMATE) - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {climate.DOMAIN: config}} + ) + assert f"[{parameter}] is an invalid option for [mqtt]. Check: mqtt->mqtt->climate->0->{parameter}" async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test the supported_features.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -170,7 +169,7 @@ async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): async def test_get_hvac_modes(hass, mqtt_mock_entry_with_yaml_config): """Test that the operation list returns the correct modes.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -193,7 +192,7 @@ async def test_set_operation_bad_attr_and_state( Also check the state. """ - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -210,7 +209,7 @@ async def test_set_operation_bad_attr_and_state( async def test_set_operation(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -225,9 +224,9 @@ async def test_set_operation(hass, mqtt_mock_entry_with_yaml_config): async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting operation mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["mode_state_topic"] = "mode-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -249,9 +248,9 @@ async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config) async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode with power command enabled.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["power_command_topic"] = "power-command" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -276,7 +275,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_ async def test_set_fan_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting fan mode without required attribute.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -293,9 +292,9 @@ async def test_set_fan_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, cap async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["fan_mode_state_topic"] = "fan-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -317,7 +316,7 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -331,7 +330,7 @@ async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting swing mode without required attribute.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -348,9 +347,9 @@ async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, c async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting swing mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["swing_mode_state_topic"] = "swing-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -372,7 +371,7 @@ async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -386,7 +385,7 @@ async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test setting the target temperature.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -425,9 +424,9 @@ async def test_set_target_temperature_pessimistic( hass, mqtt_mock_entry_with_yaml_config ): """Test setting the target temperature.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_state_topic"] = "temperature-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -449,7 +448,7 @@ async def test_set_target_temperature_pessimistic( async def test_set_target_temperature_low_high(hass, mqtt_mock_entry_with_yaml_config): """Test setting the low/high target temperature.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -467,10 +466,10 @@ async def test_set_target_temperature_low_highpessimistic( hass, mqtt_mock_entry_with_yaml_config ): """Test setting the low/high target temperature.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_low_state_topic"] = "temperature-low-state" config["climate"]["temperature_high_state_topic"] = "temperature-high-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -505,9 +504,9 @@ async def test_set_target_temperature_low_highpessimistic( async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test getting the current temperature via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["current_temperature_topic"] = "current_temperature" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -518,9 +517,9 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): """Test getting the action received via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["action_topic"] = "action" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -531,7 +530,7 @@ async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): assert hvac_action is None # Redefine actions according to https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action actions = ["off", "heating", "cooling", "drying", "idle", "fan"] - assert all(elem in actions for elem in CURRENT_HVAC_ACTIONS) + assert all(elem in actions for elem in HVACAction) for action in actions: async_fire_mqtt_message(hass, "action", action) state = hass.states.get(ENTITY_CLIMATE) @@ -543,8 +542,8 @@ async def test_set_preset_mode_optimistic( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test setting of the preset mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -591,9 +590,9 @@ async def test_set_preset_mode_pessimistic( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test setting of the preset mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["preset_mode_state_topic"] = "preset-mode-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -636,9 +635,9 @@ async def test_set_preset_mode_pessimistic( async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["aux_state_topic"] = "aux-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -664,7 +663,7 @@ async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): async def test_set_aux(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -687,28 +686,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -716,13 +715,13 @@ async def test_get_target_temperature_low_high_with_templates( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test getting temperature high/low with templates.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_low_state_topic"] = "temperature-state" config["climate"]["temperature_high_state_topic"] = "temperature-state" config["climate"]["temperature_low_state_template"] = "{{ value_json.temp_low }}" config["climate"]["temperature_high_state_template"] = "{{ value_json.temp_high }}" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -751,7 +750,7 @@ async def test_get_target_temperature_low_high_with_templates( async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test getting various attributes with templates.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) # By default, just unquote the JSON-strings config["climate"]["value_template"] = "{{ value_json }}" config["climate"]["action_template"] = "{{ value_json }}" @@ -768,7 +767,7 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog config["climate"]["aux_state_topic"] = "aux-state" config["climate"]["current_temperature_topic"] = "current-temperature" config["climate"]["preset_mode_state_topic"] = "current-preset-mode" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -850,7 +849,7 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting various attributes with templates.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) # Create simple templates config["climate"]["fan_mode_command_template"] = "fan_mode: {{ value }}" config["climate"]["preset_mode_command_template"] = "preset_mode: {{ value }}" @@ -860,7 +859,7 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}" config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -928,10 +927,10 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["min_temp"] = 26 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -944,10 +943,10 @@ async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom max temp.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["max_temp"] = 60 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -960,10 +959,10 @@ async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom temp step.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temp_step"] = 0.01 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -976,11 +975,11 @@ async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): async def test_temperature_unit(hass, mqtt_mock_entry_with_yaml_config): """Test that setting temperature unit converts temperature values.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["temperature_unit"] = "F" config["climate"]["current_temperature_topic"] = "current_temperature" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -995,7 +994,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1006,8 +1005,8 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, - CLIMATE_DOMAIN, - DEFAULT_CONFIG, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, MQTT_CLIMATE_ATTRIBUTES_BLOCKED, ) @@ -1015,7 +1014,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1024,7 +1023,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1033,21 +1036,29 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + climate.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one climate per unique_id.""" config = { - CLIMATE_DOMAIN: [ + climate.DOMAIN: [ { "platform": "mqtt", "name": "Test 1", @@ -1065,7 +1076,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ] } await help_test_unique_id( - hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, config + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, config ) @@ -1095,12 +1106,12 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - CLIMATE_DOMAIN, + climate.DOMAIN, config, topic, value, @@ -1111,9 +1122,9 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered climate.""" - data = json.dumps(DEFAULT_CONFIG[CLIMATE_DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) await help_test_discovery_removal( - hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data + hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, data ) @@ -1122,7 +1133,7 @@ async def test_discovery_update_climate(hass, mqtt_mock_entry_no_yaml_config, ca config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, config1, config2 ) @@ -1138,7 +1149,7 @@ async def test_discovery_update_unchanged_climate( hass, mqtt_mock_entry_no_yaml_config, caplog, - CLIMATE_DOMAIN, + climate.DOMAIN, data1, discovery_update, ) @@ -1150,42 +1161,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, data1, data2 ) async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - CLIMATE_DOMAIN: { + climate.DOMAIN: { "platform": "mqtt", "name": "test", "mode_state_topic": "test-topic", @@ -1195,7 +1206,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co await help_test_entity_id_update_subscriptions( hass, mqtt_mock_entry_with_yaml_config, - CLIMATE_DOMAIN, + climate.DOMAIN, config, ["test-topic", "avty-topic"], ) @@ -1204,14 +1215,14 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co 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, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - CLIMATE_DOMAIN: { + climate.DOMAIN: { "platform": "mqtt", "name": "test", "mode_command_topic": "command-topic", @@ -1221,7 +1232,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): await help_test_entity_debug_info_message( hass, mqtt_mock_entry_no_yaml_config, - CLIMATE_DOMAIN, + climate.DOMAIN, config, climate.SERVICE_TURN_ON, command_topic="command-topic", @@ -1232,7 +1243,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_precision_default(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to tenths works as intended.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1246,9 +1257,9 @@ async def test_precision_default(hass, mqtt_mock_entry_with_yaml_config): async def test_precision_halves(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to halves works as intended.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["precision"] = 0.5 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1262,9 +1273,9 @@ async def test_precision_halves(hass, mqtt_mock_entry_with_yaml_config): async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to whole works as intended.""" - config = copy.deepcopy(DEFAULT_CONFIG) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) config["climate"]["precision"] = 1.0 - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1364,7 +1375,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = climate.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic != "preset_mode_command_topic": del config["preset_mode_command_topic"] del config["preset_modes"] @@ -1385,8 +1396,8 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" - domain = CLIMATE_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = climate.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1394,15 +1405,15 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" - domain = CLIMATE_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = climate.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" - platform = CLIMATE_DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + platform = climate.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1412,7 +1423,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = climate.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = climate.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index b1c162073ed..a91f0ecc6ca 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import cover +from homeassistant.components import cover, mqtt from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -81,9 +81,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - cover.DOMAIN: {"platform": "mqtt", "name": "test", "state_topic": "test-topic"} + mqtt.DOMAIN: {cover.DOMAIN: {"name": "test", "state_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[cover.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def cover_platform_only(): @@ -96,17 +101,18 @@ async def test_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -134,19 +140,20 @@ async def test_opening_and_closing_state_via_custom_state_payload( """Test the controlling opening and closing state via a custom payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "state_opening": "34", - "state_closing": "--43", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "state_opening": "34", + "state_closing": "--43", + } } }, ) @@ -179,18 +186,19 @@ async def test_open_closed_state_from_position_optimistic( """Test the state after setting the position using optimistic mode.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "position-topic", - "set_position_topic": "set-position-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "optimistic": True, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "position-topic", + "set_position_topic": "set-position-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "optimistic": True, + } } }, ) @@ -227,19 +235,20 @@ async def test_position_via_position_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "position_open": 100, - "position_closed": 0, - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -265,20 +274,21 @@ async def test_state_via_template(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "value_template": "\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": "\ {% if (value | multiply(0.01) | int) == 0 %}\ closed\ {% else %}\ open\ {% endif %}", + } } }, ) @@ -303,20 +313,21 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock_entry_with_yaml_ """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "value_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": '\ {% if value == "open" or value == "closed" %}\ {{ value }}\ {% else %}\ {{ states(entity_id) }}\ {% endif %}', + } } }, ) @@ -345,15 +356,16 @@ async def test_state_via_template_with_json_value( """Test the controlling state via topic with JSON value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "value_template": "{{ value_json.Var1 }}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": "{{ value_json.Var1 }}", + } } }, ) @@ -387,20 +399,21 @@ async def test_position_via_template_and_entity_id( """Test the controlling state via topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "qos": 0, - "position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "qos": 0, + "position_template": '\ {% if state_attr(entity_id, "current_position") == None %}\ {{ value }}\ {% else %}\ {{ state_attr(entity_id, "current_position") + value | int }}\ {% endif %}', + } } }, ) @@ -442,8 +455,8 @@ async def test_optimistic_flag( """Test assumed_state is set correctly.""" assert await async_setup_component( hass, - cover.DOMAIN, - {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {cover.DOMAIN: {**config, "name": "test", "qos": 0}}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -460,13 +473,14 @@ async def test_optimistic_state_change(hass, mqtt_mock_entry_with_yaml_config): """Test changing state optimistically.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + } } }, ) @@ -519,15 +533,16 @@ async def test_optimistic_state_change_with_position( """Test changing state optimistically.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "optimistic": True, - "command_topic": "command-topic", - "position_topic": "position-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "optimistic": True, + "command_topic": "command-topic", + "position_topic": "position-topic", + "qos": 0, + } } }, ) @@ -583,14 +598,15 @@ async def test_send_open_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of open_cover.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 2, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 2, + } } }, ) @@ -613,14 +629,15 @@ async def test_send_close_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of close_cover.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 2, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 2, + } } }, ) @@ -643,14 +660,15 @@ async def test_send_stop__cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of stop_cover.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 2, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 2, + } } }, ) @@ -673,18 +691,19 @@ async def test_current_cover_position(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 100, - "position_closed": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -725,18 +744,19 @@ async def test_current_cover_position_inverted(hass, mqtt_mock_entry_with_yaml_c """Test the current cover position.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 0, - "position_closed": 100, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 0, + "position_closed": 100, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -784,44 +804,45 @@ async def test_current_cover_position_inverted(hass, mqtt_mock_entry_with_yaml_c assert hass.states.get("cover.test").state == STATE_CLOSED -async def test_optimistic_position(hass, mqtt_mock_entry_no_yaml_config): +async def test_optimistic_position(hass, caplog): """Test optimistic position is not supported.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("cover.test") - assert state is None + assert ( + "Invalid config for [mqtt]: 'set_position_topic' must be set together with 'position_topic'" + in caplog.text + ) async def test_position_update(hass, mqtt_mock_entry_with_yaml_config): """Test cover position update from received MQTT message.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_open": 100, - "position_closed": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_open": 100, + "position_closed": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -853,20 +874,21 @@ async def test_set_position_templated( """Test setting cover position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": pos_template, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": pos_template, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -891,17 +913,17 @@ async def test_set_position_templated_and_attributes( """Test setting cover position via template and using entities attributes.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": '\ {% if position > 99 %}\ {% if state_attr(entity_id, "current_position") == None %}\ {{ 5 }}\ @@ -911,9 +933,10 @@ async def test_set_position_templated_and_attributes( {% else %}\ {{ 42 }}\ {% endif %}', - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -934,22 +957,23 @@ async def test_set_tilt_templated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover tilt position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "tilt_command_topic": "tilt-command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": "{{position-1}}", - "tilt_command_template": "{{tilt_position+1}}", - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": "{{tilt_position+1}}", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -974,26 +998,27 @@ async def test_set_tilt_templated_and_attributes( """Test setting cover tilt position via template and using entities attributes.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "get-position-topic", - "command_topic": "command-topic", - "tilt_command_topic": "tilt-command-topic", - "position_open": 100, - "position_closed": 0, - "set_position_topic": "set-position-topic", - "set_position_template": "{{position-1}}", - "tilt_command_template": "{" - '"entity_id": "{{ entity_id }}",' - '"value": {{ value }},' - '"tilt_position": {{ tilt_position }}' - "}", - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": "{" + '"entity_id": "{{ entity_id }}",' + '"value": {{ value }},' + '"tilt_position": {{ tilt_position }}' + "}", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1061,17 +1086,18 @@ async def test_set_position_untemplated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "position-topic", - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "position-topic", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1094,19 +1120,20 @@ async def test_set_position_untemplated_custom_percentage_range( """Test setting cover position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "position_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "position-topic", - "position_open": 0, - "position_closed": 100, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "position_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "position-topic", + "position_open": 0, + "position_closed": 100, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1127,17 +1154,18 @@ async def test_no_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test with no command topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command", - "tilt_status_topic": "tilt-status", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command", + "tilt_status_topic": "tilt-status", + } } }, ) @@ -1151,16 +1179,17 @@ async def test_no_payload_close(hass, mqtt_mock_entry_with_yaml_config): """Test with no close payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": None, - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": None, + "payload_stop": "STOP", + } } }, ) @@ -1174,16 +1203,17 @@ async def test_no_payload_open(hass, mqtt_mock_entry_with_yaml_config): """Test with no open payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, - "payload_open": None, - "payload_close": "CLOSE", - "payload_stop": "STOP", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": None, + "payload_close": "CLOSE", + "payload_stop": "STOP", + } } }, ) @@ -1197,16 +1227,17 @@ async def test_no_payload_stop(hass, mqtt_mock_entry_with_yaml_config): """Test with no stop payload.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": None, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": None, + } } }, ) @@ -1220,18 +1251,19 @@ async def test_with_command_topic_and_tilt(hass, mqtt_mock_entry_with_yaml_confi """Test with command topic and tilt config.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "command_topic": "test", - "platform": "mqtt", - "name": "test", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command", - "tilt_status_topic": "tilt-status", + mqtt.DOMAIN: { + cover.DOMAIN: { + "command_topic": "test", + "name": "test", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command", + "tilt_status_topic": "tilt-status", + } } }, ) @@ -1245,19 +1277,20 @@ async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test the defaults.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command", - "tilt_status_topic": "tilt-status", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command", + "tilt_status_topic": "tilt-status", + } } }, ) @@ -1273,19 +1306,20 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_conf """Test tilt defaults on close/open.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + } } }, ) @@ -1356,21 +1390,22 @@ async def test_tilt_given_value(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 80, - "tilt_closed_value": 25, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 80, + "tilt_closed_value": 25, + } } }, ) @@ -1445,22 +1480,23 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock_entry_with_yaml_confi """Test tilting to a given value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 80, - "tilt_closed_value": 25, - "tilt_optimistic": True, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 80, + "tilt_closed_value": 25, + "tilt_optimistic": True, + } } }, ) @@ -1522,24 +1558,25 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock_entry_with_yaml_co """Test tilting to a given value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 25, - "tilt_closed_value": 0, - "tilt_min": 0, - "tilt_max": 50, - "tilt_optimistic": True, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 25, + "tilt_closed_value": 0, + "tilt_min": 0, + "tilt_max": 50, + "tilt_optimistic": True, + } } }, ) @@ -1599,19 +1636,20 @@ async def test_tilt_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + } } }, ) @@ -1637,22 +1675,23 @@ async def test_tilt_via_topic_template(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT and template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", - "tilt_opened_value": 400, - "tilt_closed_value": 125, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + } } }, ) @@ -1680,22 +1719,23 @@ async def test_tilt_via_topic_template_json_value( """Test tilt by updating status via MQTT and template with JSON value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_status_template": "{{ value_json.Var1 }}", - "tilt_opened_value": 400, - "tilt_closed_value": 125, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_status_template": "{{ value_json.Var1 }}", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + } } }, ) @@ -1727,21 +1767,22 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock_entry_with_yaml_conf """Test tilt status via MQTT with altered tilt range.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1776,21 +1817,22 @@ async def test_tilt_status_out_of_range_warning( """Test tilt status via MQTT tilt out of range warning message.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1810,21 +1852,22 @@ async def test_tilt_status_not_numeric_warning( """Test tilt status via MQTT tilt not numeric warning message.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1842,21 +1885,22 @@ async def test_tilt_via_topic_altered_range_inverted( """Test tilt status via MQTT with altered tilt range and inverted tilt position.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_min": 50, - "tilt_max": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_min": 50, + "tilt_max": 0, + } } }, ) @@ -1891,24 +1935,25 @@ async def test_tilt_via_topic_template_altered_range( """Test tilt status via MQTT and template with altered tilt range.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", - "tilt_opened_value": 400, - "tilt_closed_value": 125, - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_status_template": "{{ (value | multiply(0.01)) | int }}", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -1941,19 +1986,20 @@ async def test_tilt_position(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + } } }, ) @@ -1976,20 +2022,21 @@ async def test_tilt_position_templated(hass, mqtt_mock_entry_with_yaml_config): """Test tilt position via template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_command_template": "{{100-32}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_command_template": "{{100-32}}", + } } }, ) @@ -2012,23 +2059,24 @@ async def test_tilt_position_altered_range(hass, mqtt_mock_entry_with_yaml_confi """Test tilt via method invocation with altered range.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "qos": 0, - "payload_open": "OPEN", - "payload_close": "CLOSE", - "payload_stop": "STOP", - "tilt_command_topic": "tilt-command-topic", - "tilt_status_topic": "tilt-status-topic", - "tilt_opened_value": 400, - "tilt_closed_value": 125, - "tilt_min": 0, - "tilt_max": 50, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + "tilt_command_topic": "tilt-command-topic", + "tilt_status_topic": "tilt-status-topic", + "tilt_opened_value": 400, + "tilt_closed_value": 125, + "tilt_min": 0, + "tilt_max": 50, + } } }, ) @@ -2396,28 +2444,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2425,13 +2473,14 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid device class.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "garage", - "state_topic": "test-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "device_class": "garage", + "state_topic": "test-topic", + } } }, ) @@ -2442,25 +2491,22 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("device_class") == "garage" -async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): +async def test_invalid_device_class(hass, caplog): """Test the setting of an invalid device class.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "device_class": "abc123", - "state_topic": "test-topic", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "device_class": "abc123", + "state_topic": "test-topic", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("cover.test") - assert state is None + assert "Invalid config for [mqtt]: expected CoverDeviceClass" in caplog.text async def test_setting_attribute_via_mqtt_json_message( @@ -2468,7 +2514,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2480,7 +2526,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_COVER_ATTRIBUTES_BLOCKED, ) @@ -2488,7 +2534,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2497,7 +2543,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, cover.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + cover.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2506,14 +2556,22 @@ async def test_update_with_json_attrs_bad_json( ): """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, cover.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + cover.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, cover.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + cover.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2588,42 +2646,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2633,7 +2691,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, SERVICE_OPEN_COVER, command_payload="OPEN", ) @@ -2645,19 +2703,20 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( """Test state is not set via position topic when both state and position topics are set.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "position_topic": "get-position-topic", - "position_open": 100, - "position_closed": 0, - "state_open": "OPEN", - "state_closed": "CLOSE", - "command_topic": "command-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "state_open": "OPEN", + "state_closed": "CLOSE", + "command_topic": "command-topic", + "qos": 0, + } } }, ) @@ -2705,20 +2764,21 @@ async def test_set_state_via_position_using_stopped_state( """Test the controlling state via position topic using stopped state.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "position_topic": "get-position-topic", - "position_open": 100, - "position_closed": 0, - "state_open": "OPEN", - "state_closed": "CLOSE", - "state_stopped": "STOPPED", - "command_topic": "command-topic", - "qos": 0, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "position_topic": "get-position-topic", + "position_open": 100, + "position_closed": 0, + "state_open": "OPEN", + "state_closed": "CLOSE", + "state_stopped": "STOPPED", + "command_topic": "command-topic", + "qos": 0, + } } }, ) @@ -2761,16 +2821,17 @@ async def test_position_via_position_topic_template( """Test position by updating status via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": "{{ (value | multiply(0.01)) | int }}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": "{{ (value | multiply(0.01)) | int }}", + } } }, ) @@ -2798,16 +2859,17 @@ async def test_position_via_position_topic_template_json_value( """Test position by updating status via position template with a JSON value.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": "{{ value_json.Var1 }}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": "{{ value_json.Var1 }}", + } } }, ) @@ -2839,21 +2901,22 @@ async def test_position_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_ """Test position by updating status via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ {% if state_attr(entity_id, "current_position") != None %}\ {{ value | int + state_attr(entity_id, "current_position") }} \ {% else %} \ {{ value }} \ {% endif %}', + } } }, ) @@ -2881,16 +2944,17 @@ async def test_position_via_position_topic_template_return_json( """Test position by updating status via position template and returning json.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '{{ {"position" : value} | tojson }}', + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : value} | tojson }}', + } } }, ) @@ -2911,16 +2975,17 @@ async def test_position_via_position_topic_template_return_json_warning( """Test position by updating status via position template returning json without position attribute.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '{{ {"pos" : value} | tojson }}', + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"pos" : value} | tojson }}', + } } }, ) @@ -2941,17 +3006,18 @@ async def test_position_and_tilt_via_position_topic_template_return_json( """Test position and tilt by updating the position via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ {{ {"position" : value, "tilt_position" : (value | int / 2)| int } | tojson }}', + } } }, ) @@ -2984,27 +3050,28 @@ async def test_position_via_position_topic_template_all_variables( """Test position by updating status via position template.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "tilt_command_topic": "tilt-command-topic", - "position_open": 99, - "position_closed": 1, - "tilt_min": 11, - "tilt_max": 22, - "position_template": "\ + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 99, + "position_closed": 1, + "tilt_min": 11, + "tilt_max": 22, + "position_template": "\ {% if value | int < tilt_max %}\ {{ tilt_min }}\ {% endif %}\ {% if value | int > position_closed %}\ {{ position_open }}\ {% endif %}", + } } }, ) @@ -3031,20 +3098,21 @@ async def test_set_state_via_stopped_state_no_position_topic( """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "state_open": "OPEN", - "state_closed": "CLOSE", - "state_stopped": "STOPPED", - "state_opening": "OPENING", - "state_closing": "CLOSING", - "command_topic": "command-topic", - "qos": 0, - "optimistic": False, + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "state_open": "OPEN", + "state_closed": "CLOSE", + "state_stopped": "STOPPED", + "state_opening": "OPENING", + "state_closing": "CLOSING", + "command_topic": "command-topic", + "qos": 0, + "optimistic": False, + } } }, ) @@ -3083,16 +3151,17 @@ async def test_position_via_position_topic_template_return_invalid_json( """Test position by updating status via position template and returning invalid json.""" assert await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "get-position-topic", - "position_template": '{{ {"position" : invalid_json} }}', + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : invalid_json} }}', + } } }, ) @@ -3104,74 +3173,65 @@ async def test_position_via_position_topic_template_return_invalid_json( assert ("Payload '{'position': Undefined}' is not numeric") in caplog.text -async def test_set_position_topic_without_get_position_topic_error( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_set_position_topic_without_get_position_topic_error(hass, caplog): """Test error when set_position_topic is used without position_topic.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "value_template": "{{100-62}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "value_template": "{{100-62}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) in caplog.text async def test_value_template_without_state_topic_error( - hass, caplog, mqtt_mock_entry_no_yaml_config + hass, + caplog, ): """Test error when value_template is used and state_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "value_template": "{{100-62}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "value_template": "{{100-62}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) in caplog.text -async def test_position_template_without_position_topic_error( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_position_template_without_position_topic_error(hass, caplog): """Test error when position_template is used and position_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "position_template": "{{100-52}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "position_template": "{{100-52}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." in caplog.text @@ -3179,74 +3239,65 @@ async def test_position_template_without_position_topic_error( async def test_set_position_template_without_set_position_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config + hass, + caplog, ): """Test error when set_position_template is used and set_position_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_template": "{{100-42}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "set_position_template": "{{100-42}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." in caplog.text ) -async def test_tilt_command_template_without_tilt_command_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_tilt_command_template_without_tilt_command_topic(hass, caplog): """Test error when tilt_command_template is used and tilt_command_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "tilt_command_template": "{{100-32}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "tilt_command_template": "{{100-32}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." in caplog.text ) -async def test_tilt_status_template_without_tilt_status_topic_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_tilt_status_template_without_tilt_status_topic_topic(hass, caplog): """Test error when tilt_status_template is used and tilt_status_topic is missing.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - cover.DOMAIN, + mqtt.DOMAIN, { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "tilt_status_template": "{{100-22}}", + mqtt.DOMAIN: { + cover.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "tilt_status_template": "{{100-22}}", + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert ( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." in caplog.text @@ -3291,7 +3342,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] config["position_topic"] = "some-position-topic" await help_test_publishing_with_custom_encoding( @@ -3311,7 +3362,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -3320,7 +3371,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -3348,7 +3399,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG[cover.DOMAIN], + DEFAULT_CONFIG_LEGACY[cover.DOMAIN], topic, value, attribute, @@ -3360,7 +3411,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = cover.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -3370,7 +3421,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = cover.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 6708703ddbb..db6e0a292d7 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,10 +1,11 @@ -"""The tests for the MQTT device tracker platform using configuration.yaml.""" +"""The tests for the MQTT device tracker platform using configuration.yaml with legacy schema.""" import json from unittest.mock import patch import pytest -from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH +from homeassistant.components import device_tracker +from homeassistant.components.device_tracker import SourceType from homeassistant.config_entries import ConfigEntryDisabler from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.setup import async_setup_component @@ -12,7 +13,6 @@ from homeassistant.setup import async_setup_component from .test_common import ( MockConfigEntry, help_test_entry_reload_with_new_config, - help_test_setup_manual_entity_from_yaml, help_test_unload_config_entry, ) @@ -45,7 +45,14 @@ async def test_legacy_ensure_device_tracker_platform_validation( dev_id = "paulus" topic = "/location/paulus" assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + } + }, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -59,13 +66,15 @@ async def test_legacy_new_message( """Test new message.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" location = "work" hass.config.components = {"mqtt", "zone"} assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + hass, + device_tracker.DOMAIN, + {device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}}, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -79,7 +88,7 @@ async def test_legacy_single_level_wildcard_topic( """Test single level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/+/paulus" topic = "/location/room/paulus" location = "work" @@ -87,8 +96,13 @@ async def test_legacy_single_level_wildcard_topic( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -102,7 +116,7 @@ async def test_legacy_multi_level_wildcard_topic( """Test multi level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/#" topic = "/location/room/paulus" location = "work" @@ -110,8 +124,13 @@ async def test_legacy_multi_level_wildcard_topic( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -125,7 +144,7 @@ async def test_legacy_single_level_wildcard_topic_not_matching( """Test not matching single level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/+/paulus" topic = "/location/paulus" location = "work" @@ -133,8 +152,13 @@ async def test_legacy_single_level_wildcard_topic_not_matching( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -148,7 +172,7 @@ async def test_legacy_multi_level_wildcard_topic_not_matching( """Test not matching multi level wildcard topic.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" subscription = "/location/#" topic = "/somewhere/room/paulus" location = "work" @@ -156,8 +180,13 @@ async def test_legacy_multi_level_wildcard_topic_not_matching( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, - {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: subscription}}}, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: subscription}, + } + }, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -171,7 +200,7 @@ async def test_legacy_matching_custom_payload_for_home_and_not_home( """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" payload_home = "present" payload_not_home = "not present" @@ -179,9 +208,9 @@ async def test_legacy_matching_custom_payload_for_home_and_not_home( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, + device_tracker.DOMAIN, { - DOMAIN: { + device_tracker.DOMAIN: { CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}, "payload_home": payload_home, @@ -205,7 +234,7 @@ async def test_legacy_not_matching_custom_payload_for_home_and_not_home( """Test not matching payload does not set state to home or not_home.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" payload_home = "present" payload_not_home = "not present" @@ -214,9 +243,9 @@ async def test_legacy_not_matching_custom_payload_for_home_and_not_home( hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, + device_tracker.DOMAIN, { - DOMAIN: { + device_tracker.DOMAIN: { CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}, "payload_home": payload_home, @@ -237,17 +266,17 @@ async def test_legacy_matching_source_type( """Test setting source type.""" await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/paulus" - source_type = SOURCE_TYPE_BLUETOOTH + source_type = SourceType.BLUETOOTH location = "work" hass.config.components = {"mqtt", "zone"} assert await async_setup_component( hass, - DOMAIN, + device_tracker.DOMAIN, { - DOMAIN: { + device_tracker.DOMAIN: { CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}, "source_type": source_type, @@ -257,23 +286,10 @@ async def test_legacy_matching_source_type( async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() - assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH - - -async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): - """Test setup using the modern schema.""" - dev_id = "jan" - entity_id = f"{DOMAIN}.{dev_id}" - topic = "/location/jan" - - hass.config.components = {"zone"} - config = {"name": dev_id, "state_topic": topic} - - await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config) - - assert hass.states.get(entity_id) is not None + assert hass.states.get(entity_id).attributes["source_type"] == SourceType.BLUETOOTH +# Deprecated in HA Core 2022.6 async def test_unload_entry( hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path ): @@ -281,13 +297,15 @@ async def test_unload_entry( # setup through configuration.yaml await mqtt_mock_entry_no_yaml_config() dev_id = "jan" - entity_id = f"{DOMAIN}.{dev_id}" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/jan" location = "home" hass.config.components = {"mqtt", "zone"} assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + hass, + device_tracker.DOMAIN, + {device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}}, ) async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() @@ -296,7 +314,7 @@ async def test_unload_entry( # setup through discovery dev_id = "piet" subscription = "/location/#" - domain = DOMAIN + domain = device_tracker.DOMAIN discovery_config = { "devices": {dev_id: subscription}, "state_topic": "some-state", @@ -330,21 +348,22 @@ async def test_unload_entry( assert discovery_setup_entity is None +# Deprecated in HA Core 2022.6 async def test_reload_entry_legacy( hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path ): """Test reloading the config entry with manual MQTT items.""" # setup through configuration.yaml await mqtt_mock_entry_no_yaml_config() - entity_id = f"{DOMAIN}.jan" + entity_id = f"{device_tracker.DOMAIN}.jan" topic = "location/jan" location = "home" config = { - DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, } hass.config.components = {"mqtt", "zone"} - assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, device_tracker.DOMAIN, config) await hass.async_block_till_done() async_fire_mqtt_message(hass, topic, location) @@ -360,6 +379,7 @@ async def test_reload_entry_legacy( assert hass.states.get(entity_id).state == location +# Deprecated in HA Core 2022.6 async def test_setup_with_disabled_entry( hass, mock_device_tracker_conf, caplog ) -> None: @@ -372,11 +392,11 @@ async def test_setup_with_disabled_entry( topic = "location/jan" config = { - DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + device_tracker.DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, } hass.config.components = {"mqtt", "zone"} - await async_setup_component(hass, DOMAIN, config) + await async_setup_component(hass, device_tracker.DOMAIN, config) await hass.async_block_till_done() assert ( diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index ac4058c9372..923ae7c9f75 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -1,27 +1,37 @@ -"""The tests for the MQTT device_tracker discovery platform.""" +"""The tests for the MQTT device_tracker platform.""" +import copy from unittest.mock import patch import pytest -from homeassistant.components import device_tracker +from homeassistant.components import device_tracker, mqtt from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, Platform from homeassistant.setup import async_setup_component -from .test_common import help_test_setting_blocked_attribute_via_mqtt_json_message +from .test_common import ( + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, +) from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry DEFAULT_CONFIG = { - device_tracker.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", + mqtt.DOMAIN: { + device_tracker.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[device_tracker.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def device_tracker_platform_only(): @@ -430,6 +440,19 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, device_tracker.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, None, ) + + +async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): + """Test setup using the modern schema.""" + dev_id = "jan" + entity_id = f"{device_tracker.DOMAIN}.{dev_id}" + topic = "/location/jan" + + config = {"name": dev_id, "state_topic": topic} + + await help_test_setup_manual_entity_from_yaml(hass, device_tracker.DOMAIN, config) + + assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 37dcefc9d3f..efe38234aee 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from voluptuous.error import MultipleInvalid -from homeassistant.components import fan +from homeassistant.components import fan, mqtt from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, @@ -67,14 +67,20 @@ from tests.common import async_fire_mqtt_message from tests.components.fan import common DEFAULT_CONFIG = { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[fan.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def fan_platform_only(): @@ -83,18 +89,15 @@ def fan_platform_only(): yield -async def test_fail_setup_if_no_command_topic( - hass, caplog, mqtt_mock_entry_no_yaml_config -): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if command fails with command topic.""" - assert await async_setup_component( - hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "mqtt", "name": "test"}} + assert not await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {fan.DOMAIN: {"name": "test"}}}, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("fan.test") is None assert ( - "Invalid config for [fan.mqtt]: required key not provided @ data['command_topic']" + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['fan'][0]['command_topic']" in caplog.text ) @@ -105,35 +108,36 @@ async def test_controlling_state_via_topic( """Test the controlling state via topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "oscillation_state_topic": "oscillation-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "payload_oscillation_off": "OsC_OfF", - "payload_oscillation_on": "OsC_On", - "percentage_state_topic": "percentage-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - "silent", - ], - "speed_range_min": 1, - "speed_range_max": 200, - "payload_reset_percentage": "rEset_percentage", - "payload_reset_preset_mode": "rEset_preset_mode", + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "oscillation_state_topic": "oscillation-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "payload_oscillation_off": "OsC_OfF", + "payload_oscillation_on": "OsC_On", + "percentage_state_topic": "percentage-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "speed_range_min": 1, + "speed_range_max": 200, + "payload_reset_percentage": "rEset_percentage", + "payload_reset_preset_mode": "rEset_preset_mode", + } } }, ) @@ -226,37 +230,36 @@ async def test_controlling_state_via_topic_with_different_speed_range( """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic1", - "percentage_command_topic": "percentage-command-topic1", - "speed_range_min": 1, - "speed_range_max": 100, - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic2", - "percentage_command_topic": "percentage-command-topic2", - "speed_range_min": 1, - "speed_range_max": 200, - }, - { - "platform": "mqtt", - "name": "test3", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic3", - "percentage_command_topic": "percentage-command-topic3", - "speed_range_min": 81, - "speed_range_max": 1023, - }, - ] + mqtt.DOMAIN: { + fan.DOMAIN: [ + { + "name": "test1", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic1", + "percentage_command_topic": "percentage-command-topic1", + "speed_range_min": 1, + "speed_range_max": 100, + }, + { + "name": "test2", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic2", + "percentage_command_topic": "percentage-command-topic2", + "speed_range_min": 1, + "speed_range_max": 200, + }, + { + "name": "test3", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic3", + "percentage_command_topic": "percentage-command-topic3", + "speed_range_min": 81, + "speed_range_max": 1023, + }, + ] + } }, ) await hass.async_block_till_done() @@ -289,22 +292,23 @@ async def test_controlling_state_via_topic_no_percentage_topics( """Test the controlling state via topic without percentage topics.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + ], + } } }, ) @@ -345,33 +349,34 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message (percentage mode).""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "oscillation_state_topic": "oscillation-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_state_topic": "percentage-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - "silent", - ], - "state_value_template": "{{ value_json.val }}", - "oscillation_value_template": "{{ value_json.val }}", - "percentage_value_template": "{{ value_json.val }}", - "preset_mode_value_template": "{{ value_json.val }}", - "speed_range_min": 1, - "speed_range_max": 100, + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "oscillation-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "percentage-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "state_value_template": "{{ value_json.val }}", + "oscillation_value_template": "{{ value_json.val }}", + "percentage_value_template": "{{ value_json.val }}", + "preset_mode_value_template": "{{ value_json.val }}", + "speed_range_min": 1, + "speed_range_max": 100, + } } }, ) @@ -450,33 +455,34 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "shared-state-topic", - "command_topic": "command-topic", - "oscillation_state_topic": "shared-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_state_topic": "shared-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_state_topic": "shared-state-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "auto", - "smart", - "whoosh", - "eco", - "breeze", - "silent", - ], - "state_value_template": "{{ value_json.state }}", - "oscillation_value_template": "{{ value_json.oscillation }}", - "percentage_value_template": "{{ value_json.percentage }}", - "preset_mode_value_template": "{{ value_json.preset_mode }}", - "speed_range_min": 1, - "speed_range_max": 100, + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "shared-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "shared-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "shared-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "state_value_template": "{{ value_json.state }}", + "oscillation_value_template": "{{ value_json.oscillation }}", + "percentage_value_template": "{{ value_json.percentage }}", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "speed_range_min": 1, + "speed_range_max": 100, + } } }, ) @@ -540,24 +546,25 @@ async def test_sending_mqtt_commands_and_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "oscillation_command_topic": "oscillation-command-topic", - "payload_oscillation_off": "OsC_OfF", - "payload_oscillation_on": "OsC_On", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "oscillation_command_topic": "oscillation-command-topic", + "payload_oscillation_off": "OsC_OfF", + "payload_oscillation_on": "OsC_On", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + } } }, ) @@ -664,37 +671,36 @@ async def test_sending_mqtt_commands_with_alternate_speed_range( """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic1", - "percentage_command_topic": "percentage-command-topic1", - "speed_range_min": 1, - "speed_range_max": 3, - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic2", - "percentage_command_topic": "percentage-command-topic2", - "speed_range_min": 1, - "speed_range_max": 200, - }, - { - "platform": "mqtt", - "name": "test3", - "command_topic": "command-topic", - "percentage_state_topic": "percentage-state-topic3", - "percentage_command_topic": "percentage-command-topic3", - "speed_range_min": 81, - "speed_range_max": 1023, - }, - ] + mqtt.DOMAIN: { + fan.DOMAIN: [ + { + "name": "test1", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic1", + "percentage_command_topic": "percentage-command-topic1", + "speed_range_min": 1, + "speed_range_max": 3, + }, + { + "name": "test2", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic2", + "percentage_command_topic": "percentage-command-topic2", + "speed_range_min": 1, + "speed_range_max": 200, + }, + { + "name": "test3", + "command_topic": "command-topic", + "percentage_state_topic": "percentage-state-topic3", + "percentage_command_topic": "percentage-command-topic3", + "speed_range_min": 81, + "speed_range_max": 1023, + }, + ] + } }, ) await hass.async_block_till_done() @@ -771,19 +777,20 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy( """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + } } }, ) @@ -902,24 +909,25 @@ async def test_sending_mqtt_command_templates_( """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "command_template": "state: {{ value }}", - "oscillation_command_topic": "oscillation-command-topic", - "oscillation_command_template": "oscillation: {{ value }}", - "percentage_command_topic": "percentage-command-topic", - "percentage_command_template": "percentage: {{ value }}", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_mode_command_template": "preset_mode: {{ value }}", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "command_template": "state: {{ value }}", + "oscillation_command_topic": "oscillation-command-topic", + "oscillation_command_template": "oscillation: {{ value }}", + "percentage_command_topic": "percentage-command-topic", + "percentage_command_template": "percentage: {{ value }}", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_command_template": "preset_mode: {{ value }}", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + } } }, ) @@ -1044,20 +1052,21 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( """Test optimistic mode without state topic without percentage command topic.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - "high", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + "high", + ], + } } }, ) @@ -1105,25 +1114,26 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "oscillation_state_topic": "oscillation-state-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_state_topic": "percentage-state-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_mode_state_topic": "preset-mode-state-topic", - "preset_modes": [ - "whoosh", - "breeze", - "silent", - ], - "optimistic": True, + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "oscillation-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "percentage-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_mode_state_topic": "preset-mode-state-topic", + "preset_modes": [ + "whoosh", + "breeze", + "silent", + ], + "optimistic": True, + } } }, ) @@ -1355,7 +1365,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[fan.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[fan.DOMAIN]) config[ATTR_PRESET_MODES] = ["eco", "auto"] config[CONF_PRESET_MODE_COMMAND_TOPIC] = "fan/some_preset_mode_command_topic" config[CONF_PERCENTAGE_COMMAND_TOPIC] = "fan/some_percentage_command_topic" @@ -1377,19 +1387,20 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, - fan.DOMAIN, + mqtt.DOMAIN, { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "percentage_command_topic": "percentage-command-topic", - "preset_modes": [ - "breeze", - "silent", - ], + mqtt.DOMAIN: { + fan.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_modes": [ + "breeze", + "silent", + ], + } } }, ) @@ -1424,177 +1435,203 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): assert state.attributes.get(fan.ATTR_OSCILLATING) is False -async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): +@pytest.mark.parametrize( + "name,config,success,features", + [ + ( + "test1", + { + "name": "test1", + "command_topic": "command-topic", + }, + True, + 0, + ), + ( + "test2", + { + "name": "test2", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + }, + True, + fan.SUPPORT_OSCILLATE, + ), + ( + "test3", + { + "name": "test3", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + }, + True, + fan.SUPPORT_SET_SPEED, + ), + ( + "test4", + { + "name": "test4", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + }, + False, + None, + ), + ( + "test5", + { + "name": "test5", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["eco", "auto"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test6", + { + "name": "test6", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["eco", "smart", "auto"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test7", + { + "name": "test7", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + }, + True, + fan.SUPPORT_SET_SPEED, + ), + ( + "test8", + { + "name": "test8", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_command_topic": "percentage-command-topic", + }, + True, + fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED, + ), + ( + "test9", + { + "name": "test9", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["Mode1", "Mode2", "Mode3"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test10", + { + "name": "test10", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["whoosh", "silent", "auto"], + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ( + "test11", + { + "name": "test11", + "command_topic": "command-topic", + "oscillation_command_topic": "oscillation-command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["Mode1", "Mode2", "Mode3"], + }, + True, + fan.SUPPORT_PRESET_MODE | fan.SUPPORT_OSCILLATE, + ), + ( + "test12", + { + "name": "test12", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "speed_range_min": 1, + "speed_range_max": 40, + }, + True, + fan.SUPPORT_SET_SPEED, + ), + ( + "test13", + { + "name": "test13", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "speed_range_min": 50, + "speed_range_max": 40, + }, + False, + None, + ), + ( + "test14", + { + "name": "test14", + "command_topic": "command-topic", + "percentage_command_topic": "percentage-command-topic", + "speed_range_min": 0, + "speed_range_max": 40, + }, + False, + None, + ), + ( + "test15", + { + "name": "test7reset_payload_in_preset_modes_a", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["auto", "smart", "normal", "None"], + }, + False, + None, + ), + ( + "test16", + { + "name": "test16", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["whoosh", "silent", "auto", "None"], + "payload_reset_preset_mode": "normal", + }, + True, + fan.SUPPORT_PRESET_MODE, + ), + ], +) +async def test_supported_features( + hass, mqtt_mock_entry_with_yaml_config, name, config, success, features +): """Test optimistic mode without state topic.""" - assert await async_setup_component( - hass, - fan.DOMAIN, - { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - }, - { - "platform": "mqtt", - "name": "test3b", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - }, - { - "platform": "mqtt", - "name": "test3c1", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - }, - { - "platform": "mqtt", - "name": "test3c2", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["eco", "auto"], - }, - { - "platform": "mqtt", - "name": "test3c3", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["eco", "smart", "auto"], - }, - { - "platform": "mqtt", - "name": "test4pcta", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - }, - { - "platform": "mqtt", - "name": "test4pctb", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - "percentage_command_topic": "percentage-command-topic", - }, - { - "platform": "mqtt", - "name": "test5pr_ma", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["Mode1", "Mode2", "Mode3"], - }, - { - "platform": "mqtt", - "name": "test5pr_mb", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["whoosh", "silent", "auto"], - }, - { - "platform": "mqtt", - "name": "test5pr_mc", - "command_topic": "command-topic", - "oscillation_command_topic": "oscillation-command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["Mode1", "Mode2", "Mode3"], - }, - { - "platform": "mqtt", - "name": "test6spd_range_a", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "speed_range_min": 1, - "speed_range_max": 40, - }, - { - "platform": "mqtt", - "name": "test6spd_range_b", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "speed_range_min": 50, - "speed_range_max": 40, - }, - { - "platform": "mqtt", - "name": "test6spd_range_c", - "command_topic": "command-topic", - "percentage_command_topic": "percentage-command-topic", - "speed_range_min": 0, - "speed_range_max": 40, - }, - { - "platform": "mqtt", - "name": "test7reset_payload_in_preset_modes_a", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["auto", "smart", "normal", "None"], - }, - { - "platform": "mqtt", - "name": "test7reset_payload_in_preset_modes_b", - "command_topic": "command-topic", - "preset_mode_command_topic": "preset-mode-command-topic", - "preset_modes": ["whoosh", "silent", "auto", "None"], - "payload_reset_preset_mode": "normal", - }, - ] - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - state = hass.states.get("fan.test1") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 - state = hass.states.get("fan.test2") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_OSCILLATE - - state = hass.states.get("fan.test3b") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED - - state = hass.states.get("fan.test3c1") - assert state is None - - state = hass.states.get("fan.test3c2") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - state = hass.states.get("fan.test3c3") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - - state = hass.states.get("fan.test4pcta") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED - state = hass.states.get("fan.test4pctb") assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_OSCILLATE | fan.SUPPORT_SET_SPEED + await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {fan.DOMAIN: config}} + ) + is success ) + if success: + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() - state = hass.states.get("fan.test5pr_ma") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - state = hass.states.get("fan.test5pr_mb") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE - - state = hass.states.get("fan.test5pr_mc") - assert ( - state.attributes.get(ATTR_SUPPORTED_FEATURES) - == fan.SUPPORT_OSCILLATE | fan.SUPPORT_PRESET_MODE - ) - - state = hass.states.get("fan.test6spd_range_a") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_SET_SPEED - assert state.attributes.get("percentage_step") == 2.5 - state = hass.states.get("fan.test6spd_range_b") - assert state is None - state = hass.states.get("fan.test6spd_range_c") - assert state is None - - state = hass.states.get("fan.test7reset_payload_in_preset_modes_a") - assert state is None - state = hass.states.get("fan.test7reset_payload_in_preset_modes_b") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE + state = hass.states.get(f"fan.{name}") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == features async def test_availability_when_connection_lost( @@ -1602,14 +1639,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1619,7 +1656,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -1632,7 +1669,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -1644,7 +1681,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1656,7 +1693,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_FAN_ATTRIBUTES_BLOCKED, ) @@ -1664,7 +1701,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1673,7 +1710,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, fan.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + fan.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1682,14 +1723,18 @@ async def test_update_with_json_attrs_bad_json( ): """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, fan.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + fan.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1767,42 +1812,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1812,7 +1857,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, fan.SERVICE_TURN_ON, ) @@ -1869,7 +1914,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "preset_mode_command_topic": config["preset_modes"] = ["auto", "eco"] @@ -1890,7 +1935,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1899,14 +1944,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1916,7 +1961,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = fan.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 38dc634578f..0cc2be638bf 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from voluptuous.error import MultipleInvalid -from homeassistant.components import humidifier +from homeassistant.components import humidifier, mqtt from homeassistant.components.humidifier import ( ATTR_HUMIDITY, ATTR_MODE, @@ -68,15 +68,21 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[humidifier.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def humidifer_platform_only(): @@ -126,16 +132,17 @@ async def async_set_humidity( await hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if command fails with command topic.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - humidifier.DOMAIN, - {humidifier.DOMAIN: {"platform": "mqtt", "name": "test"}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {humidifier.DOMAIN: {"name": "test"}}}, + ) + assert ( + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['humidifier'][0]['command_topic']. Got None" + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("humidifier.test") is None async def test_controlling_state_via_topic( @@ -144,29 +151,30 @@ async def test_controlling_state_via_topic( """Test the controlling state via topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "target_humidity_state_topic": "humidity-state-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_state_topic": "mode-state-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "auto", - "comfort", - "home", - "eco", - "sleep", - "baby", - ], - "payload_reset_humidity": "rEset_humidity", - "payload_reset_mode": "rEset_mode", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "comfort", + "home", + "eco", + "sleep", + "baby", + ], + "payload_reset_humidity": "rEset_humidity", + "payload_reset_mode": "rEset_mode", + } } }, ) @@ -248,25 +256,26 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "target_humidity_state_topic": "humidity-state-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_state_topic": "mode-state-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "auto", - "eco", - "baby", - ], - "state_value_template": "{{ value_json.val }}", - "target_humidity_state_template": "{{ value_json.val }}", - "mode_state_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_state_topic": "mode-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.val }}", + "target_humidity_state_template": "{{ value_json.val }}", + "mode_state_template": "{{ value_json.val }}", + } } }, ) @@ -336,25 +345,26 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "shared-state-topic", - "command_topic": "command-topic", - "target_humidity_state_topic": "shared-state-topic", - "target_humidity_command_topic": "percentage-command-topic", - "mode_state_topic": "shared-state-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "auto", - "eco", - "baby", - ], - "state_value_template": "{{ value_json.state }}", - "target_humidity_state_template": "{{ value_json.humidity }}", - "mode_state_template": "{{ value_json.mode }}", + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "shared-state-topic", + "target_humidity_command_topic": "percentage-command-topic", + "mode_state_topic": "shared-state-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "state_value_template": "{{ value_json.state }}", + "target_humidity_state_template": "{{ value_json.humidity }}", + "mode_state_template": "{{ value_json.mode }}", + } } }, ) @@ -414,21 +424,22 @@ async def test_sending_mqtt_commands_and_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_off": "StAtE_OfF", - "payload_on": "StAtE_On", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": [ - "eco", - "auto", - "baby", - ], + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_off": "StAtE_OfF", + "payload_on": "StAtE_On", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": [ + "eco", + "auto", + "baby", + ], + } } }, ) @@ -510,22 +521,23 @@ async def test_sending_mqtt_command_templates_( """Testing command templates with optimistic mode without state topic.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "command_template": "state: {{ value }}", - "target_humidity_command_topic": "humidity-command-topic", - "target_humidity_command_template": "humidity: {{ value }}", - "mode_command_topic": "mode-command-topic", - "mode_command_template": "mode: {{ value }}", - "modes": [ - "auto", - "eco", - "sleep", - ], + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "command_template": "state: {{ value }}", + "target_humidity_command_topic": "humidity-command-topic", + "target_humidity_command_template": "humidity: {{ value }}", + "mode_command_topic": "mode-command-topic", + "mode_command_template": "mode: {{ value }}", + "modes": [ + "auto", + "eco", + "sleep", + ], + } } }, ) @@ -607,23 +619,24 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "target_humidity_state_topic": "humidity-state-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "mode_state_topic": "mode-state-topic", - "modes": [ - "auto", - "eco", - "baby", - ], - "optimistic": True, + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "target_humidity_state_topic": "humidity-state-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "mode_state_topic": "mode-state-topic", + "modes": [ + "auto", + "eco", + "baby", + ], + "optimistic": True, + } } }, ) @@ -737,7 +750,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[humidifier.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[humidifier.DOMAIN]) config["modes"] = ["eco", "auto"] config[CONF_MODE_COMMAND_TOPIC] = "humidifier/some_mode_command_topic" await help_test_encoding_subscribable_topics( @@ -757,18 +770,19 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, - humidifier.DOMAIN, + mqtt.DOMAIN, { - humidifier.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "mode_command_topic": "mode-command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "modes": [ - "eco", - "baby", - ], + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "mode_command_topic": "mode-command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "modes": [ + "eco", + "baby", + ], + } } }, ) @@ -799,157 +813,182 @@ async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): assert state.attributes.get(humidifier.ATTR_MODE) is None -async def test_invalid_configurations(hass, mqtt_mock_entry_with_yaml_config, caplog): - """Test invalid configurations.""" - assert await async_setup_component( - hass, - humidifier.DOMAIN, - { - humidifier.DOMAIN: [ - { - "platform": "mqtt", - "name": "test_valid_1", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - }, - { - "platform": "mqtt", - "name": "test_valid_2", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "device_class": "humidifier", - }, - { - "platform": "mqtt", - "name": "test_valid_3", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "device_class": "dehumidifier", - }, - { - "platform": "mqtt", - "name": "test_invalid_device_class", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "device_class": "notsupporedSpeci@l", - }, - { - "platform": "mqtt", - "name": "test_mode_command_without_modes", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - }, - { - "platform": "mqtt", - "name": "test_invalid_humidity_min_max_1", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "min_humidity": 0, - "max_humidity": 101, - }, - { - "platform": "mqtt", - "name": "test_invalid_humidity_min_max_2", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "max_humidity": 20, - "min_humidity": 40, - }, - { - "platform": "mqtt", - "name": "test_invalid_mode_is_reset", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": ["eco", "None"], - }, - ] - }, +@pytest.mark.parametrize( + "config,valid", + [ + ( + { + "name": "test_valid_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + True, + ), + ( + { + "name": "test_valid_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "humidifier", + }, + True, + ), + ( + { + "name": "test_valid_3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "dehumidifier", + }, + True, + ), + ( + { + "name": "test_invalid_device_class", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": "notsupporedSpeci@l", + }, + False, + ), + ( + { + "name": "test_mode_command_without_modes", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + }, + False, + ), + ( + { + "name": "test_invalid_humidity_min_max_1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "min_humidity": 0, + "max_humidity": 101, + }, + False, + ), + ( + { + "name": "test_invalid_humidity_min_max_2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "max_humidity": 20, + "min_humidity": 40, + }, + False, + ), + ( + { + "name": "test_invalid_mode_is_reset", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "None"], + }, + False, + ), + ], +) +async def test_validity_configurations(hass, config, valid): + """Test validity of configurations.""" + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {humidifier.DOMAIN: config}}, + ) + is valid ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - assert hass.states.get("humidifier.test_valid_1") is not None - assert hass.states.get("humidifier.test_valid_2") is not None - assert hass.states.get("humidifier.test_valid_3") is not None - assert hass.states.get("humidifier.test_invalid_device_class") is None - assert hass.states.get("humidifier.test_mode_command_without_modes") is None - assert "not all values in the same group of inclusion" in caplog.text - caplog.clear() - - assert hass.states.get("humidifier.test_invalid_humidity_min_max_1") is None - assert hass.states.get("humidifier.test_invalid_humidity_min_max_2") is None - assert hass.states.get("humidifier.test_invalid_mode_is_reset") is None -async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): +@pytest.mark.parametrize( + "name,config,success,features", + [ + ( + "test1", + { + "name": "test1", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + True, + 0, + ), + ( + "test2", + { + "name": "test2", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + True, + humidifier.SUPPORT_MODES, + ), + ( + "test3", + { + "name": "test3", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + }, + True, + 0, + ), + ( + "test4", + { + "name": "test4", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "mode_command_topic": "mode-command-topic", + "modes": ["eco", "auto"], + }, + True, + humidifier.SUPPORT_MODES, + ), + ( + "test5", + { + "name": "test5", + "command_topic": "command-topic", + }, + False, + None, + ), + ( + "test6", + { + "name": "test6", + "target_humidity_command_topic": "humidity-command-topic", + }, + False, + None, + ), + ], +) +async def test_supported_features( + hass, mqtt_mock_entry_with_yaml_config, name, config, success, features +): """Test supported features.""" - assert await async_setup_component( - hass, - humidifier.DOMAIN, - { - humidifier.DOMAIN: [ - { - "platform": "mqtt", - "name": "test1", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - }, - { - "platform": "mqtt", - "name": "test2", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": ["eco", "auto"], - }, - { - "platform": "mqtt", - "name": "test3", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - }, - { - "platform": "mqtt", - "name": "test4", - "command_topic": "command-topic", - "target_humidity_command_topic": "humidity-command-topic", - "mode_command_topic": "mode-command-topic", - "modes": ["eco", "auto"], - }, - { - "platform": "mqtt", - "name": "test5", - "command_topic": "command-topic", - }, - { - "platform": "mqtt", - "name": "test6", - "target_humidity_command_topic": "humidity-command-topic", - }, - ] - }, + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {humidifier.DOMAIN: config}}, + ) + is success ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + if success: + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() - state = hass.states.get("humidifier.test1") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 - - state = hass.states.get("humidifier.test2") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES - - state = hass.states.get("humidifier.test3") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 - - state = hass.states.get("humidifier.test4") - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == humidifier.SUPPORT_MODES - - state = hass.states.get("humidifier.test5") - assert state is None - - state = hass.states.get("humidifier.test6") - assert state is None + state = hass.states.get(f"humidifier.{name}") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == features async def test_availability_when_connection_lost( @@ -957,14 +996,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -974,7 +1013,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -987,7 +1026,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, True, "state-topic", "1", @@ -999,7 +1038,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1011,7 +1050,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, ) @@ -1019,7 +1058,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1032,7 +1071,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) @@ -1045,14 +1084,18 @@ async def test_update_with_json_attrs_bad_json( mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, ) 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, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1148,42 +1191,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1193,7 +1236,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, humidifier.SERVICE_TURN_ON, ) @@ -1243,7 +1286,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "mode_command_topic": config["modes"] = ["auto", "eco"] @@ -1264,7 +1307,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1273,14 +1316,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1288,21 +1331,33 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_config_schema_validation(hass): - """Test invalid platform options in the config schema do pass the config validation.""" + """Test invalid platform options in the config schema do not pass the config validation.""" platform = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][platform]) config["name"] = "test" - del config["platform"] - CONFIG_SCHEMA({DOMAIN: {platform: config}}) - CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) + CONFIG_SCHEMA({mqtt.DOMAIN: {platform: config}}) + CONFIG_SCHEMA({mqtt.DOMAIN: {platform: [config]}}) with pytest.raises(MultipleInvalid): - CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) + CONFIG_SCHEMA({mqtt.DOMAIN: {platform: [{"bla": "bla"}]}}) async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = humidifier.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 020fae1732b..b794d9260ba 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -281,6 +281,7 @@ async def test_command_template_value(hass): assert cmd_tpl.async_render(None, variables=variables) == "beer" +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SELECT]) async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): """Test the rendering of entity variables.""" topic = "test/select" @@ -290,14 +291,15 @@ async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config assert await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}', + mqtt.DOMAIN: { + "select": { + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}", "entity_id": "{{ entity_id }}", "name": "{{ name }}", "this_object_state": "{{ this.state }}"}', + } } }, ) @@ -2087,20 +2089,19 @@ async def test_mqtt_ws_get_device_debug_info( await mqtt_mock_entry_no_yaml_config() config_sensor = { "device": {"identifiers": ["0AFFD2"]}, - "platform": "mqtt", "state_topic": "foobar/sensor", "unique_id": "unique", } config_trigger = { "automation_type": "trigger", "device": {"identifiers": ["0AFFD2"]}, - "platform": "mqtt", "topic": "test-topic1", "type": "foo", "subtype": "bar", } data_sensor = json.dumps(config_sensor) data_trigger = json.dumps(config_trigger) + config_sensor["platform"] = config_trigger["platform"] = mqtt.DOMAIN async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data_sensor) async_fire_mqtt_message( @@ -2151,11 +2152,11 @@ async def test_mqtt_ws_get_device_debug_info_binary( await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["0AFFD2"]}, - "platform": "mqtt", "topic": "foobar/image", "unique_id": "unique", } data = json.dumps(config) + config["platform"] = mqtt.DOMAIN async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) await hass.async_block_till_done() @@ -2397,7 +2398,9 @@ async def test_debug_info_non_mqtt( device_id=device_entry.id, ) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "test"}}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {DOMAIN: {"platform": "test"}}} + ) debug_info_data = debug_info.info_for_device(hass, device_entry.id) assert len(debug_info_data["entities"]) == 0 @@ -2409,7 +2412,6 @@ async def test_debug_info_wildcard(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/#", "unique_id": "veryunique", @@ -2456,7 +2458,6 @@ async def test_debug_info_filter_same(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/#", "unique_id": "veryunique", @@ -2515,7 +2516,6 @@ async def test_debug_info_same_topic(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/status", "availability_topic": "sensor/status", @@ -2568,7 +2568,6 @@ async def test_debug_info_qos_retain(hass, mqtt_mock_entry_no_yaml_config): await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, - "platform": "mqtt", "name": "test", "state_topic": "sensor/#", "unique_id": "veryunique", @@ -2708,6 +2707,8 @@ async def test_subscribe_connection_status( assert mqtt_connected_calls[1] is False +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_one_deprecation_warning_per_platform( hass, mqtt_mock_entry_with_yaml_config, caplog ): @@ -2801,6 +2802,8 @@ async def test_reload_entry_with_new_config(hass, tmp_path): "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] }, + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 "light": [ { "platform": "mqtt", @@ -2826,6 +2829,8 @@ async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] }, + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 "light": [ { "platform": "mqtt", diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 224e81781cf..ffe9183fe4c 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import vacuum +from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum from homeassistant.components.mqtt.vacuum.schema import services_to_strings @@ -66,27 +66,37 @@ from tests.common import async_fire_mqtt_message from tests.components.vacuum import common DEFAULT_CONFIG = { - CONF_PLATFORM: "mqtt", - CONF_NAME: "mqtttest", - CONF_COMMAND_TOPIC: "vacuum/command", - mqttvacuum.CONF_SEND_COMMAND_TOPIC: "vacuum/send_command", - mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "vacuum/state", - mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value_json.battery_level }}", - mqttvacuum.CONF_CHARGING_TOPIC: "vacuum/state", - mqttvacuum.CONF_CHARGING_TEMPLATE: "{{ value_json.charging }}", - mqttvacuum.CONF_CLEANING_TOPIC: "vacuum/state", - mqttvacuum.CONF_CLEANING_TEMPLATE: "{{ value_json.cleaning }}", - mqttvacuum.CONF_DOCKED_TOPIC: "vacuum/state", - mqttvacuum.CONF_DOCKED_TEMPLATE: "{{ value_json.docked }}", - mqttvacuum.CONF_ERROR_TOPIC: "vacuum/state", - mqttvacuum.CONF_ERROR_TEMPLATE: "{{ value_json.error }}", - mqttvacuum.CONF_FAN_SPEED_TOPIC: "vacuum/state", - mqttvacuum.CONF_FAN_SPEED_TEMPLATE: "{{ value_json.fan_speed }}", - mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", - mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + mqtt.DOMAIN: { + vacuum.DOMAIN: { + CONF_NAME: "mqtttest", + CONF_COMMAND_TOPIC: "vacuum/command", + mqttvacuum.CONF_SEND_COMMAND_TOPIC: "vacuum/send_command", + mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "vacuum/state", + mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value_json.battery_level }}", + mqttvacuum.CONF_CHARGING_TOPIC: "vacuum/state", + mqttvacuum.CONF_CHARGING_TEMPLATE: "{{ value_json.charging }}", + mqttvacuum.CONF_CLEANING_TOPIC: "vacuum/state", + mqttvacuum.CONF_CLEANING_TEMPLATE: "{{ value_json.cleaning }}", + mqttvacuum.CONF_DOCKED_TOPIC: "vacuum/state", + mqttvacuum.CONF_DOCKED_TEMPLATE: "{{ value_json.docked }}", + mqttvacuum.CONF_ERROR_TOPIC: "vacuum/state", + mqttvacuum.CONF_ERROR_TEMPLATE: "{{ value_json.error }}", + mqttvacuum.CONF_FAN_SPEED_TOPIC: "vacuum/state", + mqttvacuum.CONF_FAN_SPEED_TEMPLATE: "{{ value_json.fan_speed }}", + mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", + mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + } + } } -DEFAULT_CONFIG_2 = {vacuum.DOMAIN: {"platform": "mqtt", "name": "test"}} +DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN +DEFAULT_CONFIG_2_LEGACY = deepcopy(DEFAULT_CONFIG_2[mqtt.DOMAIN]) +DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -98,9 +108,7 @@ def vacuum_platform_only(): async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" - assert await async_setup_component( - hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} - ) + assert await async_setup_component(hass, vacuum.DOMAIN, DEFAULT_CONFIG_LEGACY) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") @@ -120,12 +128,14 @@ async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands to the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -202,13 +212,15 @@ async def test_commands_without_supported_features( hass, mqtt_mock_entry_with_yaml_config ): """Test commands which are not supported by the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = mqttvacuum.STRING_TO_SERVICE["status"] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -253,13 +265,15 @@ async def test_attributes_without_supported_features( hass, mqtt_mock_entry_with_yaml_config ): """Test attributes which are not supported by the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = mqttvacuum.STRING_TO_SERVICE["turn_on"] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -281,12 +295,14 @@ async def test_attributes_without_supported_features( async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -322,12 +338,14 @@ async def test_status(hass, mqtt_mock_entry_with_yaml_config): async def test_status_battery(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -341,12 +359,14 @@ async def test_status_battery(hass, mqtt_mock_entry_with_yaml_config): async def test_status_cleaning(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -360,12 +380,14 @@ async def test_status_cleaning(hass, mqtt_mock_entry_with_yaml_config): async def test_status_docked(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -379,12 +401,14 @@ async def test_status_docked(hass, mqtt_mock_entry_with_yaml_config): async def test_status_charging(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -398,12 +422,14 @@ async def test_status_charging(hass, mqtt_mock_entry_with_yaml_config): async def test_status_fan_speed(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -417,12 +443,14 @@ async def test_status_fan_speed(hass, mqtt_mock_entry_with_yaml_config): async def test_status_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -435,13 +463,15 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): If the vacuum doesn't support fan speed, fan speed list should be None. """ - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = ALL_SERVICES - VacuumEntityFeature.FAN_SPEED config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -451,12 +481,14 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): async def test_status_error(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -477,7 +509,7 @@ async def test_status_error(hass, mqtt_mock_entry_with_yaml_config): async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): """Test that you can use non-default templates for battery_level.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.update( { mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( @@ -488,7 +520,9 @@ async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): } ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -500,12 +534,14 @@ async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -515,82 +551,64 @@ async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_missing_battery_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_battery_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_charging_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_charging_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_CHARGING_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_cleaning_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_cleaning_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_CLEANING_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_docked_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_docked_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_DOCKED_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_error_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_error_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_ERROR_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) -async def test_missing_fan_speed_template(hass, mqtt_mock_entry_no_yaml_config): +async def test_missing_fan_speed_template(hass): """Test to make sure missing template is not allowed.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config.pop(mqttvacuum.CONF_FAN_SPEED_TEMPLATE) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - - state = hass.states.get("vacuum.mqtttest") - assert state is None + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) async def test_availability_when_connection_lost( @@ -598,28 +616,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -628,7 +646,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -640,7 +658,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2, + DEFAULT_CONFIG_2_LEGACY, MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -648,7 +666,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -657,7 +675,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -666,14 +688,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -702,7 +732,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" - data = json.dumps(DEFAULT_CONFIG_2[vacuum.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data ) @@ -748,28 +778,28 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -797,7 +827,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -874,7 +904,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config["supported_features"] = [ "turn_on", "turn_off", @@ -900,7 +930,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -909,7 +939,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -944,7 +974,8 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = deepcopy(DEFAULT_CONFIG) + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config[CONF_SUPPORTED_FEATURES] = [ "turn_on", "turn_off", @@ -976,8 +1007,21 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None From e7a616e8e4585402569434d124af08501941235e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 Sep 2022 11:03:02 +0200 Subject: [PATCH 198/231] Refactor MQTT tests to use modern platform schema part 2 (#77525) * Tests light json * Tests light template * Missed test light json * Tests light * Tests lock * Tests number * Tests scene * Tests select * Tests sensor * Tests siren * Tests state vacuuum * Tests switch * Derive DEFAULT_CONFIG_LEGACY from DEFAULT_CONFIG * Suggested comment changes --- tests/components/mqtt/test_light.py | 506 ++++++++-------- tests/components/mqtt/test_light_json.py | 601 ++++++++++--------- tests/components/mqtt/test_light_template.py | 335 ++++++----- tests/components/mqtt/test_lock.py | 306 +++++----- tests/components/mqtt/test_number.py | 255 ++++---- tests/components/mqtt/test_scene.py | 62 +- tests/components/mqtt/test_select.py | 213 ++++--- tests/components/mqtt/test_sensor.py | 419 +++++++------ tests/components/mqtt/test_siren.py | 193 +++--- tests/components/mqtt/test_state_vacuum.py | 135 +++-- tests/components/mqtt/test_switch.py | 171 +++--- 11 files changed, 1782 insertions(+), 1414 deletions(-) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index e916276cb4a..b34406f42bc 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -2,169 +2,170 @@ Configuration for RGB Version with brightness: -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light RGB" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + rgb_state_topic: "office/rgb1/rgb/status" + rgb_command_topic: "office/rgb1/rgb/set" + qos: 0 + payload_on: "on" + payload_off: "off" Configuration for XY Version with brightness: -light: - platform: mqtt - name: "Office Light XY" - state_topic: "office/xy1/light/status" - command_topic: "office/xy1/light/switch" - brightness_state_topic: "office/xy1/brightness/status" - brightness_command_topic: "office/xy1/brightness/set" - xy_state_topic: "office/xy1/xy/status" - xy_command_topic: "office/xy1/xy/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - platform: mqtt + name: "Office Light XY" + state_topic: "office/xy1/light/status" + command_topic: "office/xy1/light/switch" + brightness_state_topic: "office/xy1/brightness/status" + brightness_command_topic: "office/xy1/brightness/set" + xy_state_topic: "office/xy1/xy/status" + xy_command_topic: "office/xy1/xy/set" + qos: 0 + payload_on: "on" + payload_off: "off" config without RGB: -light: - platform: mqtt - name: "Office Light" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + qos: 0 + payload_on: "on" + payload_off: "off" config without RGB and brightness: -light: - platform: mqtt - name: "Office Light" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + qos: 0 + payload_on: "on" + payload_off: "off" config for RGB Version with brightness and scale: -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_scale: 99 - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - rgb_scale: 99 - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light RGB" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_scale: 99 + rgb_state_topic: "office/rgb1/rgb/status" + rgb_command_topic: "office/rgb1/rgb/set" + rgb_scale: 99 + qos: 0 + payload_on: "on" + payload_off: "off" config with brightness and color temp -light: - platform: mqtt - name: "Office Light Color Temp" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_scale: 99 - color_temp_state_topic: "office/rgb1/color_temp/status" - color_temp_command_topic: "office/rgb1/color_temp/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_scale: 99 + color_temp_state_topic: "office/rgb1/color_temp/status" + color_temp_command_topic: "office/rgb1/color_temp/set" + qos: 0 + payload_on: "on" + payload_off: "off" config with brightness and effect -light: - platform: mqtt - name: "Office Light Color Temp" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_scale: 99 - effect_state_topic: "office/rgb1/effect/status" - effect_command_topic: "office/rgb1/effect/set" - effect_list: - - rainbow - - colorloop - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_scale: 99 + effect_state_topic: "office/rgb1/effect/status" + effect_command_topic: "office/rgb1/effect/set" + effect_list: + - rainbow + - colorloop + qos: 0 + payload_on: "on" + payload_off: "off" config for RGB Version with RGB command template: -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - rgb_command_template: "{{ '#%02x%02x%02x' | format(red, green, blue)}}" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light RGB" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + rgb_state_topic: "office/rgb1/rgb/status" + rgb_command_topic: "office/rgb1/rgb/set" + rgb_command_template: "{{ '#%02x%02x%02x' | format(red, green, blue)}}" + qos: 0 + payload_on: "on" + payload_off: "off" Configuration for HS Version with brightness: -light: - platform: mqtt - name: "Office Light HS" - state_topic: "office/hs1/light/status" - command_topic: "office/hs1/light/switch" - brightness_state_topic: "office/hs1/brightness/status" - brightness_command_topic: "office/hs1/brightness/set" - hs_state_topic: "office/hs1/hs/status" - hs_command_topic: "office/hs1/hs/set" - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light HS" + state_topic: "office/hs1/light/status" + command_topic: "office/hs1/light/switch" + brightness_state_topic: "office/hs1/brightness/status" + brightness_command_topic: "office/hs1/brightness/set" + hs_state_topic: "office/hs1/hs/status" + hs_command_topic: "office/hs1/hs/set" + qos: 0 + payload_on: "on" + payload_off: "off" Configuration with brightness command template: -light: - platform: mqtt - name: "Office Light" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - brightness_state_topic: "office/rgb1/brightness/status" - brightness_command_topic: "office/rgb1/brightness/set" - brightness_command_template: '{ "brightness": "{{ value }}" }' - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_command_template: '{ "brightness": "{{ value }}" }' + qos: 0 + payload_on: "on" + payload_off: "off" Configuration with effect command template: -light: - platform: mqtt - name: "Office Light Color Temp" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - effect_state_topic: "office/rgb1/effect/status" - effect_command_topic: "office/rgb1/effect/set" - effect_command_template: '{ "effect": "{{ value }}" }' - effect_list: - - rainbow - - colorloop - qos: 0 - payload_on: "on" - payload_off: "off" +mqtt: + light: + - name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + effect_state_topic: "office/rgb1/effect/status" + effect_command_topic: "office/rgb1/effect/set" + effect_command_template: '{ "effect": "{{ value }}" }' + effect_list: + - rainbow + - colorloop + qos: 0 + payload_on: "on" + payload_off: "off" """ import copy @@ -172,7 +173,7 @@ from unittest.mock import call, patch import pytest -from homeassistant.components import light +from homeassistant.components import light, mqtt from homeassistant.components.mqtt.light.schema_basic import ( CONF_BRIGHTNESS_COMMAND_TOPIC, CONF_COLOR_TEMP_COMMAND_TOPIC, @@ -226,17 +227,18 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import ( - assert_setup_component, - async_fire_mqtt_message, - mock_restore_cache, -) +from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.light import common DEFAULT_CONFIG = { - light.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {light.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def light_platform_only(): @@ -245,14 +247,15 @@ def light_platform_only(): yield -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if command fails with command topic.""" - assert await async_setup_component( - hass, light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "name": "test"}} + assert not await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {light.DOMAIN: {"name": "test"}}} + ) + assert ( + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']. Got None." + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("light.test") is None async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( @@ -261,13 +264,14 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( """Test if there is no color and brightness if no topic.""" assert await async_setup_component( hass, - light.DOMAIN, + mqtt.DOMAIN, { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", + mqtt.DOMAIN: { + light.DOMAIN: { + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + } } }, ) @@ -317,7 +321,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling of the state via topic.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -344,7 +347,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi } color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -437,7 +440,6 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c """Test handling of empty data via topic.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -464,7 +466,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -570,13 +572,12 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c async def test_brightness_controlling_scale(hass, mqtt_mock_entry_with_yaml_config): """Test the brightness controlling scale.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_scale/status", "command_topic": "test_scale/set", @@ -587,10 +588,11 @@ async def test_brightness_controlling_scale(hass, mqtt_mock_entry_with_yaml_conf "payload_on": "on", "payload_off": "off", } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -620,13 +622,12 @@ async def test_brightness_from_rgb_controlling_scale( hass, mqtt_mock_entry_with_yaml_config ): """Test the brightness controlling scale.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_scale_rgb/status", "command_topic": "test_scale_rgb/set", @@ -636,10 +637,11 @@ async def test_brightness_from_rgb_controlling_scale( "payload_on": "on", "payload_off": "off", } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -664,7 +666,6 @@ async def test_controlling_state_via_topic_with_templates( """Test the setting of the state with a template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -697,7 +698,7 @@ async def test_controlling_state_via_topic_with_templates( } color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -767,7 +768,6 @@ async def test_sending_mqtt_commands_and_optimistic( """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "brightness_command_topic": "test_light_rgb/brightness/set", @@ -798,10 +798,9 @@ async def test_sending_mqtt_commands_and_optimistic( ) mock_restore_cache(hass, (fake_state,)) - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -958,7 +957,6 @@ async def test_sending_mqtt_rgb_command_with_template( """Test the sending of RGB command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "rgb_command_topic": "test_light_rgb/rgb/set", @@ -970,7 +968,7 @@ async def test_sending_mqtt_rgb_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -998,7 +996,6 @@ async def test_sending_mqtt_rgbw_command_with_template( """Test the sending of RGBW command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "rgbw_command_topic": "test_light_rgb/rgbw/set", @@ -1010,7 +1007,7 @@ async def test_sending_mqtt_rgbw_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1038,7 +1035,6 @@ async def test_sending_mqtt_rgbww_command_with_template( """Test the sending of RGBWW command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_rgb/set", "rgbww_command_topic": "test_light_rgb/rgbww/set", @@ -1050,7 +1046,7 @@ async def test_sending_mqtt_rgbww_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1078,7 +1074,6 @@ async def test_sending_mqtt_color_temp_command_with_template( """Test the sending of Color Temp command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_color_temp/set", "color_temp_command_topic": "test_light_color_temp/color_temp/set", @@ -1089,7 +1084,7 @@ async def test_sending_mqtt_color_temp_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1115,7 +1110,6 @@ async def test_on_command_first(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent before brightness.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", @@ -1123,7 +1117,7 @@ async def test_on_command_first(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1152,14 +1146,13 @@ async def test_on_command_last(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent after brightness.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1188,7 +1181,6 @@ async def test_on_command_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent as only brightness.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", @@ -1197,7 +1189,7 @@ async def test_on_command_brightness(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1244,7 +1236,6 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_conf """Test brightness scale.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "brightness_command_topic": "test_light/bright", @@ -1254,7 +1245,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_conf } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1315,14 +1306,13 @@ async def test_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgb_command_topic": "test_light/rgb", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1406,14 +1396,13 @@ async def test_on_command_rgbw(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbw_command_topic": "test_light/rgbw", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1497,14 +1486,13 @@ async def test_on_command_rgbww(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbww_command_topic": "test_light/rgbww", } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1588,7 +1576,6 @@ async def test_on_command_rgb_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode with RGB template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgb_command_topic": "test_light/rgb", @@ -1596,7 +1583,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1626,7 +1613,6 @@ async def test_on_command_rgbw_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode with RGBW template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbw_command_topic": "test_light/rgbw", @@ -1634,7 +1620,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1663,7 +1649,6 @@ async def test_on_command_rgbww_template(hass, mqtt_mock_entry_with_yaml_config) """Test on command in RGBWW brightness mode with RGBWW template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgbww_command_topic": "test_light/rgbww", @@ -1671,7 +1656,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock_entry_with_yaml_config) } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1701,7 +1686,6 @@ async def test_on_command_white(hass, mqtt_mock_entry_with_yaml_config): """Test sending commands for RGB + white light.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "tasmota_B94927/cmnd/POWER", "state_value_template": "{{ value_json.POWER }}", @@ -1721,7 +1705,7 @@ async def test_on_command_white(hass, mqtt_mock_entry_with_yaml_config): } color_modes = ["rgb", "white"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1779,7 +1763,6 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): """Test explicit color mode over mqtt.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -1807,7 +1790,7 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): } color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1928,7 +1911,6 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock_entry_with_yaml_con """Test templated explicit color mode over mqtt.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", @@ -1947,7 +1929,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock_entry_with_yaml_con } color_modes = ["color_temp", "hs"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -2010,7 +1992,6 @@ async def test_white_state_update(hass, mqtt_mock_entry_with_yaml_config): """Test state updates for RGB + white light.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "state_topic": "tasmota_B94927/tele/STATE", "command_topic": "tasmota_B94927/cmnd/POWER", @@ -2034,7 +2015,7 @@ async def test_white_state_update(hass, mqtt_mock_entry_with_yaml_config): } color_modes = ["rgb", "white"] - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -2075,7 +2056,6 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "effect_command_topic": "test_light/effect/set", @@ -2083,7 +2063,7 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -2114,28 +2094,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2144,7 +2124,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2156,7 +2136,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -2164,7 +2144,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2173,7 +2153,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2182,14 +2166,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -2230,6 +2222,8 @@ async def test_discovery_removal_light(hass, mqtt_mock_entry_no_yaml_config, cap ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_discovery_deprecated(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovery of mqtt light with deprecated platform option.""" await mqtt_mock_entry_no_yaml_config() @@ -2750,42 +2744,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -2795,7 +2789,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, light.SERVICE_TURN_ON, ) @@ -2804,7 +2798,6 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_max_mireds/set", "color_temp_command_topic": "test_max_mireds/color_temp/set", @@ -2812,7 +2805,7 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -2921,7 +2914,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] elif topic == "white_command_topic": @@ -2946,7 +2939,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -2955,7 +2948,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -3000,7 +2993,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) config[CONF_EFFECT_COMMAND_TOPIC] = "light/CONF_EFFECT_COMMAND_TOPIC" config[CONF_RGB_COMMAND_TOPIC] = "light/CONF_RGB_COMMAND_TOPIC" config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" @@ -3043,7 +3036,7 @@ async def test_encoding_subscribable_topics_brightness( init_payload, ): """Test handling of incoming encoded payload for a brightness only light.""" - config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" await help_test_encoding_subscribable_topics( @@ -3066,7 +3059,6 @@ async def test_sending_mqtt_brightness_command_with_template( """Test the sending of Brightness command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_brightness/set", "brightness_command_topic": "test_light_brightness/brightness/set", @@ -3077,7 +3069,7 @@ async def test_sending_mqtt_brightness_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -3105,7 +3097,6 @@ async def test_sending_mqtt_effect_command_with_template( """Test the sending of Effect command with template.""" config = { light.DOMAIN: { - "platform": "mqtt", "name": "test", "command_topic": "test_light_brightness/set", "brightness_command_topic": "test_light_brightness/brightness/set", @@ -3118,7 +3109,7 @@ async def test_sending_mqtt_effect_command_with_template( } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -3147,7 +3138,7 @@ async def test_sending_mqtt_effect_command_with_template( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -3157,7 +3148,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 8fac9092e3f..e61d4e77286 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -2,58 +2,63 @@ Configuration with RGB, brightness, color temp, effect, and XY: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - xy: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true + xy: true Configuration with RGB, brightness, color temp and effect: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + color_temp: true + effect: true + rgb: true Configuration with RGB, brightness and color temp: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true - color_temp: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true + color_temp: true Configuration with RGB, brightness: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - rgb: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true + rgb: true Config without RGB: -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true +mqtt: + light: + schema: json + name: mqtt_json_light_1 + state_topic: "home/rgb1" + command_topic: "home/rgb1/set" + brightness: true Config without RGB and brightness: @@ -79,7 +84,7 @@ from unittest.mock import call, patch import pytest -from homeassistant.components import light +from homeassistant.components import light, mqtt from homeassistant.components.mqtt.light.schema_basic import ( MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -128,14 +133,20 @@ from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.light import common DEFAULT_CONFIG = { - light.DOMAIN: { - "platform": "mqtt", - "schema": "json", - "name": "test", - "command_topic": "test-topic", + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test-topic", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def light_platform_only(): @@ -156,22 +167,21 @@ class JsonValidator: return json.loads(self.jsondata) == json.loads(other) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): +async def test_fail_setup_if_no_command_topic(hass, caplog): """Test if setup fails with no command topic.""" - assert await async_setup_component( + assert not await async_setup_component( hass, - light.DOMAIN, - {light.DOMAIN: {"platform": "mqtt", "schema": "json", "name": "test"}}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {light.DOMAIN: {"schema": "json", "name": "test"}}}, + ) + assert ( + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']. Got None." + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("light.test") is None @pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "xy")) -async def test_fail_setup_if_color_mode_deprecated( - hass, mqtt_mock_entry_no_yaml_config, deprecated -): +async def test_fail_setup_if_color_mode_deprecated(hass, caplog, deprecated): """Test if setup fails if color mode is combined with deprecated config keys.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -181,27 +191,32 @@ async def test_fail_setup_if_color_mode_deprecated( "color_mode": True, "command_topic": "test_light_rgb/set", "name": "test", - "platform": "mqtt", "schema": "json", "supported_color_modes": supported_color_modes, } } config[light.DOMAIN][deprecated] = True - assert await async_setup_component( + assert not await async_setup_component( hass, - light.DOMAIN, - config, + mqtt.DOMAIN, + {mqtt.DOMAIN: config}, + ) + assert ( + "Invalid config for [mqtt]: color_mode must not be combined with any of" + in caplog.text ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert hass.states.get("light.test") is None @pytest.mark.parametrize( - "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]] + "supported_color_modes,error", + [ + (["onoff", "rgb"], "Unknown error calling mqtt CONFIG_SCHEMA"), + (["brightness", "rgb"], "Unknown error calling mqtt CONFIG_SCHEMA"), + (["unknown"], "Invalid config for [mqtt]: value must be one of [ - on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} - command_off_template: 'off' - state_template: '{{ value.split(",")[0] }}' - brightness_template: '{{ value.split(",")[1] }}' - color_temp_template: '{{ value.split(",")[2] }}' - red_template: '{{ value.split(",")[4].split("-")[0] }}' - green_template: '{{ value.split(",")[4].split("-")[1] }}' - blue_template: '{{ value.split(",")[4].split("-")[2] }}' +mqtt: + light: + schema: template + name: mqtt_template_light_1 + state_topic: 'home/rgb1' + command_topic: 'home/rgb1/set' + command_on_template: > + on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }} + command_off_template: 'off' + state_template: '{{ value.split(",")[0] }}' + brightness_template: '{{ value.split(",")[1] }}' + color_temp_template: '{{ value.split(",")[2] }}' + red_template: '{{ value.split(",")[4].split("-")[0] }}' + green_template: '{{ value.split(",")[4].split("-")[1] }}' + blue_template: '{{ value.split(",")[4].split("-")[2] }}' If your light doesn't support brightness feature, omit `brightness_template`. @@ -28,7 +29,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import light +from homeassistant.components import light, mqtt from homeassistant.components.mqtt.light.schema_basic import ( MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -74,24 +75,26 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import ( - assert_setup_component, - async_fire_mqtt_message, - mock_restore_cache, -) +from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.light import common DEFAULT_CONFIG = { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test-topic", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test-topic", + "command_on_template": "on,{{ transition }}", + "command_off_template": "off,{{ transition|d }}", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[light.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def light_platform_only(): @@ -103,10 +106,9 @@ def light_platform_only(): @pytest.mark.parametrize( "test_config", [ - ({"platform": "mqtt", "schema": "template", "name": "test"},), + ({"schema": "template", "name": "test"},), ( { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_topic", @@ -114,7 +116,6 @@ def light_platform_only(): ), ( { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_topic", @@ -123,7 +124,6 @@ def light_platform_only(): ), ( { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_topic", @@ -132,36 +132,33 @@ def light_platform_only(): ), ], ) -async def test_setup_fails(hass, mqtt_mock_entry_no_yaml_config, test_config): +async def test_setup_fails(hass, caplog, test_config): """Test that setup fails with missing required configuration items.""" - with assert_setup_component(0, light.DOMAIN) as setup_config: - assert await async_setup_component( - hass, - light.DOMAIN, - {light.DOMAIN: test_config}, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() - assert not setup_config[light.DOMAIN] - assert hass.states.get("light.test") is None + assert not await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {light.DOMAIN: test_config}}, + ) + assert "Invalid config for [mqtt]" in caplog.text async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, - light.DOMAIN, + mqtt.DOMAIN, { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_light_rgb/set", - "command_on_template": "on", - "command_off_template": "off", - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test_light_rgb/set", + "command_on_template": "on", + "command_off_template": "off", + "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', + } } }, ) @@ -181,13 +178,12 @@ async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test state change via topic.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "state_topic": "test_light_rgb", @@ -201,10 +197,11 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off", "state_template": '{{ value.split(",")[0] }}', } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -236,13 +233,12 @@ async def test_state_brightness_color_effect_temp_change_via_topic( hass, mqtt_mock_entry_with_yaml_config ): """Test state, bri, color, effect, color temp change.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "effect_list": ["rainbow", "colorloop"], @@ -264,10 +260,11 @@ async def test_state_brightness_color_effect_temp_change_via_topic( "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', "effect_template": '{{ value.split(",")[4] }}', } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -340,13 +337,12 @@ async def test_sending_mqtt_commands_and_optimistic( ) mock_restore_cache(hass, (fake_state,)) - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_light_rgb/set", @@ -369,10 +365,11 @@ async def test_sending_mqtt_commands_and_optimistic( "effect_template": '{{ value.split(",")[4] }}', "qos": 2, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -470,13 +467,12 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( hass, mqtt_mock_entry_with_yaml_config ): """Test the sending of command in optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "effect_list": ["rainbow", "colorloop"], @@ -499,10 +495,11 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', "effect_template": '{{ value.split(",")[4] }}', } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -591,13 +588,12 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect sent over MQTT in optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "effect_list": ["rainbow", "colorloop"], "name": "test", @@ -606,10 +602,11 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off", "qos": 0, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -644,13 +641,12 @@ async def test_effect(hass, mqtt_mock_entry_with_yaml_config): async def test_flash(hass, mqtt_mock_entry_with_yaml_config): """Test flash sent over MQTT in optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_light_rgb/set", @@ -658,10 +654,11 @@ async def test_flash(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off", "qos": 0, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -693,13 +690,12 @@ async def test_flash(hass, mqtt_mock_entry_with_yaml_config): async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_light_rgb/set", @@ -707,10 +703,11 @@ async def test_transition(hass, mqtt_mock_entry_with_yaml_config): "command_off_template": "off,{{ transition|int|d }}", "qos": 1, } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -735,13 +732,12 @@ async def test_transition(hass, mqtt_mock_entry_with_yaml_config): async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid values are ignored.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "effect_list": ["rainbow", "colorloop"], @@ -763,10 +759,11 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', "effect_template": '{{ value.split(",")[4] }}', } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -827,28 +824,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -857,7 +854,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -869,7 +866,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -877,7 +874,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -886,7 +883,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -895,14 +896,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -1017,42 +1026,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1082,7 +1091,6 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { - "platform": "mqtt", "schema": "template", "name": "test", "command_topic": "test_max_mireds/set", @@ -1093,7 +1101,7 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): } } - assert await async_setup_component(hass, light.DOMAIN, config) + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1139,7 +1147,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] @@ -1162,7 +1170,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1171,7 +1179,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -1192,7 +1200,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) config["state_template"] = "{{ value }}" await help_test_encoding_subscribable_topics( hass, @@ -1211,7 +1219,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1221,7 +1229,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = light.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index f6dc4a0ed6d..de97de23a1e 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -4,14 +4,14 @@ from unittest.mock import patch import pytest +from homeassistant.components import lock, mqtt from homeassistant.components.lock import ( - DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, STATE_LOCKED, STATE_UNLOCKED, - SUPPORT_OPEN, + LockEntityFeature, ) from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED from homeassistant.const import ( @@ -56,9 +56,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - LOCK_DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {lock.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[lock.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def lock_platform_only(): @@ -71,17 +76,18 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } } }, ) @@ -110,17 +116,18 @@ async def test_controlling_non_default_state_via_topic( """Test the controlling state via topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "closed", - "state_unlocked": "open", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", + } } }, ) @@ -148,18 +155,19 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -186,18 +194,19 @@ async def test_controlling_non_default_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "closed", - "state_unlocked": "open", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -224,16 +233,17 @@ async def test_sending_mqtt_commands_and_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } } }, ) @@ -245,7 +255,7 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -255,7 +265,7 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -271,18 +281,19 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( """Test optimistic mode without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", - "optimistic": True, + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "optimistic": True, + } } }, ) @@ -294,7 +305,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -304,7 +315,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -320,17 +331,18 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( """Test open function of the lock without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "payload_open": "OPEN", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } } }, ) @@ -340,10 +352,10 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LockEntityFeature.OPEN await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -353,7 +365,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -363,7 +375,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) @@ -379,19 +391,20 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( """Test open function of the lock without state topic.""" assert await async_setup_component( hass, - LOCK_DOMAIN, + mqtt.DOMAIN, { - LOCK_DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_lock": "LOCK", - "payload_unlock": "UNLOCK", - "payload_open": "OPEN", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", - "optimistic": True, + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "optimistic": True, + } } }, ) @@ -401,10 +414,10 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_OPEN + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LockEntityFeature.OPEN await hass.services.async_call( - LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) @@ -414,7 +427,7 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) @@ -424,7 +437,7 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) await hass.services.async_call( - LOCK_DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + lock.DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True ) mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) @@ -439,28 +452,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -469,7 +482,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -480,8 +493,8 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, - LOCK_DOMAIN, - DEFAULT_CONFIG, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, MQTT_LOCK_ATTRIBUTES_BLOCKED, ) @@ -489,7 +502,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -498,7 +511,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -507,21 +524,25 @@ async def test_update_with_json_attrs_bad_json( ): """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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one lock per unique_id.""" config = { - LOCK_DOMAIN: [ + lock.DOMAIN: [ { "platform": "mqtt", "name": "Test 1", @@ -539,7 +560,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ] } await help_test_unique_id( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, config + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, config ) @@ -547,7 +568,7 @@ async def test_discovery_removal_lock(hass, mqtt_mock_entry_no_yaml_config, capl """Test removal of discovered lock.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' await help_test_discovery_removal( - hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, data ) @@ -566,7 +587,7 @@ async def test_discovery_update_lock(hass, mqtt_mock_entry_no_yaml_config, caplo "availability_topic": "availability_topic2", } await help_test_discovery_update( - hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, config1, config2 ) @@ -586,7 +607,7 @@ async def test_discovery_update_unchanged_lock( hass, mqtt_mock_entry_no_yaml_config, caplog, - LOCK_DOMAIN, + lock.DOMAIN, data1, discovery_update, ) @@ -598,49 +619,49 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, data1, data2 ) async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -649,8 +670,8 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): await help_test_entity_debug_info_message( hass, mqtt_mock_entry_no_yaml_config, - LOCK_DOMAIN, - DEFAULT_CONFIG, + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY, SERVICE_LOCK, command_payload="LOCK", ) @@ -679,8 +700,8 @@ async def test_publishing_with_custom_encoding( template, ): """Test publishing MQTT payload with different encoding.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -698,8 +719,8 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -707,8 +728,8 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -732,8 +753,8 @@ async def test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - LOCK_DOMAIN, - DEFAULT_CONFIG[LOCK_DOMAIN], + lock.DOMAIN, + DEFAULT_CONFIG_LEGACY[lock.DOMAIN], topic, value, attribute, @@ -743,8 +764,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): """Test setup manual configured MQTT entity.""" - platform = LOCK_DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + platform = lock.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -753,8 +774,21 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" - domain = LOCK_DOMAIN - config = DEFAULT_CONFIG[domain] + domain = lock.DOMAIN + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = lock.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 603984cffad..69b0473fb9d 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -66,9 +66,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message, mock_restore_cache_with_extra_data DEFAULT_CONFIG = { - number.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {number.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[number.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def number_platform_only(): @@ -82,16 +87,17 @@ async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, - "payload_reset": "reset!", + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "device_class": "temperature", + "unit_of_measurement": TEMP_FAHRENHEIT, + "payload_reset": "reset!", + } } }, ) @@ -131,14 +137,15 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -184,14 +191,15 @@ async def test_restore_native_value(hass, mqtt_mock_entry_with_yaml_config): ) assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": topic, - "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": topic, + "device_class": "temperature", + "unit_of_measurement": TEMP_FAHRENHEIT, + "name": "Test Number", + } } }, ) @@ -220,12 +228,13 @@ async def test_run_number_service_optimistic(hass, mqtt_mock_entry_with_yaml_con ) assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": topic, + "name": "Test Number", + } } }, ) @@ -295,13 +304,14 @@ async def test_run_number_service_optimistic_with_command_template( ) assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Number", - "command_template": '{"number": {{ value }} }', + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": topic, + "name": "Test Number", + "command_template": '{"number": {{ value }} }', + } } }, ) @@ -361,13 +371,14 @@ async def test_run_number_service(hass, mqtt_mock_entry_with_yaml_config): assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Number", + } } }, ) @@ -398,14 +409,15 @@ async def test_run_number_service_with_command_template( assert await async_setup_component( hass, - number.DOMAIN, + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Number", - "command_template": '{"number": {{ value }} }', + mqtt.DOMAIN: { + number.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Number", + "command_template": '{"number": {{ value }} }', + } } }, ) @@ -434,28 +446,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -464,7 +476,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -476,7 +488,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_NUMBER_ATTRIBUTES_BLOCKED, ) @@ -484,7 +496,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -493,7 +505,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, number.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + number.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -502,14 +518,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, number.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + number.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, number.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + number.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -540,7 +564,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered number.""" - data = json.dumps(DEFAULT_CONFIG[number.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[number.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data ) @@ -600,42 +624,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -645,7 +669,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, SERVICE_SET_VALUE, service_parameters={ATTR_VALUE: 45}, command_payload="45", @@ -658,16 +682,17 @@ async def test_min_max_step_attributes(hass, mqtt_mock_entry_with_yaml_config): topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "min": 5, - "max": 110, - "step": 20, + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + "step": 20, + } } }, ) @@ -680,25 +705,24 @@ async def test_min_max_step_attributes(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get(ATTR_STEP) == 20 -async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock_entry_no_yaml_config): +async def test_invalid_min_max_attributes(hass, caplog): """Test invalid min/max attributes.""" topic = "test/number" - await async_setup_component( + assert not await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "min": 35, - "max": 10, + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 35, + "max": 10, + } } }, ) - await hass.async_block_till_done() - await mqtt_mock_entry_no_yaml_config() assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text @@ -779,15 +803,16 @@ async def test_mqtt_payload_not_a_number_warning( ): """Test warning for MQTT payload which is not a number.""" topic = "test/number" - await async_setup_component( + assert await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + } } }, ) @@ -808,15 +833,16 @@ async def test_mqtt_payload_out_of_range_error( topic = "test/number" await async_setup_component( hass, - "number", + mqtt.DOMAIN, { - "number": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Number", - "min": 5, - "max": 110, + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + } } }, ) @@ -856,7 +882,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = NUMBER_DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -875,7 +901,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = number.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -884,7 +910,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = number.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -910,7 +936,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, "number", - DEFAULT_CONFIG["number"], + DEFAULT_CONFIG_LEGACY["number"], topic, value, attribute, @@ -921,7 +947,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = number.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -931,7 +957,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = number.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = number.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index d676429bf3e..a1c1644cc37 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import scene +from homeassistant.components import mqtt, scene from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNKNOWN, Platform import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -28,14 +28,20 @@ from .test_common import ( from tests.common import mock_restore_cache DEFAULT_CONFIG = { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "payload_on": "test-payload-on", + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "test-topic", + "payload_on": "test-payload-on", + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[scene.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def scene_platform_only(): @@ -51,14 +57,15 @@ async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): assert await async_setup_component( hass, - scene.DOMAIN, + mqtt.DOMAIN, { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - }, + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + }, + } }, ) await hass.async_block_till_done() @@ -80,14 +87,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -168,8 +175,8 @@ async def test_discovery_removal_scene(hass, mqtt_mock_entry_no_yaml_config, cap async def test_discovery_update_payload(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered scene.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["payload_on"] = "ON" @@ -216,7 +223,7 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -225,14 +232,14 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = scene.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -242,7 +249,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = scene.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 085c4d5df00..c66638a18af 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import select +from homeassistant.components import mqtt, select from homeassistant.components.mqtt.select import MQTT_SELECT_ATTRIBUTES_BLOCKED from homeassistant.components.select import ( ATTR_OPTION, @@ -56,14 +56,20 @@ from .test_common import ( from tests.common import async_fire_mqtt_message, mock_restore_cache DEFAULT_CONFIG = { - select.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "name": "test", + "command_topic": "test-topic", + "options": ["milk", "beer"], + } } } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[select.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def select_platform_only(): @@ -77,14 +83,15 @@ async def test_run_select_setup(hass, mqtt_mock_entry_with_yaml_config): topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -111,15 +118,16 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "value_template": "{{ value_json.val }}", + } } }, ) @@ -157,13 +165,14 @@ async def test_run_select_service_optimistic(hass, mqtt_mock_entry_with_yaml_con assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -198,14 +207,15 @@ async def test_run_select_service_optimistic_with_command_template( assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}"}', + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}"}', + } } }, ) @@ -238,14 +248,15 @@ async def test_run_select_service(hass, mqtt_mock_entry_with_yaml_config): assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -276,15 +287,16 @@ async def test_run_select_service_with_command_template( assert await async_setup_component( hass, - select.DOMAIN, + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "command_topic": cmd_topic, - "state_topic": state_topic, - "name": "Test Select", - "options": ["milk", "beer"], - "command_template": '{"option": "{{ value }}"}', + mqtt.DOMAIN: { + select.DOMAIN: { + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Select", + "options": ["milk", "beer"], + "command_template": '{"option": "{{ value }}"}', + } } }, ) @@ -311,28 +323,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -341,7 +353,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -353,7 +365,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_SELECT_ATTRIBUTES_BLOCKED, ) @@ -361,7 +373,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -370,7 +382,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, select.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + select.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -379,14 +395,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, select.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + select.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, select.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + select.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -419,7 +443,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered select.""" - data = json.dumps(DEFAULT_CONFIG[select.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_LEGACY[select.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data ) @@ -477,42 +501,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -522,7 +546,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, select.SERVICE_SELECT_OPTION, service_parameters={ATTR_OPTION: "beer"}, command_payload="beer", @@ -536,14 +560,15 @@ async def test_options_attributes(hass, mqtt_mock_entry_with_yaml_config, option topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test select", - "options": options, + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test select", + "options": options, + } } }, ) @@ -561,14 +586,15 @@ async def test_mqtt_payload_not_an_option_warning( topic = "test/select" await async_setup_component( hass, - "select", + mqtt.DOMAIN, { - "select": { - "platform": "mqtt", - "state_topic": topic, - "command_topic": topic, - "name": "Test Select", - "options": ["milk", "beer"], + mqtt.DOMAIN: { + select.DOMAIN: { + "state_topic": topic, + "command_topic": topic, + "name": "Test Select", + "options": ["milk", "beer"], + } } }, ) @@ -609,7 +635,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] config["options"] = ["milk", "beer"] await help_test_publishing_with_custom_encoding( @@ -629,7 +655,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -638,7 +664,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -659,7 +685,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG["select"]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY["select"]) config["options"] = ["milk", "beer"] await help_test_encoding_subscribable_topics( hass, @@ -677,7 +703,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = select.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -687,7 +713,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = select.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = select.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index b446b1a8b76..c5cf377cf1a 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -6,8 +6,8 @@ from unittest.mock import MagicMock, patch import pytest +from homeassistant.components import mqtt, sensor from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED -import homeassistant.components.sensor as sensor from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNAVAILABLE, @@ -64,16 +64,20 @@ from .test_common import ( ) from tests.common import ( - assert_setup_component, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache_with_extra_data, ) DEFAULT_CONFIG = { - sensor.DOMAIN: {"platform": "mqtt", "name": "test", "state_topic": "test-topic"} + mqtt.DOMAIN: {sensor.DOMAIN: {"name": "test", "state_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[sensor.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def sensor_platform_only(): @@ -88,13 +92,14 @@ async def test_setting_sensor_value_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + } } }, ) @@ -146,13 +151,14 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "device_class": device_class, + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "device_class": device_class, + } } }, ) @@ -173,15 +179,16 @@ async def test_setting_sensor_value_expires_availability_topic( """Test the expiration of the value.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "expire_after": 4, - "force_update": True, - "availability_topic": "availability-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + "availability_topic": "availability-topic", + } } }, ) @@ -206,15 +213,16 @@ async def test_setting_sensor_value_expires( """Test the expiration of the value.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "expire_after": "4", - "force_update": True, + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "expire_after": "4", + "force_update": True, + } } }, ) @@ -285,14 +293,15 @@ async def test_setting_sensor_value_via_mqtt_json_message( """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -311,14 +320,15 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st """Test the setting of the value via MQTT with fall back to current state.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": "{{ value_json.val | is_defined }}-{{ value_json.par }}", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": "{{ value_json.val | is_defined }}-{{ value_json.par }}", + } } }, ) @@ -344,15 +354,16 @@ async def test_setting_sensor_last_reset_via_mqtt_message( """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } } }, ) @@ -376,15 +387,16 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } } }, ) @@ -403,15 +415,16 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } } }, ) @@ -430,16 +443,17 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message( """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "last_reset_topic": "last-reset-topic", - "last_reset_value_template": "{{ value_json.last_reset }}", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + "last_reset_value_template": "{{ value_json.last_reset }}", + } } }, ) @@ -460,19 +474,20 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - **{ - "platform": "mqtt", - "name": "test", - "state_class": "total", - "state_topic": "test-topic", - "unit_of_measurement": "kWh", - "value_template": "{{ value_json.value | float / 60000 }}", - "last_reset_value_template": "{{ utcnow().fromtimestamp(value_json.time / 1000, tz=utcnow().tzinfo) }}", - }, - **extra, + mqtt.DOMAIN: { + sensor.DOMAIN: { + **{ + "name": "test", + "state_class": "total", + "state_topic": "test-topic", + "unit_of_measurement": "kWh", + "value_template": "{{ value_json.value | float / 60000 }}", + "last_reset_value_template": "{{ utcnow().fromtimestamp(value_json.time / 1000, tz=utcnow().tzinfo) }}", + }, + **extra, + } } }, ) @@ -498,13 +513,14 @@ async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + } } }, ) @@ -532,14 +548,15 @@ async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "force_update": True, + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "force_update": True, + } } }, ) @@ -568,21 +585,21 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -591,7 +608,7 @@ async def test_default_availability_list_payload( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -600,7 +617,7 @@ async def test_default_availability_list_payload_all( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_all( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -609,7 +626,7 @@ async def test_default_availability_list_payload_any( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_any( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -618,21 +635,25 @@ async def test_default_availability_list_single( ): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( - hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_discovery_update_availability(hass, mqtt_mock_entry_no_yaml_config): """Test availability discovery update.""" await help_test_discovery_update_availability( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -642,11 +663,12 @@ async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): hass, sensor.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "device_class": "foobarnotreal", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "device_class": "foobarnotreal", + } } }, ) @@ -661,17 +683,18 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, - "sensor", + mqtt.DOMAIN, { - "sensor": [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "device_class": "temperature", - }, - {"platform": "mqtt", "name": "Test 2", "state_topic": "test-topic"}, - ] + mqtt.DOMAIN: { + sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "device_class": "temperature", + }, + {"name": "Test 2", "state_topic": "test-topic"}, + ] + } }, ) await hass.async_block_till_done() @@ -689,11 +712,12 @@ async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config): hass, sensor.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "state_class": "foobarnotreal", + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "state_class": "foobarnotreal", + } } }, ) @@ -708,17 +732,18 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): """Test state_class option with valid values.""" assert await async_setup_component( hass, - "sensor", + mqtt.DOMAIN, { - "sensor": [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "state_class": "measurement", - }, - {"platform": "mqtt", "name": "Test 2", "state_topic": "test-topic"}, - ] + mqtt.DOMAIN: { + sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "state_class": "measurement", + }, + {"name": "Test 2", "state_topic": "test-topic"}, + ] + } }, ) await hass.async_block_till_done() @@ -735,7 +760,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -747,7 +772,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, MQTT_SENSOR_ATTRIBUTES_BLOCKED, ) @@ -755,7 +780,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -764,7 +789,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -773,14 +802,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + sensor.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -914,42 +951,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -967,7 +1004,6 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config) data = json.dumps( { - "platform": "mqtt", "name": "Test 1", "state_topic": "test-topic", "device": {"identifiers": ["helloworld"], "via_device": "hub-id"}, @@ -985,42 +1021,42 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config) async def test_entity_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_max_messages(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_max_messages( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY, None ) async def test_entity_debug_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_remove( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_debug_info_update_entity_id(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_update_entity_id( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): """Test entity disabled by default.""" await help_test_entity_disabled_by_default( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1028,7 +1064,7 @@ async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): async def test_entity_category(hass, mqtt_mock_entry_no_yaml_config): """Test entity category.""" await help_test_entity_category( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -1036,19 +1072,20 @@ async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_con """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( hass, - sensor.DOMAIN, + mqtt.DOMAIN, { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": '\ + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": '\ {% if state_attr(entity_id, "friendly_name") == "test" %} \ {{ value | int + 1 }} \ {% else %} \ {{ value }} \ {% endif %}', + } } }, ) @@ -1064,7 +1101,7 @@ async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_con async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -1073,7 +1110,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -1082,14 +1119,14 @@ async def test_cleanup_triggers_and_restoring_state( ): """Test cleanup old triggers at reloading and restoring the state.""" domain = sensor.DOMAIN - config1 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config1["name"] = "test1" config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" config1["device_class"] = "temperature" config1["unit_of_measurement"] = TEMP_FAHRENHEIT - config2 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" @@ -1100,8 +1137,8 @@ async def test_cleanup_triggers_and_restoring_state( assert await async_setup_component( hass, - domain, - {domain: [config1, config2]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {domain: [config1, config2]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1116,7 +1153,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, {domain: [config1, config2]} + hass, caplog, tmp_path, {mqtt.DOMAIN: {domain: [config1, config2]}} ) await hass.async_block_till_done() @@ -1150,7 +1187,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( freezer.move_to("2022-02-02 12:02:00+01:00") domain = sensor.DOMAIN - config3 = copy.deepcopy(DEFAULT_CONFIG[domain]) + config3 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config3["name"] = "test3" config3["expire_after"] = 10 config3["state_topic"] = "test-topic3" @@ -1163,10 +1200,11 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( fake_extra_data = MagicMock() mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) - with assert_setup_component(1, domain): - assert await async_setup_component(hass, domain, {domain: config3}) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {domain: config3}} + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for sensor.test3" in caplog.text @@ -1192,7 +1230,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG[sensor.DOMAIN], + DEFAULT_CONFIG_LEGACY[sensor.DOMAIN], topic, value, attribute, @@ -1204,7 +1242,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -1214,7 +1252,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = sensor.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 13648f1c486..fd91847f767 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import siren +from homeassistant.components import mqtt, siren from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -53,9 +53,14 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - siren.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {siren.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[siren.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def siren_platform_only(): @@ -83,15 +88,16 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } }, ) @@ -119,15 +125,16 @@ async def test_sending_mqtt_commands_and_optimistic( """Test the sending MQTT commands in optimistic mode.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "qos": "2", + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "qos": "2", + } } }, ) @@ -162,16 +169,17 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "state_value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "state_value_template": "{{ value_json.val }}", + } } }, ) @@ -202,16 +210,17 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa """Test the controlling state via topic and JSON message without a value template.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "available_tones": ["ping", "siren", "bell"], + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "available_tones": ["ping", "siren", "bell"], + } } }, ) @@ -284,7 +293,6 @@ async def test_filtering_not_supported_attributes_optimistic( ): """Test setting attributes with support flags optimistic.""" config = { - "platform": "mqtt", "command_topic": "command-topic", "available_tones": ["ping", "siren", "bell"], } @@ -300,8 +308,8 @@ async def test_filtering_not_supported_attributes_optimistic( assert await async_setup_component( hass, - siren.DOMAIN, - {siren.DOMAIN: [config1, config2, config3]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {siren.DOMAIN: [config1, config2, config3]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -370,7 +378,6 @@ async def test_filtering_not_supported_attributes_via_state( ): """Test setting attributes with support flags via state.""" config = { - "platform": "mqtt", "command_topic": "command-topic", "available_tones": ["ping", "siren", "bell"], } @@ -389,8 +396,8 @@ async def test_filtering_not_supported_attributes_via_state( assert await async_setup_component( hass, - siren.DOMAIN, - {siren.DOMAIN: [config1, config2, config3]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {siren.DOMAIN: [config1, config2, config3]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -450,14 +457,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -513,17 +520,18 @@ async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, - siren.DOMAIN, + mqtt.DOMAIN, { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, - "state_on": "HIGH", - "state_off": "LOW", + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "state_on": "HIGH", + "state_off": "LOW", + } } }, ) @@ -550,7 +558,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -559,14 +567,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY, {} ) 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, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -575,7 +583,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, siren.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + siren.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -584,14 +596,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, siren.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + siren.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, siren.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + siren.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -636,8 +656,8 @@ async def test_discovery_update_siren_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -673,8 +693,8 @@ async def test_discovery_update_siren_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -706,7 +726,7 @@ async def test_discovery_update_siren_template( async def test_command_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test siren with command templates optimistic.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) config1["name"] = "Beer" config1["available_tones"] = ["ping", "chimes"] config1[ @@ -719,8 +739,8 @@ async def test_command_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) assert await async_setup_component( hass, - siren.DOMAIN, - {siren.DOMAIN: [config1, config2]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {siren.DOMAIN: [config1, config2]}}, ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -824,42 +844,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -869,7 +889,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, siren.SERVICE_TURN_ON, command_payload='{"state":"ON"}', ) @@ -906,7 +926,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with command templates and different encoding.""" domain = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[domain]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config[siren.ATTR_AVAILABLE_TONES] = ["siren", "xylophone"] await help_test_publishing_with_custom_encoding( @@ -926,7 +946,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -935,7 +955,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -960,7 +980,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG[siren.DOMAIN], + DEFAULT_CONFIG_LEGACY[siren.DOMAIN], topic, value, attribute, @@ -971,7 +991,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -981,7 +1001,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = siren.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index b0b89c28646..4c2fbf3a596 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import vacuum +from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED @@ -73,19 +73,27 @@ SEND_COMMAND_TOPIC = "vacuum/send_command" STATE_TOPIC = "vacuum/state" DEFAULT_CONFIG = { - CONF_PLATFORM: "mqtt", - CONF_SCHEMA: "state", - CONF_NAME: "mqtttest", - CONF_COMMAND_TOPIC: COMMAND_TOPIC, - mqttvacuum.CONF_SEND_COMMAND_TOPIC: SEND_COMMAND_TOPIC, - CONF_STATE_TOPIC: STATE_TOPIC, - mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", - mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + mqtt.DOMAIN: { + vacuum.DOMAIN: { + CONF_SCHEMA: "state", + CONF_NAME: "mqtttest", + CONF_COMMAND_TOPIC: COMMAND_TOPIC, + mqttvacuum.CONF_SEND_COMMAND_TOPIC: SEND_COMMAND_TOPIC, + CONF_STATE_TOPIC: STATE_TOPIC, + mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: "vacuum/set_fan_speed", + mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], + } + } } -DEFAULT_CONFIG_2 = { - vacuum.DOMAIN: {"platform": "mqtt", "schema": "state", "name": "test"} -} +DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"schema": "state", "name": "test"}}} + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN +DEFAULT_CONFIG_2_LEGACY = deepcopy(DEFAULT_CONFIG_2[mqtt.DOMAIN]) +DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -97,9 +105,7 @@ def vacuum_platform_only(): async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" - assert await async_setup_component( - hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} - ) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") @@ -111,12 +117,14 @@ async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands send to the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -185,13 +193,15 @@ async def test_commands_without_supported_features( hass, mqtt_mock_entry_with_yaml_config ): """Test commands which are not supported by the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) services = mqttvacuum.STRING_TO_SERVICE["status"] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( services, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -243,12 +253,14 @@ async def test_commands_without_supported_features( async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") @@ -288,13 +300,15 @@ async def test_status(hass, mqtt_mock_entry_with_yaml_config): async def test_no_fan_vacuum(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum when fan is not supported.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) del config[mqttvacuum.CONF_FAN_SPEED_LIST] config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.DEFAULT_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -340,12 +354,14 @@ async def test_no_fan_vacuum(hass, mqtt_mock_entry_with_yaml_config): @pytest.mark.no_fail_on_log_exception async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN]) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + assert await async_setup_component( + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {vacuum.DOMAIN: config}} + ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -359,28 +375,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -389,7 +405,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -401,7 +417,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2, + DEFAULT_CONFIG_2_LEGACY, MQTT_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -409,7 +425,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -418,7 +434,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -427,14 +447,22 @@ async def test_update_with_json_attrs_bad_json( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + DEFAULT_CONFIG_2_LEGACY, ) @@ -511,42 +539,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY ) @@ -556,7 +584,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2, + DEFAULT_CONFIG_2_LEGACY, vacuum.SERVICE_START, command_payload="start", state_payload="{}", @@ -615,7 +643,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) config["supported_features"] = [ "battery", "clean_spot", @@ -646,7 +674,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -655,7 +683,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -691,7 +719,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN], topic, value, attribute, @@ -703,8 +731,21 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG) + config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index ac69b17e18e..87d919f41c5 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import switch +from homeassistant.components import mqtt, switch from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_DEVICE_CLASS, @@ -51,9 +51,14 @@ from tests.common import async_fire_mqtt_message, mock_restore_cache from tests.components.switch import common DEFAULT_CONFIG = { - switch.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + mqtt.DOMAIN: {switch.DOMAIN: {"name": "test", "command_topic": "test-topic"}} } +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) +DEFAULT_CONFIG_LEGACY[switch.DOMAIN]["platform"] = mqtt.DOMAIN + @pytest.fixture(autouse=True) def switch_platform_only(): @@ -66,16 +71,17 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi """Test the controlling state via topic.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, - "device_class": "switch", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "device_class": "switch", + } } }, ) @@ -112,15 +118,16 @@ async def test_sending_mqtt_commands_and_optimistic( assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "qos": "2", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "qos": "2", + } } }, ) @@ -155,12 +162,13 @@ async def test_sending_inital_state_and_optimistic( """Test the initial state in optimistic mode.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + } } }, ) @@ -178,16 +186,17 @@ async def test_controlling_state_via_topic_and_json_message( """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": "beer on", - "payload_off": "beer off", - "value_template": "{{ value_json.val }}", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": "beer on", + "payload_off": "beer off", + "value_template": "{{ value_json.val }}", + } } }, ) @@ -218,14 +227,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -281,17 +290,18 @@ async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, - switch.DOMAIN, + mqtt.DOMAIN, { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, - "state_on": "HIGH", - "state_off": "LOW", + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "state_on": "HIGH", + "state_off": "LOW", + } } }, ) @@ -318,7 +328,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -327,14 +337,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY, {} ) 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, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -343,7 +353,11 @@ async def test_update_with_json_attrs_not_dict( ): """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, switch.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + switch.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -352,14 +366,22 @@ async def test_update_with_json_attrs_bad_JSON( ): """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, switch.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + switch.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) 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, switch.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + switch.DOMAIN, + DEFAULT_CONFIG_LEGACY, ) @@ -404,8 +426,8 @@ async def test_discovery_update_switch_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -441,8 +463,8 @@ async def test_discovery_update_switch_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -512,42 +534,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) 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, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY ) @@ -557,7 +579,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, - DEFAULT_CONFIG, + DEFAULT_CONFIG_LEGACY, switch.SERVICE_TURN_ON, ) @@ -593,7 +615,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_publishing_with_custom_encoding( hass, @@ -612,7 +634,7 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) @@ -621,7 +643,7 @@ async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_pa async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) @@ -646,7 +668,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG[switch.DOMAIN], + DEFAULT_CONFIG_LEGACY[switch.DOMAIN], topic, value, attribute, @@ -657,7 +679,7 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = switch.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) config["name"] = "test" del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) @@ -667,7 +689,20 @@ async def test_setup_manual_entity_from_yaml(hass): async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG[domain] + config = DEFAULT_CONFIG_LEGACY[domain] await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 +async def test_setup_with_legacy_schema(hass, mqtt_mock_entry_with_yaml_config): + """Test a setup with deprecated yaml platform schema.""" + domain = switch.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config["name"] = "test" + assert await async_setup_component(hass, domain, {domain: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + assert hass.states.get(f"{domain}.test") is not None From 4894e2e5a43e80a2f64d8f9486c7eb215fcdaa6b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 9 Sep 2022 11:15:48 +0200 Subject: [PATCH 199/231] Refactor common MQTT tests to use modern schema (#77583) * Common tests availability * Common tests attributes * Common tests unique id * Common tests discovery * Common tests encoding * Common tests device info * Common tests entity_id updated * Common tests entity debug info * Common test entity category * Common tests setup reload unload+corrections * Cleanup sweep * Comments from curent change * Cleanup * Remove unused legacy config --- .../mqtt/test_alarm_control_panel.py | 103 +++++---- tests/components/mqtt/test_binary_sensor.py | 103 +++++---- tests/components/mqtt/test_button.py | 103 ++++----- tests/components/mqtt/test_camera.py | 83 ++++--- tests/components/mqtt/test_climate.py | 113 +++++----- tests/components/mqtt/test_common.py | 202 +++++++++--------- tests/components/mqtt/test_cover.py | 85 ++++---- .../mqtt/test_device_tracker_discovery.py | 14 +- tests/components/mqtt/test_fan.py | 89 ++++---- tests/components/mqtt/test_humidifier.py | 93 ++++---- tests/components/mqtt/test_init.py | 38 +++- tests/components/mqtt/test_legacy_vacuum.py | 117 +++++----- tests/components/mqtt/test_light.py | 95 ++++---- tests/components/mqtt/test_light_json.py | 99 ++++----- tests/components/mqtt/test_light_template.py | 116 +++++----- tests/components/mqtt/test_lock.py | 87 ++++---- tests/components/mqtt/test_number.py | 93 ++++---- tests/components/mqtt/test_scene.py | 71 +++--- tests/components/mqtt/test_select.py | 99 +++++---- tests/components/mqtt/test_sensor.py | 110 +++++----- tests/components/mqtt/test_siren.py | 125 +++++------ tests/components/mqtt/test_state_vacuum.py | 87 ++++---- tests/components/mqtt/test_switch.py | 123 +++++------ 23 files changed, 1125 insertions(+), 1123 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index e51ed9aeae9..d305d2ae7aa 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -61,7 +61,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -121,8 +121,6 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { # Scheduled to be removed in HA core 2022.12 DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN -DEFAULT_CONFIG_CODE_LEGACY = copy.deepcopy(DEFAULT_CONFIG_CODE[mqtt.DOMAIN]) -DEFAULT_CONFIG_CODE_LEGACY[alarm_control_panel.DOMAIN]["platform"] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -588,7 +586,7 @@ async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG_CODE, ) @@ -598,7 +596,7 @@ async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG_CODE, ) @@ -608,7 +606,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG_CODE, ) @@ -618,7 +616,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_CODE_LEGACY, + DEFAULT_CONFIG, ) @@ -630,7 +628,7 @@ async def test_setting_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -642,7 +640,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -653,7 +651,7 @@ async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_c hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -666,20 +664,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -690,29 +688,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one alarm per unique_id.""" config = { - alarm_control_panel.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, config @@ -721,7 +719,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_alarm(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered alarm_control_panel.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, data ) @@ -731,8 +729,8 @@ async def test_discovery_update_alarm_topic_and_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -766,8 +764,8 @@ async def test_discovery_update_alarm_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "alarm/state1" @@ -799,7 +797,7 @@ async def test_discovery_update_unchanged_alarm( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered alarm_control_panel.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -851,7 +849,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY[alarm_control_panel.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][alarm_control_panel.DOMAIN], topic, value, ) @@ -863,7 +861,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -873,7 +871,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -883,7 +881,7 @@ async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -893,7 +891,7 @@ async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -903,7 +901,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -913,7 +911,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_c hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -923,7 +921,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, alarm_control_panel.SERVICE_ALARM_DISARM, command_payload="DISARM", ) @@ -966,7 +964,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -987,12 +985,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = alarm_control_panel.DOMAIN @@ -1003,17 +1003,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = alarm_control_panel.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = alarm_control_panel.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index bbe8b978707..48acde5c6c9 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -45,7 +45,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -542,7 +542,7 @@ async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -552,7 +552,7 @@ async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -562,7 +562,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -572,7 +572,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -708,7 +708,7 @@ async def test_setting_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -718,7 +718,7 @@ async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_c hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -731,20 +731,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -755,27 +755,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { - binary_sensor.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + binary_sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, config @@ -786,7 +786,7 @@ async def test_discovery_removal_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test removal of discovered binary_sensor.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, data ) @@ -796,8 +796,8 @@ async def test_discovery_update_binary_sensor_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -833,8 +833,8 @@ async def test_discovery_update_binary_sensor_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "sensor/state1" @@ -892,7 +892,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN], topic, value, attribute, @@ -904,7 +904,7 @@ async def test_discovery_update_unchanged_binary_sensor( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered binary_sensor.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[binary_sensor.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][binary_sensor.DOMAIN]) config1["name"] = "Beer" data1 = json.dumps(config1) @@ -942,7 +942,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -952,7 +952,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -962,7 +962,7 @@ async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -972,7 +972,7 @@ async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -982,7 +982,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -992,7 +992,7 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_c hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1002,7 +1002,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, None, ) @@ -1010,12 +1010,14 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = binary_sensor.DOMAIN @@ -1040,11 +1042,11 @@ async def test_cleanup_triggers_and_restoring_state( ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config1["name"] = "test1" config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" @@ -1053,8 +1055,8 @@ async def test_cleanup_triggers_and_restoring_state( assert await async_setup_component( hass, - binary_sensor.DOMAIN, - {binary_sensor.DOMAIN: [config1, config2]}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {binary_sensor.DOMAIN: [config1, config2]}}, ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1070,7 +1072,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, {domain: [config1, config2]} + hass, caplog, tmp_path, {mqtt.DOMAIN: {domain: [config1, config2]}} ) assert "Clean up expire after trigger for binary_sensor.test1" in caplog.text assert "Clean up expire after trigger for binary_sensor.test2" not in caplog.text @@ -1125,17 +1127,14 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = binary_sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = binary_sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 1274c700800..80e5ce60a47 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -38,7 +38,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -140,25 +140,26 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.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, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - button.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_press": 1, + mqtt.DOMAIN: { + button.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_press": 1, + } } } @@ -176,11 +177,12 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - button.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_press": 1, + mqtt.DOMAIN: { + button.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_press": 1, + } } } @@ -200,7 +202,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) @@ -209,14 +211,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY, None + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, None ) 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, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) @@ -229,20 +231,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -253,27 +255,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one button per unique_id.""" config = { - button.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + button.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, config @@ -290,8 +292,8 @@ async def test_discovery_removal_button(hass, mqtt_mock_entry_no_yaml_config, ca async def test_discovery_update_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered button.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[button.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][button.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][button.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" @@ -340,35 +342,35 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.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, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.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, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.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, button.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) @@ -378,7 +380,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, button.SERVICE_PRESS, command_payload="PRESS", state_topic=None, @@ -457,7 +459,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = button.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -476,12 +478,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = button.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = button.DOMAIN @@ -492,17 +496,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = button.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = button.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 4d0b2fbfca0..4cb6afb6495 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -37,7 +37,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -195,28 +195,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -225,7 +225,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -237,7 +237,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_CAMERA_ATTRIBUTES_BLOCKED, ) @@ -245,7 +245,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -258,20 +258,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -282,27 +282,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one camera per unique_id.""" config = { - camera.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + camera.DOMAIN: [ + { + "name": "Test 1", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, config @@ -311,7 +311,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered camera.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[camera.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][camera.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data ) @@ -359,28 +359,28 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -390,7 +390,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ["test_topic"], ) @@ -398,7 +398,7 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co 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, camera.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) @@ -408,7 +408,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, None, state_topic="test_topic", state_payload=b"ON", @@ -418,12 +418,14 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = camera.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = camera.DOMAIN @@ -434,17 +436,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = camera.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = camera.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 14bd9084abe..ec2501e11d3 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -52,7 +52,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -686,28 +686,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) @@ -994,7 +994,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) @@ -1006,7 +1006,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_CLIMATE_ATTRIBUTES_BLOCKED, ) @@ -1014,7 +1014,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) @@ -1027,20 +1027,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1051,29 +1051,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one climate per unique_id.""" config = { - climate.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + climate.DOMAIN: [ + { + "name": "Test 1", + "power_state_topic": "test-topic", + "power_command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "power_state_topic": "test-topic", + "power_command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, climate.DOMAIN, config @@ -1106,7 +1106,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, @@ -1122,7 +1122,7 @@ async def test_encoding_subscribable_topics( async def test_discovery_removal_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered climate.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[climate.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][climate.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, climate.DOMAIN, data ) @@ -1168,39 +1168,40 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - climate.DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_state_topic": "test-topic", - "availability_topic": "avty-topic", + mqtt.DOMAIN: { + climate.DOMAIN: { + "name": "test", + "mode_state_topic": "test-topic", + "availability_topic": "avty-topic", + } } } await help_test_entity_id_update_subscriptions( @@ -1215,18 +1216,19 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co 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, climate.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, climate.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - climate.DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_command_topic": "command-topic", - "mode_state_topic": "test-topic", + mqtt.DOMAIN: { + climate.DOMAIN: { + "name": "test", + "mode_command_topic": "command-topic", + "mode_state_topic": "test-topic", + } } } await help_test_entity_debug_info_message( @@ -1375,10 +1377,10 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = climate.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic != "preset_mode_command_topic": - del config["preset_mode_command_topic"] - del config["preset_modes"] + del config[mqtt.DOMAIN][domain]["preset_mode_command_topic"] + del config[mqtt.DOMAIN][domain]["preset_modes"] await help_test_publishing_with_custom_encoding( hass, @@ -1397,12 +1399,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = climate.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = climate.DOMAIN @@ -1413,17 +1417,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = climate.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = climate.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index ac8ef98531e..e2411f9fc6c 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -53,7 +53,7 @@ async def help_test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config, domain, config ): """Test availability after MQTT disconnection.""" - assert await async_setup_component(hass, domain, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -72,8 +72,8 @@ async def help_test_availability_without_topic( hass, mqtt_mock_entry_with_yaml_config, domain, config ): """Test availability without defined availability topic.""" - assert "availability_topic" not in config[domain] - assert await async_setup_component(hass, domain, config) + assert "availability_topic" not in config[mqtt.DOMAIN][domain] + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -96,10 +96,10 @@ async def help_test_default_availability_payload( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_topic"] = "availability-topic" + config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -147,13 +147,13 @@ async def help_test_default_availability_list_payload( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, {"topic": "availability-topic2"}, ] assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -213,14 +213,14 @@ async def help_test_default_availability_list_payload_all( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_mode"] = "all" - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability_mode"] = "all" + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, {"topic": "availability-topic2"}, ] assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -281,14 +281,14 @@ async def help_test_default_availability_list_payload_any( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_mode"] = "any" - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability_mode"] = "any" + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, {"topic": "availability-topic2"}, ] assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -331,7 +331,6 @@ async def help_test_default_availability_list_payload_any( async def help_test_default_availability_list_single( hass, - mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -345,22 +344,17 @@ async def help_test_default_availability_list_single( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability"] = [ + config[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic1"}, ] - config[domain]["availability_topic"] = "availability-topic" - assert await async_setup_component( + config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" + assert not await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(f"{domain}.test") - assert state is None assert ( - "Invalid config for [sensor.mqtt]: two or more values in the same group of exclusion 'availability'" + "Invalid config for [mqtt]: two or more values in the same group of exclusion 'availability'" in caplog.text ) @@ -380,12 +374,12 @@ async def help_test_custom_availability_payload( """ # Add availability settings to config config = copy.deepcopy(config) - config[domain]["availability_topic"] = "availability-topic" - config[domain]["payload_available"] = "good" - config[domain]["payload_not_available"] = "nogood" + config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" + config[mqtt.DOMAIN][domain]["payload_available"] = "good" + config[mqtt.DOMAIN][domain]["payload_not_available"] = "nogood" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -434,17 +428,17 @@ async def help_test_discovery_update_availability( await mqtt_mock_entry_no_yaml_config() # Add availability settings to config config1 = copy.deepcopy(config) - config1[domain]["availability_topic"] = "availability-topic1" + config1[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic1" config2 = copy.deepcopy(config) - config2[domain]["availability"] = [ + config2[mqtt.DOMAIN][domain]["availability"] = [ {"topic": "availability-topic2"}, {"topic": "availability-topic3"}, ] config3 = copy.deepcopy(config) - config3[domain]["availability_topic"] = "availability-topic4" - data1 = json.dumps(config1[domain]) - data2 = json.dumps(config2[domain]) - data3 = json.dumps(config3[domain]) + config3[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic4" + data1 = json.dumps(config1[mqtt.DOMAIN][domain]) + data2 = json.dumps(config2[mqtt.DOMAIN][domain]) + data3 = json.dumps(config3[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -508,10 +502,10 @@ async def help_test_setting_attribute_via_mqtt_json_message( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -535,8 +529,8 @@ async def help_test_setting_blocked_attribute_via_mqtt_json_message( # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" - data = json.dumps(config[domain]) + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" + data = json.dumps(config[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() val = "abc123" @@ -561,11 +555,13 @@ async def help_test_setting_attribute_with_template( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" - config[domain]["json_attributes_template"] = "{{ value_json['Timer1'] | tojson }}" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain][ + "json_attributes_template" + ] = "{{ value_json['Timer1'] | tojson }}" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -589,10 +585,10 @@ async def help_test_update_with_json_attrs_not_dict( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -605,7 +601,7 @@ async def help_test_update_with_json_attrs_not_dict( assert "JSON result was not a dictionary" in caplog.text -async def help_test_update_with_json_attrs_bad_JSON( +async def help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test JSON validation of attributes. @@ -614,10 +610,10 @@ async def help_test_update_with_json_attrs_bad_JSON( """ # Add JSON attributes settings to config config = copy.deepcopy(config) - config[domain]["json_attributes_topic"] = "attr-topic" + config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -640,11 +636,11 @@ async def help_test_discovery_update_attr( await mqtt_mock_entry_no_yaml_config() # Add JSON attributes settings to config config1 = copy.deepcopy(config) - config1[domain]["json_attributes_topic"] = "attr-topic1" + config1[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic1" config2 = copy.deepcopy(config) - config2[domain]["json_attributes_topic"] = "attr-topic2" - data1 = json.dumps(config1[domain]) - data2 = json.dumps(config2[domain]) + config2[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic2" + data1 = json.dumps(config1[mqtt.DOMAIN][domain]) + data2 = json.dumps(config2[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -669,7 +665,7 @@ async def help_test_discovery_update_attr( async def help_test_unique_id(hass, mqtt_mock_entry_with_yaml_config, domain, config): """Test unique id option only creates one entity per unique_id.""" - assert await async_setup_component(hass, domain, config) + assert await async_setup_component(hass, mqtt.DOMAIN, config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() assert len(hass.states.async_entity_ids(domain)) == 1 @@ -881,7 +877,7 @@ async def help_test_encoding_subscribable_topics( await hass.async_block_till_done() assert await async_setup_component( - hass, domain, {domain: [config1, config2, config3]} + hass, mqtt.DOMAIN, {mqtt.DOMAIN: {domain: [config1, config2, config3]}} ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -944,7 +940,7 @@ async def help_test_entity_device_info_with_identifier( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -975,7 +971,7 @@ async def help_test_entity_device_info_with_connection( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) config["unique_id"] = "veryunique" @@ -1005,7 +1001,7 @@ async def help_test_entity_device_info_remove( """Test device registry remove.""" await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1037,7 +1033,7 @@ async def help_test_entity_device_info_update( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1067,19 +1063,19 @@ async def help_test_entity_id_update_subscriptions( """Test MQTT subscriptions are managed when entity_id is updated.""" # Add unique_id to config config = copy.deepcopy(config) - config[domain]["unique_id"] = "TOTALLY_UNIQUE" + config[mqtt.DOMAIN][domain]["unique_id"] = "TOTALLY_UNIQUE" if topics is None: # Add default topics to config - config[domain]["availability_topic"] = "avty-topic" - config[domain]["state_topic"] = "test-topic" + config[mqtt.DOMAIN][domain]["availability_topic"] = "avty-topic" + config[mqtt.DOMAIN][domain]["state_topic"] = "test-topic" topics = ["avty-topic", "test-topic"] assert len(topics) > 0 registry = mock_registry(hass, {}) assert await async_setup_component( hass, - domain, + mqtt.DOMAIN, config, ) await hass.async_block_till_done() @@ -1111,16 +1107,16 @@ async def help_test_entity_id_update_discovery_update( # Add unique_id to config await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config) - config[domain]["unique_id"] = "TOTALLY_UNIQUE" + config[mqtt.DOMAIN][domain]["unique_id"] = "TOTALLY_UNIQUE" if topic is None: # Add default topic to config - config[domain]["availability_topic"] = "avty-topic" + config[mqtt.DOMAIN][domain]["availability_topic"] = "avty-topic" topic = "avty-topic" ent_registry = mock_registry(hass, {}) - data = json.dumps(config[domain]) + data = json.dumps(config[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() @@ -1135,8 +1131,8 @@ async def help_test_entity_id_update_discovery_update( ent_registry.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") await hass.async_block_till_done() - config[domain]["availability_topic"] = f"{topic}_2" - data = json.dumps(config[domain]) + config[mqtt.DOMAIN][domain]["availability_topic"] = f"{topic}_2" + data = json.dumps(config[mqtt.DOMAIN][domain]) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(domain)) == 1 @@ -1155,9 +1151,10 @@ async def help_test_entity_debug_info( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" + config["platform"] = "mqtt" registry = dr.async_get(hass) @@ -1192,7 +1189,7 @@ async def help_test_entity_debug_info_max_messages( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1256,7 +1253,7 @@ async def help_test_entity_debug_info_message( """ # Add device settings to config await mqtt_mock_entry_no_yaml_config() - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1359,9 +1356,10 @@ async def help_test_entity_debug_info_remove( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" + config["platform"] = "mqtt" registry = dr.async_get(hass) @@ -1405,9 +1403,10 @@ async def help_test_entity_debug_info_update_entity_id( """ await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" + config["platform"] = "mqtt" dev_registry = dr.async_get(hass) ent_registry = mock_registry(hass, {}) @@ -1461,7 +1460,7 @@ async def help_test_entity_disabled_by_default( """Test device registry remove.""" await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["enabled_by_default"] = False config["unique_id"] = "veryunique1" @@ -1500,7 +1499,7 @@ async def help_test_entity_category( """Test device registry remove.""" await mqtt_mock_entry_no_yaml_config() # Add device settings to config - config = copy.deepcopy(config[domain]) + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) ent_registry = er.async_get(hass) @@ -1564,7 +1563,7 @@ async def help_test_publishing_with_custom_encoding( setup_config = [] service_data = {} for test_id, test_data in test_config.items(): - test_config_setup = copy.deepcopy(config) + test_config_setup = copy.deepcopy(config[mqtt.DOMAIN][domain]) test_config_setup.update( { topic: f"cmd/{test_id}", @@ -1573,7 +1572,7 @@ async def help_test_publishing_with_custom_encoding( ) if test_data["encoding"] is not None: test_config_setup["encoding"] = test_data["encoding"] - if test_data["cmd_tpl"]: + if template and test_data["cmd_tpl"]: test_config_setup[ template ] = f"{{{{ (('%.1f'|format({tpl_par}))[0] if is_number({tpl_par}) else {tpl_par}[0]) | ord | pack('b') }}}}" @@ -1587,8 +1586,8 @@ async def help_test_publishing_with_custom_encoding( # setup test entities assert await async_setup_component( hass, - domain, - {domain: setup_config}, + mqtt.DOMAIN, + {mqtt.DOMAIN: {domain: setup_config}}, ) await hass.async_block_till_done() mqtt_mock = await mqtt_mock_entry_with_yaml_config() @@ -1698,24 +1697,29 @@ async def help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ): """Test reloading an MQTT platform.""" + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) # Create and test an old config of 2 entities based on the config supplied old_config_1 = copy.deepcopy(config) old_config_1["name"] = "test_old_1" old_config_2 = copy.deepcopy(config) old_config_2["name"] = "test_old_2" + + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 old_config_3 = copy.deepcopy(config) old_config_3["name"] = "test_old_3" - old_config_3.pop("platform") + old_config_3["platform"] = mqtt.DOMAIN old_config_4 = copy.deepcopy(config) old_config_4["name"] = "test_old_4" - old_config_4.pop("platform") + old_config_4["platform"] = mqtt.DOMAIN old_config = { - domain: [old_config_1, old_config_2], - "mqtt": {domain: [old_config_3, old_config_4]}, + mqtt.DOMAIN: {domain: [old_config_1, old_config_2]}, + domain: [old_config_3, old_config_4], } assert await async_setup_component(hass, domain, old_config) + assert await async_setup_component(hass, mqtt.DOMAIN, old_config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() @@ -1731,21 +1735,24 @@ async def help_test_reloadable( new_config_1["name"] = "test_new_1" new_config_2 = copy.deepcopy(config) new_config_2["name"] = "test_new_2" + new_config_extra = copy.deepcopy(config) + new_config_extra["name"] = "test_new_5" + + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" - new_config_3.pop("platform") + new_config_3["platform"] = mqtt.DOMAIN new_config_4 = copy.deepcopy(config) new_config_4["name"] = "test_new_4" - new_config_4.pop("platform") - new_config_5 = copy.deepcopy(config) - new_config_5["name"] = "test_new_5" - new_config_6 = copy.deepcopy(config) - new_config_6["name"] = "test_new_6" - new_config_6.pop("platform") + new_config_4["platform"] = mqtt.DOMAIN + new_config_extra_legacy = copy.deepcopy(config) + new_config_extra_legacy["name"] = "test_new_6" + new_config_extra_legacy["platform"] = mqtt.DOMAIN new_config = { - domain: [new_config_1, new_config_2, new_config_5], - "mqtt": {domain: [new_config_3, new_config_4, new_config_6]}, + mqtt.DOMAIN: {domain: [new_config_1, new_config_2, new_config_extra]}, + domain: [new_config_3, new_config_4, new_config_extra_legacy], } await help_test_reload_with_config(hass, caplog, tmp_path, new_config) @@ -1760,9 +1767,12 @@ async def help_test_reloadable( assert hass.states.get(f"{domain}.test_new_6") +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): - """Test reloading an MQTT platform when config entry is setup late.""" + """Test reloading an MQTT platform when config entry is setup is late.""" # Create and test an old config of 2 entities based on the config supplied + # using the deprecated platform schema old_config_1 = copy.deepcopy(config) old_config_1["name"] = "test_old_1" old_config_2 = copy.deepcopy(config) @@ -1815,7 +1825,7 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): assert hass.states.get(f"{domain}.test_new_3") -async def help_test_setup_manual_entity_from_yaml(hass, platform, config): +async def help_test_setup_manual_entity_from_yaml(hass, config): """Help to test setup from yaml through configuration entry.""" calls = MagicMock() @@ -1823,9 +1833,7 @@ async def help_test_setup_manual_entity_from_yaml(hass, platform, config): """Mock reload.""" calls() - config_structure = {mqtt.DOMAIN: {platform: config}} - - await async_setup_component(hass, mqtt.DOMAIN, config_structure) + assert await async_setup_component(hass, mqtt.DOMAIN, config) # Mock config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) @@ -1864,14 +1872,14 @@ async def help_test_unload_config_entry_with_platform( """Test unloading the MQTT config entry with a specific platform domain.""" # prepare setup through configuration.yaml config_setup = copy.deepcopy(config) - config_setup["name"] = "config_setup" + config_setup[mqtt.DOMAIN][domain]["name"] = "config_setup" config_name = config_setup - assert await async_setup_component(hass, domain, {domain: [config_setup]}) + assert await async_setup_component(hass, mqtt.DOMAIN, config_setup) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() # prepare setup through discovery - discovery_setup = copy.deepcopy(config) + discovery_setup = copy.deepcopy(config[mqtt.DOMAIN][domain]) discovery_setup["name"] = "discovery_setup" async_fire_mqtt_message( hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index a91f0ecc6ca..fb7df111d96 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -74,7 +74,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -2444,28 +2444,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2514,7 +2514,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2526,7 +2526,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_COVER_ATTRIBUTES_BLOCKED, ) @@ -2534,7 +2534,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2547,7 +2547,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2555,12 +2555,12 @@ 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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2571,27 +2571,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one cover per id.""" config = { - cover.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + cover.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, config @@ -2646,42 +2646,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, cover.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, cover.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) @@ -2691,7 +2691,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, SERVICE_OPEN_COVER, command_payload="OPEN", ) @@ -3342,8 +3342,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - config["position_topic"] = "some-position-topic" + config = DEFAULT_CONFIG + config[mqtt.DOMAIN][domain]["position_topic"] = "some-position-topic" await help_test_publishing_with_custom_encoding( hass, @@ -3362,12 +3362,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = cover.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = cover.DOMAIN @@ -3399,7 +3401,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, - DEFAULT_CONFIG_LEGACY[cover.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][cover.DOMAIN], topic, value, attribute, @@ -3411,17 +3413,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = cover.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = cover.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 923ae7c9f75..876b66e8a4d 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -1,6 +1,5 @@ """The tests for the MQTT device_tracker platform.""" -import copy from unittest.mock import patch import pytest @@ -27,11 +26,6 @@ DEFAULT_CONFIG = { } } -# Test deprecated YAML configuration under the platform key -# Scheduled to be removed in HA core 2022.12 -DEFAULT_CONFIG_LEGACY = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) -DEFAULT_CONFIG_LEGACY[device_tracker.DOMAIN]["platform"] = mqtt.DOMAIN - @pytest.fixture(autouse=True) def device_tracker_platform_only(): @@ -440,7 +434,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, device_tracker.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, None, ) @@ -451,8 +445,10 @@ async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): entity_id = f"{device_tracker.DOMAIN}.{dev_id}" topic = "/location/jan" - config = {"name": dev_id, "state_topic": topic} + config = { + mqtt.DOMAIN: {device_tracker.DOMAIN: {"name": dev_id, "state_topic": topic}} + } - await help_test_setup_manual_entity_from_yaml(hass, device_tracker.DOMAIN, config) + await help_test_setup_manual_entity_from_yaml(hass, config) assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index efe38234aee..1666ccee6ce 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -59,7 +59,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -1365,7 +1365,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[fan.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][fan.DOMAIN]) config[ATTR_PRESET_MODES] = ["eco", "auto"] config[CONF_PRESET_MODE_COMMAND_TOPIC] = "fan/some_preset_mode_command_topic" config[CONF_PERCENTAGE_COMMAND_TOPIC] = "fan/some_percentage_command_topic" @@ -1639,14 +1639,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.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, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1656,7 +1656,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1669,7 +1669,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1681,7 +1681,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1693,7 +1693,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED, ) @@ -1701,7 +1701,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1714,7 +1714,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1722,41 +1722,41 @@ 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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + 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, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + fan.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, config @@ -1812,42 +1812,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.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, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.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, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, fan.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, fan.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) @@ -1857,7 +1857,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, fan.SERVICE_TURN_ON, ) @@ -1914,9 +1914,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "preset_mode_command_topic": - config["preset_modes"] = ["auto", "eco"] + config[mqtt.DOMAIN][domain]["preset_modes"] = ["auto", "eco"] await help_test_publishing_with_custom_encoding( hass, @@ -1935,12 +1935,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = fan.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = fan.DOMAIN @@ -1951,17 +1953,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = fan.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = fan.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 0cc2be638bf..1e2d64b66cf 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -61,7 +61,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -750,7 +750,7 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[humidifier.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][humidifier.DOMAIN]) config["modes"] = ["eco", "auto"] config[CONF_MODE_COMMAND_TOPIC] = "humidifier/some_mode_command_topic" await help_test_encoding_subscribable_topics( @@ -996,14 +996,14 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.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, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1013,7 +1013,7 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1026,7 +1026,7 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, True, "state-topic", "1", @@ -1038,7 +1038,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1050,7 +1050,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, ) @@ -1058,7 +1058,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1071,7 +1071,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1079,12 +1079,12 @@ 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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1095,31 +1095,31 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { - humidifier.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "target_humidity_command_topic": "humidity-command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "target_humidity_command_topic": "humidity-command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + humidifier.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "target_humidity_command_topic": "humidity-command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, config @@ -1191,42 +1191,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.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, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.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, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, humidifier.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, humidifier.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) @@ -1236,7 +1236,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, humidifier.SERVICE_TURN_ON, ) @@ -1286,9 +1286,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "mode_command_topic": - config["modes"] = ["auto", "eco"] + config[mqtt.DOMAIN][domain]["modes"] = ["auto", "eco"] await help_test_publishing_with_custom_encoding( hass, @@ -1307,12 +1307,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = humidifier.DOMAIN @@ -1323,11 +1325,8 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = humidifier.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") async def test_config_schema_validation(hass): @@ -1344,7 +1343,7 @@ async def test_config_schema_validation(hass): async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): """Test unloading the config entry.""" domain = humidifier.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b794d9260ba..b76979cc990 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1485,9 +1485,17 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): @patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_with_platform_key(hass, caplog): """Test set up a manual MQTT item with a platform key.""" - config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + config = { + mqtt.DOMAIN: { + "light": { + "platform": "mqtt", + "name": "test", + "command_topic": "test-topic", + } + } + } with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml(hass, config) assert ( "Invalid config for [mqtt]: [platform] is an invalid option for [mqtt]" in caplog.text @@ -1497,9 +1505,9 @@ async def test_setup_manual_mqtt_with_platform_key(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): """Test set up a manual MQTT item with an invalid config.""" - config = {"name": "test"} + config = {mqtt.DOMAIN: {"light": {"name": "test"}}} with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml(hass, config) assert ( "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." " Got None. (See ?, line ?)" in caplog.text @@ -1509,8 +1517,8 @@ async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_empty_platform(hass, caplog): """Test set up a manual MQTT platform without items.""" - config = [] - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + config = {mqtt.DOMAIN: {"light": []}} + await help_test_setup_manual_entity_from_yaml(hass, config) assert "voluptuous.error.MultipleInvalid" not in caplog.text @@ -2797,7 +2805,11 @@ async def test_publish_or_subscribe_without_valid_config_entry(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_reload_entry_with_new_config(hass, tmp_path): """Test reloading the config entry with a new yaml config.""" - config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + # Test deprecated YAML configuration under the platform key + # Scheduled to be removed in HA core 2022.12 + config_old = { + "mqtt": {"light": [{"name": "test_old1", "command_topic": "test-topic_old"}]} + } config_yaml_new = { "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] @@ -2812,7 +2824,7 @@ async def test_reload_entry_with_new_config(hass, tmp_path): } ], } - await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + await help_test_setup_manual_entity_from_yaml(hass, config_old) assert hass.states.get("light.test_old1") is not None await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) @@ -2824,7 +2836,9 @@ async def test_reload_entry_with_new_config(hass, tmp_path): @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): """Test disabling and enabling the config entry.""" - config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_old = { + "mqtt": {"light": [{"name": "test_old1", "command_topic": "test-topic_old"}]} + } config_yaml_new = { "mqtt": { "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] @@ -2839,7 +2853,7 @@ async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): } ], } - await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + await help_test_setup_manual_entity_from_yaml(hass, config_old) assert hass.states.get("light.test_old1") is not None mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] @@ -2929,7 +2943,9 @@ async def test_setup_manual_items_with_unique_ids( hass, tmp_path, caplog, config, unique ): """Test setup manual items is generating unique id's.""" - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml( + hass, {mqtt.DOMAIN: {"light": config}} + ) assert hass.states.get("light.test1") is not None assert (hass.states.get("light.test2") is not None) == unique diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index ffe9183fe4c..4b44807b7c9 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -58,7 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -95,8 +95,6 @@ DEFAULT_CONFIG_2 = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} # Scheduled to be removed in HA core 2022.12 DEFAULT_CONFIG_LEGACY = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN -DEFAULT_CONFIG_2_LEGACY = deepcopy(DEFAULT_CONFIG_2[mqtt.DOMAIN]) -DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN][CONF_PLATFORM] = mqtt.DOMAIN @pytest.fixture(autouse=True) @@ -108,7 +106,7 @@ def vacuum_platform_only(): async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" - assert await async_setup_component(hass, vacuum.DOMAIN, DEFAULT_CONFIG_LEGACY) + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") @@ -616,28 +614,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -646,7 +644,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -658,7 +656,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -666,7 +664,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -679,20 +677,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) @@ -703,27 +701,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { - vacuum.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + vacuum.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config @@ -732,7 +730,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" - data = json.dumps(DEFAULT_CONFIG_2_LEGACY[vacuum.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_2[mqtt.DOMAIN][vacuum.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data ) @@ -778,41 +776,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - vacuum.DOMAIN: { - "platform": "mqtt", - "name": "test", - "battery_level_topic": "test-topic", - "battery_level_template": "{{ value_json.battery_level }}", - "command_topic": "command-topic", - "availability_topic": "avty-topic", + mqtt.DOMAIN: { + vacuum.DOMAIN: { + "name": "test", + "battery_level_topic": "test-topic", + "battery_level_template": "{{ value_json.battery_level }}", + "command_topic": "command-topic", + "availability_topic": "avty-topic", + } } } await help_test_entity_id_update_subscriptions( @@ -827,20 +826,21 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_co 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - vacuum.DOMAIN: { - "platform": "mqtt", - "name": "test", - "battery_level_topic": "state-topic", - "battery_level_template": "{{ value_json.battery_level }}", - "command_topic": "command-topic", - "payload_turn_on": "ON", + mqtt.DOMAIN: { + vacuum.DOMAIN: { + "name": "test", + "battery_level_topic": "state-topic", + "battery_level_template": "{{ value_json.battery_level }}", + "command_topic": "command-topic", + "payload_turn_on": "ON", + } } } await help_test_entity_debug_info_message( @@ -904,8 +904,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["supported_features"] = [ + config = deepcopy(DEFAULT_CONFIG) + config[mqtt.DOMAIN][domain]["supported_features"] = [ "turn_on", "turn_off", "clean_spot", @@ -930,12 +930,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN @@ -975,7 +977,7 @@ async def test_encoding_subscribable_topics( ): """Test handling of incoming encoded payload.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config[CONF_SUPPORTED_FEATURES] = [ "turn_on", "turn_off", @@ -1007,11 +1009,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.mqtttest") # Test deprecated YAML configuration under the platform key diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index b34406f42bc..05f9d0e72f0 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -223,7 +223,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -2094,28 +2094,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2124,7 +2124,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2136,7 +2136,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -2144,7 +2144,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2157,20 +2157,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2181,29 +2181,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + light.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config @@ -2744,42 +2744,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2789,7 +2789,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, light.SERVICE_TURN_ON, ) @@ -2914,11 +2914,11 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "effect_command_topic": - config["effect_list"] = ["random", "color_loop"] + config[mqtt.DOMAIN][domain]["effect_list"] = ["random", "color_loop"] elif topic == "white_command_topic": - config["rgb_command_topic"] = "some-cmd-topic" + config[mqtt.DOMAIN][domain]["rgb_command_topic"] = "some-cmd-topic" await help_test_publishing_with_custom_encoding( hass, @@ -2939,12 +2939,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN @@ -2993,7 +2995,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config[CONF_EFFECT_COMMAND_TOPIC] = "light/CONF_EFFECT_COMMAND_TOPIC" config[CONF_RGB_COMMAND_TOPIC] = "light/CONF_RGB_COMMAND_TOPIC" config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" @@ -3036,7 +3038,7 @@ async def test_encoding_subscribable_topics_brightness( init_payload, ): """Test handling of incoming encoded payload for a brightness only light.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" await help_test_encoding_subscribable_topics( @@ -3138,17 +3140,14 @@ async def test_sending_mqtt_effect_command_with_template( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index e61d4e77286..77ccd73f21b 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -125,7 +125,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -1830,28 +1830,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -1860,7 +1860,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -1872,7 +1872,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -1880,7 +1880,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -1893,20 +1893,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -1917,31 +1917,31 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "schema": "json", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "schema": "json", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + light.DOMAIN: [ + { + "name": "Test 1", + "schema": "json", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "schema": "json", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config @@ -2033,7 +2033,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2043,7 +2043,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_ hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2053,7 +2053,7 @@ async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -2063,27 +2063,21 @@ async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, - mqtt_mock_entry_with_yaml_config, - light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + hass, mqtt_mock_entry_with_yaml_config, light.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, - light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -2093,7 +2087,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, light.SERVICE_TURN_ON, command_payload='{"state":"ON"}', state_payload='{"state":"ON"}', @@ -2158,9 +2152,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "effect_command_topic": - config["effect_list"] = ["random", "color_loop"] + config[mqtt.DOMAIN][domain]["effect_list"] = ["random", "color_loop"] await help_test_publishing_with_custom_encoding( hass, @@ -2181,12 +2175,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN @@ -2217,7 +2213,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config["color_mode"] = True config["supported_color_modes"] = [ "color_temp", @@ -2245,11 +2241,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.test") # Test deprecated YAML configuration under the platform key diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index d88480f9f5a..2b53097f502 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -71,7 +71,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -824,28 +824,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -854,7 +854,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -866,7 +866,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) @@ -874,7 +874,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) @@ -887,20 +887,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -911,33 +911,35 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "schema": "template", - "state_topic": "test-topic", - "command_topic": "test_topic", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "schema": "template", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + light.DOMAIN: [ + { + "name": "Test 1", + "schema": "template", + "state_topic": "test-topic", + "command_topic": "test_topic", + "command_on_template": "on,{{ transition }}", + "command_off_template": "off,{{ transition|d }}", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "schema": "template", + "state_topic": "test-topic2", + "command_topic": "test_topic2", + "command_on_template": "on,{{ transition }}", + "command_off_template": "off,{{ transition|d }}", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config @@ -1026,56 +1028,57 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, light.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, light.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test-topic", - "command_on_template": "ON", - "command_off_template": "off,{{ transition|d }}", - "state_template": '{{ value.split(",")[0] }}', + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test-topic", + "command_on_template": "ON", + "command_off_template": "off,{{ transition|d }}", + "state_template": '{{ value.split(",")[0] }}', + } } } await help_test_entity_debug_info_message( @@ -1147,9 +1150,9 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) + config = copy.deepcopy(DEFAULT_CONFIG) if topic == "effect_command_topic": - config["effect_list"] = ["random", "color_loop"] + config[mqtt.DOMAIN][domain]["effect_list"] = ["random", "color_loop"] await help_test_publishing_with_custom_encoding( hass, @@ -1170,12 +1173,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = light.DOMAIN @@ -1200,7 +1205,7 @@ async def test_encoding_subscribable_topics( init_payload, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[light.DOMAIN]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][light.DOMAIN]) config["state_template"] = "{{ value }}" await help_test_encoding_subscribable_topics( hass, @@ -1219,17 +1224,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = light.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index de97de23a1e..0784065ddbe 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -49,7 +49,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -452,28 +452,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -482,7 +482,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -494,7 +494,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED, ) @@ -502,7 +502,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -515,7 +515,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -523,41 +523,41 @@ 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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + 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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, caplog, lock.DOMAIN, DEFAULT_CONFIG ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one lock per unique_id.""" config = { - lock.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test_topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + lock.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test_topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, config @@ -626,42 +626,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, lock.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, lock.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, DEFAULT_CONFIG ) @@ -671,7 +671,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, SERVICE_LOCK, command_payload="LOCK", ) @@ -701,7 +701,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = lock.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -720,12 +720,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = lock.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = lock.DOMAIN @@ -754,7 +756,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, lock.DOMAIN, - DEFAULT_CONFIG_LEGACY[lock.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][lock.DOMAIN], topic, value, attribute, @@ -765,17 +767,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): """Test setup manual configured MQTT entity.""" platform = lock.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = lock.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 69b0473fb9d..65c8655472c 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -59,7 +59,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -446,28 +446,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -476,7 +476,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -488,7 +488,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_NUMBER_ATTRIBUTES_BLOCKED, ) @@ -496,7 +496,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -509,20 +509,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -533,29 +533,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one number per unique_id.""" config = { - number.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + number.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, config @@ -564,7 +564,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered number.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[number.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][number.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data ) @@ -624,42 +624,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, number.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, number.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) @@ -669,7 +669,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, SERVICE_SET_VALUE, service_parameters={ATTR_VALUE: 45}, command_payload="45", @@ -882,7 +882,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = NUMBER_DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -901,12 +901,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = number.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = number.DOMAIN @@ -935,8 +937,8 @@ async def test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - "number", - DEFAULT_CONFIG_LEGACY["number"], + number.DOMAIN, + DEFAULT_CONFIG[mqtt.DOMAIN][number.DOMAIN], topic, value, attribute, @@ -947,17 +949,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = number.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = number.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index a1c1644cc37..122c32caa03 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -87,25 +87,26 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, scene.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, scene.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": 1, + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": 1, + } } } @@ -123,11 +124,12 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - scene.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "payload_on": 1, + mqtt.DOMAIN: { + scene.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "payload_on": 1, + } } } @@ -145,20 +147,20 @@ async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_confi async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one scene per unique_id.""" config = { - scene.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + scene.DOMAIN: [ + { + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, config @@ -175,8 +177,8 @@ async def test_discovery_removal_scene(hass, mqtt_mock_entry_no_yaml_config, cap async def test_discovery_update_payload(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered scene.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[scene.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][scene.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][scene.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["payload_on"] = "ON" @@ -223,12 +225,14 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = scene.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = scene.DOMAIN @@ -239,17 +243,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = scene.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = scene.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index c66638a18af..5b588304061 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -49,7 +49,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -323,28 +323,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -353,7 +353,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -365,7 +365,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_SELECT_ATTRIBUTES_BLOCKED, ) @@ -373,7 +373,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -386,20 +386,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -410,31 +410,31 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one select per unique_id.""" config = { - select.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - "options": ["milk", "beer"], - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - "options": ["milk", "beer"], - }, - ] + mqtt.DOMAIN: { + select.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + "options": ["milk", "beer"], + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + "options": ["milk", "beer"], + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, config @@ -443,7 +443,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered select.""" - data = json.dumps(DEFAULT_CONFIG_LEGACY[select.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][select.DOMAIN]) await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data ) @@ -501,42 +501,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, select.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, select.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) @@ -546,7 +546,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, select.SERVICE_SELECT_OPTION, service_parameters={ATTR_OPTION: "beer"}, command_payload="beer", @@ -635,8 +635,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = select.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] - config["options"] = ["milk", "beer"] + config = DEFAULT_CONFIG + config[mqtt.DOMAIN][domain]["options"] = ["milk", "beer"] await help_test_publishing_with_custom_encoding( hass, @@ -655,12 +655,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = select.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = select.DOMAIN @@ -685,13 +687,13 @@ async def test_encoding_subscribable_topics( attribute_value, ): """Test handling of incoming encoded payload.""" - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY["select"]) + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][select.DOMAIN]) config["options"] = ["milk", "beer"] await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, caplog, - "select", + select.DOMAIN, config, topic, value, @@ -703,17 +705,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = select.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = select.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index c5cf377cf1a..6cfaa9678bb 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -59,7 +59,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -585,21 +585,21 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -608,7 +608,7 @@ async def test_default_availability_list_payload( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -617,7 +617,7 @@ async def test_default_availability_list_payload_all( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_all( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -626,34 +626,31 @@ async def test_default_availability_list_payload_any( ): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_any( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_single( - hass, mqtt_mock_entry_no_yaml_config, caplog -): +async def test_default_availability_list_single(hass, caplog): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( hass, - mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + 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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_availability(hass, mqtt_mock_entry_no_yaml_config): """Test availability discovery update.""" await help_test_discovery_update_availability( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -760,7 +757,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -772,7 +769,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, MQTT_SENSOR_ATTRIBUTES_BLOCKED, ) @@ -780,7 +777,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -793,20 +790,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -817,27 +814,27 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { - sensor.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + sensor.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, config @@ -951,42 +948,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1021,42 +1018,42 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config) async def test_entity_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_max_messages(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_max_messages( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY, None + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG, None ) async def test_entity_debug_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_remove( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_debug_info_update_entity_id(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_update_entity_id( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): """Test entity disabled by default.""" await help_test_entity_disabled_by_default( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1064,7 +1061,7 @@ async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): async def test_entity_category(hass, mqtt_mock_entry_no_yaml_config): """Test entity category.""" await help_test_entity_category( - hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -1101,12 +1098,14 @@ async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_con async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = sensor.DOMAIN @@ -1230,7 +1229,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, - DEFAULT_CONFIG_LEGACY[sensor.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][sensor.DOMAIN], topic, value, attribute, @@ -1242,17 +1241,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = sensor.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = sensor.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index fd91847f767..7c16f3f01a3 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -46,7 +46,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -457,27 +457,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.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, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -495,13 +496,14 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - siren.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + siren.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -558,7 +560,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) @@ -567,14 +569,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY, {} + hass, mqtt_mock_entry_no_yaml_config, siren.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, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) @@ -587,20 +589,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -611,29 +613,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one siren per unique_id.""" config = { - siren.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + siren.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, config @@ -656,8 +658,8 @@ async def test_discovery_update_siren_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -693,8 +695,8 @@ async def test_discovery_update_siren_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered siren.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[siren.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "siren/state1" @@ -844,42 +846,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.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, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.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, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, siren.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, siren.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) @@ -889,7 +891,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, siren.SERVICE_TURN_ON, command_payload='{"state":"ON"}', ) @@ -926,8 +928,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with command templates and different encoding.""" domain = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config[siren.ATTR_AVAILABLE_TONES] = ["siren", "xylophone"] + config = copy.deepcopy(DEFAULT_CONFIG) + config[mqtt.DOMAIN][domain][siren.ATTR_AVAILABLE_TONES] = ["siren", "xylophone"] await help_test_publishing_with_custom_encoding( hass, @@ -946,12 +948,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = siren.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = siren.DOMAIN @@ -980,7 +984,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, - DEFAULT_CONFIG_LEGACY[siren.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][siren.DOMAIN], topic, value, attribute, @@ -991,17 +995,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = siren.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = siren.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 4c2fbf3a596..917c5fa43d8 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -61,7 +61,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, - help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -375,28 +375,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -405,7 +405,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -417,7 +417,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, MQTT_VACUUM_ATTRIBUTES_BLOCKED, ) @@ -425,7 +425,7 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -438,7 +438,7 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) @@ -446,12 +446,12 @@ 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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) @@ -462,29 +462,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { - vacuum.DOMAIN: [ - { - "platform": "mqtt", - "schema": "state", - "name": "Test 1", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "schema": "state", - "name": "Test 2", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + vacuum.DOMAIN: [ + { + "schema": "state", + "name": "Test 1", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "schema": "state", + "name": "Test 2", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config @@ -539,42 +539,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_2_LEGACY + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -584,7 +584,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, - DEFAULT_CONFIG_2_LEGACY, + DEFAULT_CONFIG_2, vacuum.SERVICE_START, command_payload="start", state_payload="{}", @@ -643,8 +643,8 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[domain]) - config["supported_features"] = [ + config = deepcopy(DEFAULT_CONFIG) + config[mqtt.DOMAIN][domain]["supported_features"] = [ "battery", "clean_spot", "fan_speed", @@ -674,12 +674,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = vacuum.DOMAIN @@ -719,7 +721,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, - DEFAULT_CONFIG_LEGACY[vacuum.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][vacuum.DOMAIN], topic, value, attribute, @@ -731,11 +733,8 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN - config = deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + await help_test_setup_manual_entity_from_yaml(hass, DEFAULT_CONFIG) + assert hass.states.get(f"{platform}.mqtttest") # Test deprecated YAML configuration under the platform key diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 87d919f41c5..72a09529242 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -43,7 +43,7 @@ from .test_common import ( 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_bad_json, help_test_update_with_json_attrs_not_dict, ) @@ -227,27 +227,28 @@ async def test_availability_when_connection_lost( ): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.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, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -265,13 +266,14 @@ async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_conf async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } } } @@ -328,7 +330,7 @@ async def test_setting_attribute_via_mqtt_json_message( ): """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, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) @@ -337,14 +339,14 @@ async def test_setting_blocked_attribute_via_mqtt_json_message( ): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY, {} + hass, mqtt_mock_entry_no_yaml_config, switch.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, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) @@ -357,20 +359,20 @@ async def test_update_with_json_attrs_not_dict( mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON( +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( + await help_test_update_with_json_attrs_bad_json( hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) @@ -381,29 +383,29 @@ async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplo mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, ) async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one switch per unique_id.""" config = { - switch.DOMAIN: [ - { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test 2", - "state_topic": "test-topic", - "command_topic": "command-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] + mqtt.DOMAIN: { + switch.DOMAIN: [ + { + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "command-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } } await help_test_unique_id( hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, config @@ -426,8 +428,8 @@ async def test_discovery_update_switch_topic_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -463,8 +465,8 @@ async def test_discovery_update_switch_template( hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test update of discovered switch.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_LEGACY[switch.DOMAIN]) + config1 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" config1["state_topic"] = "switch/state1" @@ -534,42 +536,42 @@ async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.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, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.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, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_with_yaml_config, switch.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, switch.DOMAIN, DEFAULT_CONFIG_LEGACY + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) @@ -579,7 +581,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY, + DEFAULT_CONFIG, switch.SERVICE_TURN_ON, ) @@ -615,7 +617,7 @@ async def test_publishing_with_custom_encoding( ): """Test publishing MQTT payload with different encoding.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_publishing_with_custom_encoding( hass, @@ -634,12 +636,14 @@ async def test_publishing_with_custom_encoding( async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = switch.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ) +# Test deprecated YAML configuration under the platform key +# Scheduled to be removed in HA core 2022.12 async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): """Test reloading the MQTT platform with late entry setup.""" domain = switch.DOMAIN @@ -668,7 +672,7 @@ async def test_encoding_subscribable_topics( mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, - DEFAULT_CONFIG_LEGACY[switch.DOMAIN], + DEFAULT_CONFIG[mqtt.DOMAIN][switch.DOMAIN], topic, value, attribute, @@ -679,17 +683,14 @@ async def test_encoding_subscribable_topics( async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = switch.DOMAIN - config = copy.deepcopy(DEFAULT_CONFIG_LEGACY[platform]) - config["name"] = "test" - del config["platform"] - await help_test_setup_manual_entity_from_yaml(hass, platform, config) - assert hass.states.get(f"{platform}.test") is not None + 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 = switch.DOMAIN - config = DEFAULT_CONFIG_LEGACY[domain] + config = DEFAULT_CONFIG await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) From 3dd0dbf38fe016e1e2a223fc6ced086a79cde4e9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 17 Sep 2022 21:43:42 +0200 Subject: [PATCH 200/231] Make hass.data["mqtt"] an instance of a DataClass (#77972) * Use dataclass to reference hass.data globals * Add discovery_registry_hooks to dataclass * Move discovery registry hooks to dataclass * Add device triggers to dataclass * Cleanup DEVICE_TRIGGERS const * Add last_discovery to data_class * Simplify typing for class `Subscription` * Follow up on comment * Redo suggested typing change to sasisfy mypy * Restore typing * Add mypy version to CI check logging * revert changes to ci.yaml * Add docstr for protocol Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Mypy update after merging #78399 * Remove mypy ignore * Correct return type Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/mqtt/__init__.py | 104 ++++++++---------- homeassistant/components/mqtt/client.py | 45 +++++--- homeassistant/components/mqtt/config_flow.py | 25 +++-- homeassistant/components/mqtt/const.py | 8 -- .../components/mqtt/device_trigger.py | 35 +++--- homeassistant/components/mqtt/diagnostics.py | 2 +- homeassistant/components/mqtt/discovery.py | 15 ++- homeassistant/components/mqtt/mixins.py | 61 ++++++---- tests/common.py | 2 +- tests/components/mqtt/test_config_flow.py | 2 +- tests/components/mqtt/test_discovery.py | 2 +- tests/components/mqtt/test_init.py | 10 +- 12 files changed, 174 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 842e5b6405f..315f116ed92 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -20,13 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, SERVICE_RELOAD, ) -from homeassistant.core import ( - CALLBACK_TYPE, - HassJob, - HomeAssistant, - ServiceCall, - callback, -) +from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import ( config_validation as cv, @@ -71,15 +65,7 @@ from .const import ( # noqa: F401 CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, - CONFIG_ENTRY_IS_SETUP, DATA_MQTT, - DATA_MQTT_CONFIG, - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, - DATA_MQTT_RELOAD_DISPATCHERS, - DATA_MQTT_RELOAD_ENTRY, - DATA_MQTT_RELOAD_NEEDED, - DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE, - DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_QOS, DEFAULT_RETAIN, @@ -89,7 +75,7 @@ from .const import ( # noqa: F401 PLATFORMS, RELOADABLE_PLATFORMS, ) -from .mixins import async_discover_yaml_entities +from .mixins import MqttData, async_discover_yaml_entities from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -177,6 +163,8 @@ async def _async_setup_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the MQTT protocol service.""" + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + conf: ConfigType | None = config.get(DOMAIN) websocket_api.async_register_command(hass, websocket_subscribe) @@ -185,7 +173,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if conf: conf = dict(conf) - hass.data[DATA_MQTT_CONFIG] = conf + mqtt_data.config = conf if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None: # Create an import flow if the user has yaml configured entities etc. @@ -197,12 +185,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={}, ) - hass.data[DATA_MQTT_RELOAD_NEEDED] = True + mqtt_data.reload_needed = True elif mqtt_entry_status is False: _LOGGER.info( "MQTT will be not available until the config entry is enabled", ) - hass.data[DATA_MQTT_RELOAD_NEEDED] = True + mqtt_data.reload_needed = True return True @@ -260,33 +248,34 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - Causes for this is config entry options changing. """ - mqtt_client = hass.data[DATA_MQTT] + mqtt_data: MqttData = hass.data[DATA_MQTT] + assert (client := mqtt_data.client) is not None - if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + if (conf := mqtt_data.config) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - mqtt_client.conf = _merge_extended_config(entry, conf) - await mqtt_client.async_disconnect() - mqtt_client.init_client() - await mqtt_client.async_connect() + mqtt_data.config = _merge_extended_config(entry, conf) + await client.async_disconnect() + client.init_client() + await client.async_connect() await discovery.async_stop(hass) - if mqtt_client.conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, mqtt_client.conf, entry) + if client.conf.get(CONF_DISCOVERY): + await _async_setup_discovery(hass, cast(ConfigType, mqtt_data.config), entry) async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" - if DATA_MQTT_RELOAD_ENTRY in hass.data: + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.reload_entry: hass_config = await conf_util.async_hass_config_yaml(hass) - mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) - hass.data[DATA_MQTT_CONFIG] = mqtt_config + mqtt_data.config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) # Remove unknown keys from config entry data _filter_entry_config(hass, entry) # Merge basic configuration, and add missing defaults for basic options - _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) + _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") @@ -294,7 +283,7 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | # If user doesn't have configuration.yaml config, generate default values # for options not in config entry data - if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + if (conf := mqtt_data.config) is None: conf = CONFIG_SCHEMA_BASE(dict(entry.data)) # User has configuration.yaml config, warn about config entry overrides @@ -317,21 +306,20 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + # Merge basic configuration, and add missing defaults for basic options if (conf := await async_fetch_config(hass, entry)) is None: # Bail out return False - - hass.data[DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] = {} - hass.data[DATA_MQTT] = MQTT(hass, entry, conf) + mqtt_data.client = MQTT(hass, entry, conf) # Restore saved subscriptions - if DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE in hass.data: - hass.data[DATA_MQTT].subscriptions = hass.data.pop( - DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE - ) + if mqtt_data.subscriptions_to_restore: + mqtt_data.client.subscriptions = mqtt_data.subscriptions_to_restore + mqtt_data.subscriptions_to_restore = [] entry.add_update_listener(_async_config_entry_updated) - await hass.data[DATA_MQTT].async_connect() + await mqtt_data.client.async_connect() async def async_publish_service(call: ServiceCall) -> None: """Handle MQTT publish service calls.""" @@ -380,7 +368,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return - await hass.data[DATA_MQTT].async_publish(msg_topic, payload, qos, retain) + assert mqtt_data.client is not None and msg_topic is not None + await mqtt_data.client.async_publish(msg_topic, payload, qos, retain) hass.services.async_register( DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA @@ -421,7 +410,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # setup platforms and discovery - hass.data[CONFIG_ENTRY_IS_SETUP] = set() async def async_setup_reload_service() -> None: """Create the reload service for the MQTT domain.""" @@ -435,7 +423,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Reload the modern yaml platforms config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} - hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + mqtt_data.updated_config = config_yaml.get(DOMAIN, {}) await asyncio.gather( *( [ @@ -476,13 +464,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setup reload service after all platforms have loaded await async_setup_reload_service() # When the entry is reloaded, also reload manual set up items to enable MQTT - if DATA_MQTT_RELOAD_ENTRY in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_ENTRY) + if mqtt_data.reload_entry: + mqtt_data.reload_entry = False reload_manual_setup = True # When the entry was disabled before, reload manual set up items to enable MQTT again - if DATA_MQTT_RELOAD_NEEDED in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + if mqtt_data.reload_needed: + mqtt_data.reload_needed = False reload_manual_setup = True if reload_manual_setup: @@ -592,7 +580,9 @@ def async_subscribe_connection_status( def is_connected(hass: HomeAssistant) -> bool: """Return if MQTT client is connected.""" - return hass.data[DATA_MQTT].connected + mqtt_data: MqttData = hass.data[DATA_MQTT] + assert mqtt_data.client is not None + return mqtt_data.client.connected async def async_remove_config_entry_device( @@ -608,6 +598,10 @@ async def async_remove_config_entry_device( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload MQTT dump and publish service when the config entry is unloaded.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] + assert mqtt_data.client is not None + mqtt_client = mqtt_data.client + # Unload publish and dump services. hass.services.async_remove( DOMAIN, @@ -620,7 +614,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Stop the discovery await discovery.async_stop(hass) - mqtt_client: MQTT = hass.data[DATA_MQTT] # Unload the platforms await asyncio.gather( *( @@ -630,26 +623,23 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.async_block_till_done() # Unsubscribe reload dispatchers - while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []): + while reload_dispatchers := mqtt_data.reload_dispatchers: reload_dispatchers.pop()() - hass.data[CONFIG_ENTRY_IS_SETUP] = set() # Cleanup listeners mqtt_client.cleanup() # Trigger reload manual MQTT items at entry setup if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: # The entry is disabled reload legacy manual items when the entry is enabled again - hass.data[DATA_MQTT_RELOAD_NEEDED] = True + mqtt_data.reload_needed = True elif mqtt_entry_status is True: # The entry is reloaded: # Trigger re-fetching the yaml config at entry setup - hass.data[DATA_MQTT_RELOAD_ENTRY] = True + mqtt_data.reload_entry = True # Reload the legacy yaml platform to make entities unavailable await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) # Cleanup entity registry hooks - registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS - ] + registry_hooks = mqtt_data.discovery_registry_hooks while registry_hooks: registry_hooks.popitem()[1]() # Wait for all ACKs and stop the loop @@ -657,6 +647,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Store remaining subscriptions to be able to restore or reload them # when the entry is set up again if mqtt_client.subscriptions: - hass.data[DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE] = mqtt_client.subscriptions + mqtt_data.subscriptions_to_restore = mqtt_client.subscriptions return True diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 57f51593ed4..50674706624 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Coroutine, Iterable +from collections.abc import Callable, Coroutine, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -17,6 +17,7 @@ import attr import certifi from paho.mqtt.client import MQTTMessage +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_PASSWORD, @@ -52,7 +53,6 @@ from .const import ( MQTT_DISCONNECTED, PROTOCOL_31, ) -from .discovery import LAST_DISCOVERY from .models import ( AsyncMessageCallbackType, MessageCallbackType, @@ -68,6 +68,9 @@ if TYPE_CHECKING: # because integrations should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt + from .mixins import MqttData + + _LOGGER = logging.getLogger(__name__) DISCOVERY_COOLDOWN = 2 @@ -97,8 +100,12 @@ async def async_publish( encoding: str | None = DEFAULT_ENCODING, ) -> None: """Publish message to a MQTT topic.""" + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from .mixins import MqttData - if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot publish to topic '{topic}', MQTT is not enabled" ) @@ -126,11 +133,13 @@ async def async_publish( ) return - await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) + await mqtt_data.client.async_publish( + topic, outgoing_payload, qos or 0, retain or False + ) AsyncDeprecatedMessageCallbackType = Callable[ - [str, ReceivePayloadType, int], Awaitable[None] + [str, ReceivePayloadType, int], Coroutine[Any, Any, None] ] DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] @@ -175,13 +184,18 @@ async def async_subscribe( | DeprecatedMessageCallbackType | AsyncDeprecatedMessageCallbackType, qos: int = DEFAULT_QOS, - encoding: str | None = "utf-8", + encoding: str | None = DEFAULT_ENCODING, ): """Subscribe to an MQTT topic. Call the return value to unsubscribe. """ - if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from .mixins import MqttData + + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled" ) @@ -206,7 +220,7 @@ async def async_subscribe( cast(DeprecatedMessageCallbackType, msg_callback) ) - async_remove = await hass.data[DATA_MQTT].async_subscribe( + async_remove = await mqtt_data.client.async_subscribe( topic, catch_log_exception( wrapped_msg_callback, @@ -309,15 +323,17 @@ class MQTT: def __init__( self, - hass, - config_entry, - conf, + hass: HomeAssistant, + config_entry: ConfigEntry, + conf: ConfigType, ) -> None: """Initialize Home Assistant MQTT client.""" # 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 + self._mqtt_data: MqttData = hass.data[DATA_MQTT] + self.hass = hass self.config_entry = config_entry self.conf = conf @@ -634,7 +650,6 @@ class MQTT: subscription.job, ) continue - self.hass.async_run_hass_job( subscription.job, ReceiveMessage( @@ -694,10 +709,10 @@ class MQTT: async def _discovery_cooldown(self): now = time.time() # Reset discovery and subscribe cooldowns - self.hass.data[LAST_DISCOVERY] = now + self._mqtt_data.last_discovery = now self._last_subscribe = now - last_discovery = self.hass.data[LAST_DISCOVERY] + last_discovery = self._mqtt_data.last_discovery last_subscribe = self._last_subscribe wait_until = max( last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN @@ -705,7 +720,7 @@ class MQTT: while now < wait_until: await asyncio.sleep(wait_until - now) now = time.time() - last_discovery = self.hass.data[LAST_DISCOVERY] + last_discovery = self._mqtt_data.last_discovery last_subscribe = self._last_subscribe wait_until = max( last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 538c12d258c..12d97b41a74 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from .client import MqttClientSetup @@ -30,12 +30,13 @@ from .const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_WILL_MESSAGE, - DATA_MQTT_CONFIG, + DATA_MQTT, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_WILL, DOMAIN, ) +from .mixins import MqttData from .util import MQTT_WILL_BIRTH_SCHEMA MQTT_TIMEOUT = 5 @@ -164,9 +165,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT broker configuration.""" + mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) errors = {} current_config = self.config_entry.data - yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) + yaml_config = mqtt_data.config or {} if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, @@ -214,9 +216,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT options.""" + mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) errors = {} current_config = self.config_entry.data - yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) + yaml_config = mqtt_data.config or {} options_config: dict[str, Any] = {} if user_input is not None: bad_birth = False @@ -334,14 +337,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) -def try_connection(hass, broker, port, username, password, protocol="3.1"): +def try_connection( + hass: HomeAssistant, + broker: str, + port: int, + username: str | None, + password: str | None, + protocol: str = "3.1", +) -> 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 - yaml_config = hass.data.get(DATA_MQTT_CONFIG, {}) + mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + yaml_config = mqtt_data.config or {} entry_config = { CONF_BROKER: broker, CONF_PORT: port, @@ -351,7 +362,7 @@ def try_connection(hass, broker, port, username, password, protocol="3.1"): } client = MqttClientSetup({**yaml_config, **entry_config}).client - result = queue.Queue(maxsize=1) + result: queue.Queue[bool] = queue.Queue(maxsize=1) def on_connect(client_, userdata, flags, result_code): """Handle connection result.""" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index c8af58862e0..93410f0c792 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -30,16 +30,8 @@ CONF_CLIENT_CERT = "client_cert" CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" -CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_MQTT = "mqtt" -DATA_MQTT_SUBSCRIPTIONS_TO_RESTORE = "mqtt_client_subscriptions" -DATA_MQTT_DISCOVERY_REGISTRY_HOOKS = "mqtt_discovery_registry_hooks" -DATA_MQTT_CONFIG = "mqtt_config" MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" -DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" -DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry" -DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" -DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 30d6fdea05f..7e37ed72821 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -33,11 +33,13 @@ from .const import ( CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, + DATA_MQTT, DOMAIN, ) from .discovery import MQTT_DISCOVERY_DONE from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MqttData, MqttDiscoveryDeviceUpdate, send_discovery_done, update_device, @@ -81,8 +83,6 @@ TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend( extra=vol.REMOVE_EXTRA, ) -DEVICE_TRIGGERS = "mqtt_device_triggers" - LOG_NAME = "Device trigger" @@ -203,6 +203,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): self.device_id = device_id self.discovery_data = discovery_data self.hass = hass + self._mqtt_data: MqttData = hass.data[DATA_MQTT] MqttDiscoveryDeviceUpdate.__init__( self, @@ -217,8 +218,8 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): """Initialize the device trigger.""" discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH] discovery_id = discovery_hash[1] - if discovery_id not in self.hass.data.setdefault(DEVICE_TRIGGERS, {}): - self.hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger( + if discovery_id not in self._mqtt_data.device_triggers: + self._mqtt_data.device_triggers[discovery_id] = Trigger( hass=self.hass, device_id=self.device_id, discovery_data=self.discovery_data, @@ -230,7 +231,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): value_template=self._config[CONF_VALUE_TEMPLATE], ) else: - await self.hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger( + await self._mqtt_data.device_triggers[discovery_id].update_trigger( self._config ) debug_info.add_trigger_discovery_data( @@ -246,16 +247,16 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): ) config = TRIGGER_DISCOVERY_SCHEMA(discovery_data) update_device(self.hass, self._config_entry, config) - device_trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id] + device_trigger: Trigger = self._mqtt_data.device_triggers[discovery_id] await device_trigger.update_trigger(config) async def async_tear_down(self) -> None: """Cleanup device trigger.""" discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH] discovery_id = discovery_hash[1] - if discovery_id in self.hass.data[DEVICE_TRIGGERS]: + if discovery_id in self._mqtt_data.device_triggers: _LOGGER.info("Removing trigger: %s", discovery_hash) - trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id] + trigger: Trigger = self._mqtt_data.device_triggers[discovery_id] trigger.detach_trigger() debug_info.remove_trigger_discovery_data(self.hass, discovery_hash) @@ -280,11 +281,10 @@ async def async_setup_trigger( async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None: """Handle Mqtt removed from a device.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] triggers = await async_get_triggers(hass, device_id) for trig in triggers: - device_trigger: Trigger = hass.data[DEVICE_TRIGGERS].pop( - trig[CONF_DISCOVERY_ID] - ) + device_trigger: Trigger = mqtt_data.device_triggers.pop(trig[CONF_DISCOVERY_ID]) if device_trigger: device_trigger.detach_trigger() discovery_data = cast(dict, device_trigger.discovery_data) @@ -296,12 +296,13 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for MQTT devices.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] triggers: list[dict[str, str]] = [] - if DEVICE_TRIGGERS not in hass.data: + if not mqtt_data.device_triggers: return triggers - for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items(): + for discovery_id, trig in mqtt_data.device_triggers.items(): if trig.device_id != device_id or trig.topic is None: continue @@ -324,12 +325,12 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - hass.data.setdefault(DEVICE_TRIGGERS, {}) + mqtt_data: MqttData = hass.data[DATA_MQTT] device_id = config[CONF_DEVICE_ID] discovery_id = config[CONF_DISCOVERY_ID] - if discovery_id not in hass.data[DEVICE_TRIGGERS]: - hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger( + if discovery_id not in mqtt_data.device_triggers: + mqtt_data.device_triggers[discovery_id] = Trigger( hass=hass, device_id=device_id, discovery_data=None, @@ -340,6 +341,6 @@ async def async_attach_trigger( qos=None, value_template=None, ) - return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger( + return await mqtt_data.device_triggers[discovery_id].add_trigger( action, trigger_info ) diff --git a/homeassistant/components/mqtt/diagnostics.py b/homeassistant/components/mqtt/diagnostics.py index ea490783fc0..2a6322cac63 100644 --- a/homeassistant/components/mqtt/diagnostics.py +++ b/homeassistant/components/mqtt/diagnostics.py @@ -43,7 +43,7 @@ def _async_get_diagnostics( device: DeviceEntry | None = None, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - mqtt_instance: MQTT = hass.data[DATA_MQTT] + mqtt_instance: MQTT = hass.data[DATA_MQTT].client redacted_config = async_redact_data(mqtt_instance.conf, REDACT_CONFIG) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 8a4c4d0c542..65051ce54fc 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 TYPE_CHECKING from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant @@ -28,9 +29,13 @@ from .const import ( ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, CONF_TOPIC, + DATA_MQTT, DOMAIN, ) +if TYPE_CHECKING: + from .mixins import MqttData + _LOGGER = logging.getLogger(__name__) TOPIC_MATCHER = re.compile( @@ -69,7 +74,6 @@ INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" -LAST_DISCOVERY = "mqtt_last_discovery" TOPIC_BASE = "~" @@ -80,12 +84,12 @@ class MQTTConfig(dict): discovery_data: dict -def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple) -> None: +def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: """Clear entry in ALREADY_DISCOVERED list.""" del hass.data[ALREADY_DISCOVERED][discovery_hash] -def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple): +def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]): """Clear entry in ALREADY_DISCOVERED list.""" hass.data[ALREADY_DISCOVERED][discovery_hash] = {} @@ -94,11 +98,12 @@ async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic, config_entry=None ) -> None: """Start MQTT Discovery.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] mqtt_integrations = {} async def async_discovery_message_received(msg): """Process the received message.""" - hass.data[LAST_DISCOVERY] = time.time() + mqtt_data.last_discovery = time.time() payload = msg.payload topic = msg.topic topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1) @@ -253,7 +258,7 @@ async def async_start( # noqa: C901 ) ) - hass.data[LAST_DISCOVERY] = time.time() + mqtt_data.last_discovery = time.time() mqtt_integrations = await async_get_mqtt(hass) hass.data[INTEGRATION_UNSUBSCRIBE] = {} diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index fddbe838303..a16394667d8 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -4,9 +4,10 @@ from __future__ import annotations from abc import abstractmethod import asyncio from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field from functools import partial import logging -from typing import Any, Protocol, cast, final +from typing import TYPE_CHECKING, Any, Protocol, cast, final import voluptuous as vol @@ -60,7 +61,7 @@ from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription -from .client import async_publish +from .client import MQTT, Subscription, async_publish from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -70,11 +71,6 @@ from .const import ( CONF_QOS, CONF_TOPIC, DATA_MQTT, - DATA_MQTT_CONFIG, - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS, - DATA_MQTT_RELOAD_DISPATCHERS, - DATA_MQTT_RELOAD_ENTRY, - DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, @@ -98,6 +94,9 @@ from .subscription import ( ) from .util import mqtt_config_entry_enabled, valid_subscribe_topic +if TYPE_CHECKING: + from .device_trigger import Trigger + _LOGGER = logging.getLogger(__name__) AVAILABILITY_ALL = "all" @@ -274,6 +273,24 @@ def warn_for_legacy_schema(domain: str) -> Callable: return validator +@dataclass +class MqttData: + """Keep the MQTT entry data.""" + + client: MQTT | None = None + config: ConfigType | None = None + device_triggers: dict[str, Trigger] = field(default_factory=dict) + discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field( + default_factory=dict + ) + last_discovery: float = 0.0 + reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) + reload_entry: bool = False + reload_needed: bool = False + subscriptions_to_restore: list[Subscription] = field(default_factory=list) + updated_config: ConfigType = field(default_factory=dict) + + class SetupEntity(Protocol): """Protocol type for async_setup_entities.""" @@ -292,11 +309,12 @@ async def async_discover_yaml_entities( hass: HomeAssistant, platform_domain: str ) -> None: """Discover entities for a platform.""" - if DATA_MQTT_UPDATED_CONFIG in hass.data: + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.updated_config: # The platform has been reloaded - config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] + config_yaml = mqtt_data.updated_config else: - config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + config_yaml = mqtt_data.config or {} if not config_yaml: return if platform_domain not in config_yaml: @@ -318,8 +336,9 @@ async def async_get_platform_config_from_yaml( ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] if config_yaml is None: - config_yaml = hass.data.get(DATA_MQTT_CONFIG) + config_yaml = mqtt_data.config if not config_yaml: return [] if not (platform_configs := config_yaml.get(platform_domain)): @@ -334,6 +353,7 @@ async def async_setup_entry_helper( schema: vol.Schema, ) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] async def async_discover(discovery_payload): """Discover and add an MQTT entity, automation or tag.""" @@ -357,7 +377,7 @@ async def async_setup_entry_helper( ) raise - hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append( + mqtt_data.reload_dispatchers.append( async_dispatcher_connect( hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover ) @@ -372,7 +392,8 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Help to set up the platform for manual configured MQTT entities.""" - if DATA_MQTT_RELOAD_ENTRY in hass.data: + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.reload_entry: _LOGGER.debug( "MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry", platform_domain, @@ -597,7 +618,10 @@ class MqttAvailability(Entity): @property def available(self) -> bool: """Return if the device is available.""" - if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: + mqtt_data: MqttData = self.hass.data[DATA_MQTT] + assert mqtt_data.client is not None + client = mqtt_data.client + if not client.connected and not self.hass.is_stopping: return False if not self._avail_topics: return True @@ -632,7 +656,7 @@ async def cleanup_device_registry( ) -def get_discovery_hash(discovery_data: dict) -> tuple: +def get_discovery_hash(discovery_data: dict) -> tuple[str, str]: """Get the discovery hash from the discovery data.""" return discovery_data[ATTR_DISCOVERY_HASH] @@ -817,9 +841,8 @@ class MqttDiscoveryUpdate(Entity): self._removed_from_hass = False if discovery_data is None: return - self._registry_hooks: dict[tuple, CALLBACK_TYPE] = hass.data[ - DATA_MQTT_DISCOVERY_REGISTRY_HOOKS - ] + mqtt_data: MqttData = hass.data[DATA_MQTT] + self._registry_hooks = mqtt_data.discovery_registry_hooks discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] if discovery_hash in self._registry_hooks: self._registry_hooks.pop(discovery_hash)() @@ -897,7 +920,7 @@ class MqttDiscoveryUpdate(Entity): def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" if self._discovery_data is not None: - discovery_hash: tuple = self._discovery_data[ATTR_DISCOVERY_HASH] + discovery_hash: tuple[str, str] = self._discovery_data[ATTR_DISCOVERY_HASH] if self.registry_entry is not None: self._registry_hooks[ discovery_hash diff --git a/tests/common.py b/tests/common.py index 89d1a1d9116..c97ee8a24f6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -369,7 +369,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): if isinstance(payload, str): payload = payload.encode("utf-8") msg = ReceiveMessage(topic, payload, qos, retain) - hass.data["mqtt"]._mqtt_handle_message(msg) + hass.data["mqtt"].client._mqtt_handle_message(msg) fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index e40397fd1d4..dba06e5cd5b 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -155,7 +155,7 @@ async def test_manual_config_set( assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}}) await hass.async_block_till_done() # do not try to reload - del hass.data["mqtt_reload_needed"] + hass.data["mqtt"].reload_needed = False assert len(mock_finish_setup.mock_calls) == 0 mock_try_connection.return_value = True diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index c625d0a21f9..a9ac66f8851 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1438,7 +1438,7 @@ async def test_clean_up_registry_monitoring( ): """Test registry monitoring hook is removed after a reload.""" await mqtt_mock_entry_no_yaml_config() - hooks: dict = hass.data[mqtt.const.DATA_MQTT_DISCOVERY_REGISTRY_HOOKS] + hooks: dict = hass.data["mqtt"].discovery_registry_hooks # discover an entity that is not enabled by default config1 = { "name": "sbfspot_12345", diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b76979cc990..46649bf703f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1776,14 +1776,14 @@ async def test_delayed_birth_message( await hass.async_block_till_done() mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"], - spec_set=hass.data["mqtt"], - wraps=hass.data["mqtt"], + return_value=hass.data["mqtt"].client, + spec_set=hass.data["mqtt"].client, + wraps=hass.data["mqtt"].client, ) mqtt_component_mock._mqttc = mqtt_client_mock - hass.data["mqtt"] = mqtt_component_mock - mqtt_mock = hass.data["mqtt"] + hass.data["mqtt"].client = mqtt_component_mock + mqtt_mock = hass.data["mqtt"].client mqtt_mock.reset_mock() async def wait_birth(topic, payload, qos): From bfcb3332275a7c1f26426601266690aa4b9396f9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 20 Sep 2022 17:32:10 +0200 Subject: [PATCH 201/231] Add status codes 23 and 26 to Xiaomi Miio vacuum (#78289) * Add status codes 23 and 26 * change status 26 --- homeassistant/components/xiaomi_miio/vacuum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 7df38109d16..20196f38919 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -73,6 +73,8 @@ STATE_CODE_TO_STATE = { 17: STATE_CLEANING, # "Zoned cleaning" 18: STATE_CLEANING, # "Segment cleaning" 22: STATE_DOCKED, # "Emptying the bin" on s7+ + 23: STATE_DOCKED, # "Washing the mop" on s7maxV + 26: STATE_RETURNING, # "Going to wash the mop" on s7maxV 100: STATE_DOCKED, # "Charging complete" 101: STATE_ERROR, # "Device offline" } From dea221b155da483fc1901d054e2aec38aabf618b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 18 Sep 2022 18:55:31 +0200 Subject: [PATCH 202/231] Link manually added MQTT entities the the MQTT config entry (#78547) Co-authored-by: Erik --- homeassistant/components/mqtt/__init__.py | 17 ++++- .../components/mqtt/alarm_control_panel.py | 4 -- .../components/mqtt/binary_sensor.py | 4 -- homeassistant/components/mqtt/button.py | 4 -- homeassistant/components/mqtt/camera.py | 4 -- homeassistant/components/mqtt/climate.py | 4 -- homeassistant/components/mqtt/cover.py | 4 -- homeassistant/components/mqtt/fan.py | 4 -- homeassistant/components/mqtt/humidifier.py | 4 -- .../components/mqtt/light/__init__.py | 4 -- homeassistant/components/mqtt/lock.py | 4 -- homeassistant/components/mqtt/mixins.py | 57 ++++++++-------- homeassistant/components/mqtt/number.py | 4 -- homeassistant/components/mqtt/scene.py | 4 -- homeassistant/components/mqtt/select.py | 4 -- homeassistant/components/mqtt/sensor.py | 12 ++-- homeassistant/components/mqtt/siren.py | 4 -- homeassistant/components/mqtt/switch.py | 4 -- .../components/mqtt/vacuum/__init__.py | 9 +-- tests/components/mqtt/test_init.py | 67 +++++++++++++++++++ 20 files changed, 117 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 315f116ed92..c14266e296f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -30,6 +30,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.reload import ( async_integration_yaml_config, async_reload_integration_platforms, @@ -75,7 +76,7 @@ from .const import ( # noqa: F401 PLATFORMS, RELOADABLE_PLATFORMS, ) -from .mixins import MqttData, async_discover_yaml_entities +from .mixins import MqttData from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -422,13 +423,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) # Reload the modern yaml platforms + mqtt_platforms = async_get_platforms(hass, DOMAIN) + tasks = [ + entity.async_remove() + for mqtt_platform in mqtt_platforms + for entity in mqtt_platform.entities.values() + if not entity._discovery_data # type: ignore[attr-defined] # pylint: disable=protected-access + if mqtt_platform.config_entry + and mqtt_platform.domain in RELOADABLE_PLATFORMS + ] + await asyncio.gather(*tasks) + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} mqtt_data.updated_config = config_yaml.get(DOMAIN, {}) await asyncio.gather( *( [ - async_discover_yaml_entities(hass, component) + mqtt_data.reload_handlers[component]() for component in RELOADABLE_PLATFORMS + if component in mqtt_data.reload_handlers ] ) ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index cf7262f9468..c3502cd8e64 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -44,7 +44,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -146,9 +145,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, alarm.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index cffb2fd8300..32608e86f53 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -42,7 +42,6 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -102,9 +101,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, binary_sensor.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 0881b963b04..a14bf87c3be 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -25,7 +25,6 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -81,9 +80,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, button.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 61c87e86888..f6039251882 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -23,7 +23,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -105,9 +104,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, camera.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f39d3857ec2..7226d1f8d1f 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -50,7 +50,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -350,9 +349,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, climate.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index fd96fe524d9..1f5d26c3a78 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -46,7 +46,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -242,9 +241,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, cover.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index fab748d2bfc..584df08e7d7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -50,7 +50,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -241,9 +240,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, fan.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 3a1271ea2c9..837bbb8b909 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -46,7 +46,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -187,9 +186,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, humidifier.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 76c2980e63b..b7d52919d5e 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -111,9 +110,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, light.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 4910eafae75..dca02f909dc 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -28,7 +28,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -102,9 +101,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, lock.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index a16394667d8..477be399e26 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -39,7 +39,6 @@ from homeassistant.core import ( from homeassistant.helpers import ( config_validation as cv, device_registry as dr, - discovery, entity_registry as er, ) from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -286,6 +285,9 @@ class MqttData: last_discovery: float = 0.0 reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) reload_entry: bool = False + reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( + default_factory=dict + ) reload_needed: bool = False subscriptions_to_restore: list[Subscription] = field(default_factory=list) updated_config: ConfigType = field(default_factory=dict) @@ -305,30 +307,6 @@ class SetupEntity(Protocol): """Define setup_entities type.""" -async def async_discover_yaml_entities( - hass: HomeAssistant, platform_domain: str -) -> None: - """Discover entities for a platform.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] - if mqtt_data.updated_config: - # The platform has been reloaded - config_yaml = mqtt_data.updated_config - else: - config_yaml = mqtt_data.config or {} - if not config_yaml: - return - if platform_domain not in config_yaml: - return - await asyncio.gather( - *( - discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) - for config in await async_get_platform_config_from_yaml( - hass, platform_domain, config_yaml - ) - ) - ) - - async def async_get_platform_config_from_yaml( hass: HomeAssistant, platform_domain: str, @@ -350,7 +328,7 @@ async def async_setup_entry_helper( hass: HomeAssistant, domain: str, async_setup: partial[Coroutine[HomeAssistant, str, None]], - schema: vol.Schema, + discovery_schema: vol.Schema, ) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" mqtt_data: MqttData = hass.data[DATA_MQTT] @@ -367,7 +345,7 @@ async def async_setup_entry_helper( return discovery_data = discovery_payload.discovery_data try: - config = schema(discovery_payload) + config = discovery_schema(discovery_payload) await async_setup(config, discovery_data=discovery_data) except Exception: discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] @@ -383,6 +361,31 @@ async def async_setup_entry_helper( ) ) + async def _async_setup_entities() -> None: + """Set up MQTT items from configuration.yaml.""" + mqtt_data: MqttData = hass.data[DATA_MQTT] + if mqtt_data.updated_config: + # The platform has been reloaded + config_yaml = mqtt_data.updated_config + else: + config_yaml = mqtt_data.config or {} + if not config_yaml: + return + if domain not in config_yaml: + return + await asyncio.gather( + *[ + async_setup(config) + for config in await async_get_platform_config_from_yaml( + hass, domain, config_yaml + ) + ] + ) + + # discover manual configured MQTT items + mqtt_data.reload_handlers[domain] = _async_setup_entities + await _async_setup_entities() + async def async_setup_platform_helper( hass: HomeAssistant, diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index eeac406f668..09f9d122b98 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -44,7 +44,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -138,9 +137,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, number.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 62de54505eb..861bdd14f6e 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -22,7 +22,6 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -78,9 +77,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, scene.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index ec88b1732d4..a6de0495690 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -30,7 +30,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -93,9 +92,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, select.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4c04d6176f1..e95979bbaca 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,7 +1,7 @@ """Support for MQTT sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import functools import logging @@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, callback 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 -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import dt as dt_util from . import subscription @@ -41,7 +41,6 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -146,9 +145,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, sensor.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -346,7 +342,7 @@ class MqttSensor(MqttEntity, RestoreSensor): self.async_write_ha_state() @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit this state is expressed in.""" return self._config.get(CONF_UNIT_OF_MEASUREMENT) @@ -356,7 +352,7 @@ class MqttSensor(MqttEntity, RestoreSensor): return self._config[CONF_FORCE_UPDATE] @property - def native_value(self): + def native_value(self) -> StateType | datetime: """Return the state of the entity.""" return self._state diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 5ed76fd6330..ebb08919789 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -51,7 +51,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -142,9 +141,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, siren.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index b5c7ab13dfc..af16b14bea1 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -42,7 +42,6 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_discover_yaml_entities, async_setup_entry_helper, async_setup_platform_helper, warn_for_legacy_schema, @@ -101,9 +100,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, switch.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index cdd14e6d8e3..abab55c632c 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -11,11 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from ..mixins import ( - async_discover_yaml_entities, - async_setup_entry_helper, - async_setup_platform_helper, -) +from ..mixins import async_setup_entry_helper, async_setup_platform_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import ( DISCOVERY_SCHEMA_LEGACY, @@ -90,9 +86,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await async_discover_yaml_entities(hass, vacuum.DOMAIN) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 46649bf703f..90df45b65a1 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -29,11 +29,13 @@ from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, template from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from .test_common import ( help_test_entry_reload_with_new_config, + help_test_reload_with_config, help_test_setup_manual_entity_from_yaml, ) @@ -2986,3 +2988,68 @@ async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog) "MQTT config entry: {'protocol'}. Add them to configuration.yaml if they " "are needed" ) in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_link_config_entry(hass, tmp_path, caplog): + """Test manual and dynamically setup entities are linked to the config entry.""" + config_manual = { + "mqtt": { + "light": [ + { + "name": "test_manual", + "unique_id": "test_manual_unique_id123", + "command_topic": "test-topic_manual", + } + ] + } + } + config_discovery = { + "name": "test_discovery", + "unique_id": "test_discovery_unique456", + "command_topic": "test-topic_discovery", + } + + # set up manual item + await help_test_setup_manual_entity_from_yaml(hass, config_manual) + + # set up item through discovery + async_fire_mqtt_message( + hass, "homeassistant/light/bla/config", json.dumps(config_discovery) + ) + await hass.async_block_till_done() + + assert hass.states.get("light.test_manual") is not None + assert hass.states.get("light.test_discovery") is not None + entity_names = ["test_manual", "test_discovery"] + + # Check if both entities were linked to the MQTT config entry + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + mqtt_platforms = async_get_platforms(hass, mqtt.DOMAIN) + + def _check_entities(): + entities = [] + for mqtt_platform in mqtt_platforms: + assert mqtt_platform.config_entry is mqtt_config_entry + entities += (entity for entity in mqtt_platform.entities.values()) + + for entity in entities: + assert entity.name in entity_names + return len(entities) + + assert _check_entities() == 2 + + # reload entry and assert again + await help_test_entry_reload_with_new_config(hass, tmp_path, config_manual) + # manual set up item should remain + assert _check_entities() == 1 + # set up item through discovery + async_fire_mqtt_message( + hass, "homeassistant/light/bla/config", json.dumps(config_discovery) + ) + await hass.async_block_till_done() + assert _check_entities() == 2 + + # reload manual configured items and assert again + await help_test_reload_with_config(hass, caplog, tmp_path, config_manual) + assert _check_entities() == 2 From 91398b6a75ac4791016797418ee676c88b329cff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Sep 2022 16:36:50 -0500 Subject: [PATCH 203/231] Drop PARALLEL_UPDATES from switchbot (#78713) --- homeassistant/components/switchbot/binary_sensor.py | 2 +- homeassistant/components/switchbot/cover.py | 2 +- homeassistant/components/switchbot/light.py | 2 +- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 2 +- homeassistant/components/switchbot/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index bf071d64a2d..a5378028264 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -15,7 +15,7 @@ from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "calibration": BinarySensorEntityDescription( diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index df716be6ff3..696c9455f28 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -24,7 +24,7 @@ from .entity import SwitchbotEntity # Initialize the logger _LOGGER = logging.getLogger(__name__) -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 async def async_setup_entry( diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py index e55f5fff9b1..0b4f748f1b2 100644 --- a/homeassistant/components/switchbot/light.py +++ b/homeassistant/components/switchbot/light.py @@ -29,7 +29,7 @@ SWITCHBOT_COLOR_MODE_TO_HASS = { SwitchBotColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP, } -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 async def async_setup_entry( diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index a321c964edc..bb670cc72d3 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.9"], + "requirements": ["PySwitchbot==0.19.10"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 9658c1ed9c8..e435e71efbd 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -21,7 +21,7 @@ from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 SENSOR_TYPES: dict[str, SensorEntityDescription] = { "rssi": SensorEntityDescription( diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index d524a7100f0..c4bbc2af1e0 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -19,7 +19,7 @@ from .entity import SwitchbotEntity # Initialize the logger _LOGGER = logging.getLogger(__name__) -PARALLEL_UPDATES = 1 +PARALLEL_UPDATES = 0 async def async_setup_entry( diff --git a/requirements_all.txt b/requirements_all.txt index 91a8c24ad1d..15662fa01f4 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.9 +PySwitchbot==0.19.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c0e0039851..b9dc80104b8 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.9 +PySwitchbot==0.19.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 8c9e0a8239bd86a46c1bb952ca664adc174e0912 Mon Sep 17 00:00:00 2001 From: Nico Date: Mon, 19 Sep 2022 12:59:09 +0200 Subject: [PATCH 204/231] Bump aioimaplib to 1.0.1 (#78738) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index f4bbadfa6ac..36004113351 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==1.0.0"], + "requirements": ["aioimaplib==1.0.1"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["aioimaplib"] diff --git a/requirements_all.txt b/requirements_all.txt index 15662fa01f4..e34c34e3ada 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aiohttp_cors==0.7.0 aiohue==4.5.0 # homeassistant.components.imap -aioimaplib==1.0.0 +aioimaplib==1.0.1 # homeassistant.components.apache_kafka aiokafka==0.7.2 From a688b4c5818fb2bef6130af4e17cc31e2cc94746 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 21 Sep 2022 11:34:04 -0600 Subject: [PATCH 205/231] Fix bug wherein RainMachine services use the wrong controller (#78780) --- .../components/rainmachine/__init__.py | 77 +++++++++++++------ 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 756dc9b958d..4d19dbc7bfc 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import timedelta -from functools import partial +from functools import partial, wraps from typing import Any from regenmaschine import Client @@ -22,7 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -152,9 +153,9 @@ class RainMachineData: @callback -def async_get_controller_for_service_call( +def async_get_entry_for_service_call( hass: HomeAssistant, call: ServiceCall -) -> Controller: +) -> ConfigEntry: """Get the controller related to a service call (by device ID).""" device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) @@ -166,8 +167,7 @@ def async_get_controller_for_service_call( if (entry := hass.config_entries.async_get_entry(entry_id)) is None: continue if entry.domain == DOMAIN: - data: RainMachineData = hass.data[DOMAIN][entry_id] - return data.controller + return entry raise ValueError(f"No controller for device ID: {device_id}") @@ -288,15 +288,42 @@ async def async_setup_entry( # noqa: C901 entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - async def async_pause_watering(call: ServiceCall) -> None: - """Pause watering for a set number of seconds.""" - controller = async_get_controller_for_service_call(hass, call) - await controller.watering.pause_all(call.data[CONF_SECONDS]) - await async_update_programs_and_zones(hass, entry) + def call_with_controller(update_programs_and_zones: bool = True) -> Callable: + """Hydrate a service call with the appropriate controller.""" - async def async_push_weather_data(call: ServiceCall) -> None: + def decorator(func: Callable) -> Callable[..., Awaitable]: + """Define the decorator.""" + + @wraps(func) + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + entry = async_get_entry_for_service_call(hass, call) + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + try: + await func(call, data.controller) + except RainMachineError as err: + raise HomeAssistantError( + f"Error while executing {func.__name__}: {err}" + ) from err + + if update_programs_and_zones: + await async_update_programs_and_zones(hass, entry) + + return wrapper + + return decorator + + @call_with_controller() + async def async_pause_watering(call: ServiceCall, controller: Controller) -> None: + """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_weather_data( + call: ServiceCall, controller: Controller + ) -> None: """Push weather data to the device.""" - controller = async_get_controller_for_service_call(hass, call) await controller.parsers.post_data( { CONF_WEATHER: [ @@ -309,9 +336,11 @@ async def async_setup_entry( # noqa: C901 } ) - async def async_restrict_watering(call: ServiceCall) -> None: + @call_with_controller() + async def async_restrict_watering( + call: ServiceCall, controller: Controller + ) -> None: """Restrict watering for a time period.""" - controller = async_get_controller_for_service_call(hass, call) duration = call.data[CONF_DURATION] await controller.restrictions.set_universal( { @@ -319,30 +348,28 @@ async def async_setup_entry( # noqa: C901 "rainDelayDuration": duration.total_seconds(), }, ) - await async_update_programs_and_zones(hass, entry) - async def async_stop_all(call: ServiceCall) -> None: + @call_with_controller() + async def async_stop_all(call: ServiceCall, controller: Controller) -> None: """Stop all watering.""" - controller = async_get_controller_for_service_call(hass, call) await controller.watering.stop_all() - await async_update_programs_and_zones(hass, entry) - async def async_unpause_watering(call: ServiceCall) -> None: + @call_with_controller() + async def async_unpause_watering(call: ServiceCall, controller: Controller) -> None: """Unpause watering.""" - controller = async_get_controller_for_service_call(hass, call) await controller.watering.unpause_all() - await async_update_programs_and_zones(hass, entry) - async def async_unrestrict_watering(call: ServiceCall) -> None: + @call_with_controller() + async def async_unrestrict_watering( + call: ServiceCall, controller: Controller + ) -> None: """Unrestrict watering.""" - controller = async_get_controller_for_service_call(hass, call) await controller.restrictions.set_universal( { "rainDelayStartTime": round(as_timestamp(utcnow())), "rainDelayDuration": 0, }, ) - await async_update_programs_and_zones(hass, entry) for service_name, schema, method in ( ( From fac2a46781b288148b7273d10524c0eb68c7fc49 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 19 Sep 2022 13:15:32 -0600 Subject: [PATCH 206/231] Guard Guardian switches from redundant on/off calls (#78791) --- homeassistant/components/guardian/switch.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 4e100ce4fe4..870dd721843 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -92,7 +92,10 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): ) async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the valve off (closed).""" + """Turn the switch off.""" + if not self._attr_is_on: + return + try: async with self._client: await self._client.valve.close() @@ -103,7 +106,10 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the valve on (open).""" + """Turn the switch on.""" + if self._attr_is_on: + return + try: async with self._client: await self._client.valve.open() From 48c6fbf22ea6afae9a6eed71319d5e8ff2cf5f07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Sep 2022 19:59:07 -0500 Subject: [PATCH 207/231] Bump dbus-fast to 1.5.1 (#78802) --- 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 1b1ec016e82..413ceb77e39 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -9,7 +9,7 @@ "bleak-retry-connector==1.17.1", "bluetooth-adapters==0.4.1", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.4.0" + "dbus-fast==1.5.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ee61afca23..8a0ee6c3205 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==37.0.4 -dbus-fast==1.4.0 +dbus-fast==1.5.1 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index e34c34e3ada..24ef4d6c7eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -538,7 +538,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.4.0 +dbus-fast==1.5.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9dc80104b8..da0d14f522f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -415,7 +415,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.4.0 +dbus-fast==1.5.1 # homeassistant.components.debugpy debugpy==1.6.3 From 829777a2114238fbc48d1bce6ec62bc72b5d08dd Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 20 Sep 2022 13:57:54 +0200 Subject: [PATCH 208/231] If brightness is not available, don't set a value (#78827) --- homeassistant/components/google_assistant/trait.py | 2 -- tests/components/google_assistant/test_smart_home.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 4f2971c01fa..a05e8ebe4aa 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -260,8 +260,6 @@ class BrightnessTrait(_Trait): brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS) if brightness is not None: response["brightness"] = round(100 * (brightness / 255)) - else: - response["brightness"] = 0 return response diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 684a6db2640..a7b061f2ec8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -381,7 +381,7 @@ async def test_query_message(hass): "payload": { "devices": { "light.non_existing": {"online": False}, - "light.demo_light": {"on": False, "online": True, "brightness": 0}, + "light.demo_light": {"on": False, "online": True}, "light.another_light": { "on": True, "online": True, @@ -725,7 +725,6 @@ async def test_execute_times_out(hass, report_state, on, brightness, value): "states": { "on": on, "online": True, - "brightness": brightness, }, }, { From 6704efd1ef3525b28400b4417c07b2209a548b8e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 20 Sep 2022 20:24:39 +0200 Subject: [PATCH 209/231] Pin Python patch versions [ci] (#78830) --- .github/workflows/ci.yaml | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 80c6cbac34b..350ba9336a2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,7 +23,8 @@ env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 HA_SHORT_VERSION: 2022.9 - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: 3.9.14 + ALL_PYTHON_VERSIONS: "['3.9.14', '3.10.7']" PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache SQLALCHEMY_WARN_20: 1 @@ -46,6 +47,7 @@ jobs: pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }} python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }} requirements: ${{ steps.core.outputs.requirements }} + python_versions: ${{ steps.info.outputs.python_versions }} test_full_suite: ${{ steps.info.outputs.test_full_suite }} test_group_count: ${{ steps.info.outputs.test_group_count }} test_groups: ${{ steps.info.outputs.test_groups }} @@ -143,6 +145,8 @@ jobs: fi # Output & sent to GitHub Actions + echo "python_versions: ${ALL_PYTHON_VERSIONS}" + echo "::set-output name=python_versions::${ALL_PYTHON_VERSIONS}" echo "test_full_suite: ${test_full_suite}" echo "::set-output name=test_full_suite::${test_full_suite}" echo "integrations_glob: ${integrations_glob}" @@ -463,7 +467,7 @@ jobs: timeout-minutes: 60 strategy: matrix: - python-version: ["3.9", "3.10"] + python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -483,7 +487,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' @@ -491,10 +495,10 @@ jobs: with: path: ${{ env.PIP_CACHE }} key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-pip-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -541,7 +545,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ 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' @@ -573,7 +577,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ 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' @@ -606,7 +610,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ 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' @@ -650,7 +654,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ + ${{ 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' @@ -682,7 +686,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10"] + python-version: ${{ fromJson(needs.info.outputs.python_versions) }} name: Run pip check ${{ matrix.python-version }} steps: - name: Check out code from GitHub @@ -698,7 +702,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ + ${{ 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' @@ -729,7 +733,7 @@ jobs: fail-fast: false matrix: group: ${{ fromJson(needs.info.outputs.test_groups) }} - python-version: ["3.9", "3.10"] + python-version: ${{ fromJson(needs.info.outputs.python_versions) }} name: >- Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) steps: @@ -751,7 +755,7 @@ jobs: uses: actions/cache@v3.0.8 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ + 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' From 8f21e7775bacd9230662360acb9e4f79f698ca70 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 21 Sep 2022 16:41:01 +0100 Subject: [PATCH 210/231] Fix parsing Eve Energy characteristic data (#78880) --- 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 d9e5bfb854b..7ecd54e0a79 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==1.5.9"], + "requirements": ["aiohomekit==1.5.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 24ef4d6c7eb..9b6582cb501 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==1.5.9 +aiohomekit==1.5.12 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da0d14f522f..5d4b6c7d223 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==1.5.9 +aiohomekit==1.5.12 # homeassistant.components.emulated_hue # homeassistant.components.http From 72769130f97c042d820398319d34a3e733e68f94 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:27:05 +0200 Subject: [PATCH 211/231] Check Surveillance Station permissions during setup of Synology DSM integration (#78884) --- homeassistant/components/synology_dsm/common.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 82f2c214804..019108c3230 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -91,6 +91,16 @@ class SynoApi: self._with_surveillance_station = bool( self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY) ) + if self._with_surveillance_station: + try: + self.dsm.surveillance_station.update() + except SYNOLOGY_CONNECTION_EXCEPTIONS: + self._with_surveillance_station = False + self.dsm.reset(SynoSurveillanceStation.API_KEY) + LOGGER.info( + "Surveillance Station found, but disabled due to missing user permissions" + ) + LOGGER.debug( "State of Surveillance_station during setup of '%s': %s", self._entry.unique_id, From 5c294550e8c96d636ff22f4206c23de05b13bdb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 11:03:50 -1000 Subject: [PATCH 212/231] Handle default RSSI values from bleak in bluetooth (#78908) --- homeassistant/components/bluetooth/manager.py | 3 +- tests/components/bluetooth/test_manager.py | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 014da818e12..9f6b0bbe3ed 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -60,6 +60,7 @@ APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE} RSSI_SWITCH_THRESHOLD = 6 +NO_RSSI_VALUE = -1000 _LOGGER = logging.getLogger(__name__) @@ -83,7 +84,7 @@ def _prefer_previous_adv( STALE_ADVERTISEMENT_SECONDS, ) return False - if new.device.rssi - RSSI_SWITCH_THRESHOLD > old.device.rssi: + 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( diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 9ce5985318b..28f870e3a35 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -116,6 +116,62 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): ) +async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): + """Test switching adapters based on zero rssi.""" + + 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=[] + ) + inject_advertisement_with_source( + hass, switchbot_device_no_rssi, switchbot_adv_no_rssi, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + 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=[] + ) + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci1" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_no_rssi, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + # 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=[] + ) + + inject_advertisement_with_source( + hass, switchbot_device_similar_signal, switchbot_adv_similar_signal, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): """Test switching adapters based on the previous advertisement being stale.""" From 68fa40c0fab9f3965de756460532f8f7f4e3a42b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Sep 2022 22:31:14 -0400 Subject: [PATCH 213/231] Disable force update Netatmo (#78913) --- homeassistant/components/netatmo/data_handler.py | 4 ++-- tests/components/netatmo/test_camera.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 3a1ea73e311..50a3bed17ff 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -138,8 +138,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.""" diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 0e10ce92288..5b01668925f 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -328,6 +328,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): ) +@pytest.mark.skip async def test_camera_reconnect_webhook(hass, config_entry): """Test webhook event on camera reconnect.""" fake_post_hits = 0 From 103f4905194b8c84d272354504669d376501cdf6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Sep 2022 22:33:08 -0400 Subject: [PATCH 214/231] Bumped version to 2022.9.6 --- 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 c11f9a9e054..6c3d2a32ea3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __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 2d1213ea199..2f4d1e5315d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.5" +version = "2022.9.6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f3451858ef4a6921691b1cec0de33ebb6ab37ade Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 17 Sep 2022 17:27:22 +0200 Subject: [PATCH 215/231] Correct return typing for `catch_log_exception` (#78399) --- homeassistant/util/logging.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 76df4bb17b6..e493a3378fd 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -120,27 +120,30 @@ def log_exception(format_err: Callable[..., Any], *args: Any) -> None: def catch_log_exception( func: Callable[..., Coroutine[Any, Any, Any]], format_err: Callable[..., Any] ) -> Callable[..., Coroutine[Any, Any, None]]: - """Overload for Callables that return a Coroutine.""" + """Overload for Coroutine that returns a Coroutine.""" @overload def catch_log_exception( func: Callable[..., Any], format_err: Callable[..., Any] -) -> Callable[..., None | Coroutine[Any, Any, None]]: - """Overload for Callables that return Any.""" +) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: + """Overload for a callback that returns a callback.""" def catch_log_exception( func: Callable[..., Any], format_err: Callable[..., Any] -) -> Callable[..., None | Coroutine[Any, Any, None]]: - """Decorate a callback to catch and log exceptions.""" +) -> Callable[..., None] | Callable[..., Coroutine[Any, Any, None]]: + """Decorate a function func to catch and log exceptions. + If func is a coroutine function, a coroutine function will be returned. + If func is a callback, a callback will be returned. + """ # Check for partials to properly determine if coroutine function check_func = func while isinstance(check_func, partial): check_func = check_func.func - wrapper_func: Callable[..., None | Coroutine[Any, Any, None]] + wrapper_func: Callable[..., None] | Callable[..., Coroutine[Any, Any, None]] if asyncio.iscoroutinefunction(check_func): async_func = cast(Callable[..., Coroutine[Any, Any, None]], func) From b51dc0884eed61edf524557bcc02a8dfc6611ca3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 08:02:54 -1000 Subject: [PATCH 216/231] Fix samsungtv to abort when ATTR_UPNP_MANUFACTURER is missing (#78895) --- .../components/samsungtv/config_flow.py | 2 +- .../components/samsungtv/test_config_flow.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 099f0afbae8..d7ad62bdf7a 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -458,7 +458,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) if hostname := urlparse(discovery_info.ssdp_location or "").hostname: self._host = hostname - self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] + self._manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER) self._abort_if_manufacturer_is_not_samsung() # Set defaults, in case they cannot be extracted from device_info diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 0b49a064a19..30bb1052702 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -100,6 +100,15 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", }, ) +MOCK_SSDP_DATA_NO_MANUFACTURER = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="https://fake_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", + }, +) MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", @@ -521,6 +530,18 @@ async def test_ssdp(hass: HomeAssistant) -> None: assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" +@pytest.mark.usefixtures("remote", "rest_api_failing") +async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None: + """Test starting a flow from discovery when the manufacturer data is missing.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_NO_MANUFACTURER, + ) + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + @pytest.mark.parametrize( "data", [MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST] ) From d44ff16f9d73e7bbe9f534bff95b604c47f28b3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 Sep 2022 08:03:05 -1000 Subject: [PATCH 217/231] Handle timeout fetching bond token in config flow (#78896) --- homeassistant/components/bond/config_flow.py | 6 +++- tests/components/bond/test_config_flow.py | 38 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 09386c3587d..da8e6781bfa 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Bond integration.""" from __future__ import annotations +import asyncio import contextlib from http import HTTPStatus import logging @@ -83,7 +84,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): instead ask them to manually enter the token. """ host = self._discovered[CONF_HOST] - if not (token := await async_get_token(self.hass, host)): + try: + if not (token := await async_get_token(self.hass, host)): + return + except asyncio.TimeoutError: return self._discovered[CONF_ACCESS_TOKEN] = token diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 15aa643abaf..a54360283e6 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Bond config flow.""" from __future__ import annotations +import asyncio from http import HTTPStatus from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -268,6 +269,43 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_form_token_times_out(hass: core.HomeAssistant): + """Test we get the discovery form and we handle the token request timeout.""" + + with patch_bond_version(), patch_bond_token(side_effect=asyncio.TimeoutError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="test-host", + addresses=["test-host"], + hostname="mock_hostname", + name="ZXXX12345.some-other-tail-info", + port=None, + properties={}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {} + + with patch_bond_version(), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ACCESS_TOKEN: "test-token"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "bond-name" + assert result2["data"] == { + CONF_HOST: "test-host", + CONF_ACCESS_TOKEN: "test-token", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): """Test we get the discovery form when we can get the token.""" From 7be5fde8d635d6cb6372fc771b2c2e3f15d001b9 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Thu, 22 Sep 2022 08:53:29 +0200 Subject: [PATCH 218/231] Bump bimmer_connected to 0.10.4 (#78910) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index f540176a837..98b6861fd49 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.10.2"], + "requirements": ["bimmer_connected==0.10.4"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 9b6582cb501..608c558b7ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -402,7 +402,7 @@ beautifulsoup4==4.11.1 bellows==0.33.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.2 +bimmer_connected==0.10.4 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d4b6c7d223..ffca40fe7da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ beautifulsoup4==4.11.1 bellows==0.33.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.2 +bimmer_connected==0.10.4 # homeassistant.components.bluetooth bleak-retry-connector==1.17.1 From 32cea6b95d10ac21f53688809e45a28cea353e56 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 23 Sep 2022 15:23:33 +0100 Subject: [PATCH 219/231] Bump pyipma to 3.0.5 (#78936) * fix #78928 and review of #78332 * address comment --- homeassistant/components/ipma/__init__.py | 30 +++++++++------------ homeassistant/components/ipma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 315362247a2..eec16a0c811 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,4 +1,5 @@ """Component for the Portuguese weather service - IPMA.""" +import asyncio import logging import async_timeout @@ -22,36 +23,31 @@ PLATFORMS = [Platform.WEATHER] _LOGGER = logging.getLogger(__name__) -async def async_get_api(hass): - """Get the pyipma api object.""" - websession = async_get_clientsession(hass) - return IPMA_API(websession) - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up IPMA station as config entry.""" latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] - api = await async_get_api(hass) + api = IPMA_API(async_get_clientsession(hass)) + try: async with async_timeout.timeout(30): location = await Location.get(api, float(latitude), float(longitude)) - - _LOGGER.debug( - "Initializing for coordinates %s, %s -> station %s (%d, %d)", - latitude, - longitude, - location.station, - location.id_station, - location.global_id_local, - ) - except IPMAException as err: + except (IPMAException, asyncio.TimeoutError) as err: raise ConfigEntryNotReady( f"Could not get location for ({latitude},{longitude})" ) from err + _LOGGER.debug( + "Initializing for coordinates %s, %s -> station %s (%d, %d)", + latitude, + longitude, + location.station, + location.id_station, + location.global_id_local, + ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {DATA_API: api, DATA_LOCATION: location} diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 23558600373..36dca71e957 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==3.0.4"], + "requirements": ["pyipma==3.0.5"], "codeowners": ["@dgomes", "@abmantis"], "iot_class": "cloud_polling", "loggers": ["geopy", "pyipma"] diff --git a/requirements_all.txt b/requirements_all.txt index 608c558b7ef..b809ca1a00f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1614,7 +1614,7 @@ pyinsteon==1.2.0 pyintesishome==1.8.0 # homeassistant.components.ipma -pyipma==3.0.4 +pyipma==3.0.5 # homeassistant.components.ipp pyipp==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffca40fe7da..f2b17378d31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1127,7 +1127,7 @@ pyicloud==1.0.0 pyinsteon==1.2.0 # homeassistant.components.ipma -pyipma==3.0.4 +pyipma==3.0.5 # homeassistant.components.ipp pyipp==0.11.0 From 12903a14c5f5e6ac044cb9f7e0eb55d4a314249b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 22 Sep 2022 18:17:53 +0200 Subject: [PATCH 220/231] Bump motionblinds to 0.6.13 (#78946) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 90ad330bd40..e6e4c50c7fe 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.12"], + "requirements": ["motionblinds==0.6.13"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index b809ca1a00f..07c76359465 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1079,7 +1079,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.12 +motionblinds==0.6.13 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2b17378d31..18d2edeebd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -778,7 +778,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.12 +motionblinds==0.6.13 # homeassistant.components.motioneye motioneye-client==0.3.12 From 60c78fd33f961f1142070274e269754aae11edad Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 25 Sep 2022 04:03:54 +0200 Subject: [PATCH 221/231] Set OWM default mode to hourly legacy API (#78951) --- homeassistant/components/openweathermap/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 836a56c70b2..5a370604e1f 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -86,7 +86,7 @@ FORECAST_MODES = [ FORECAST_MODE_ONECALL_HOURLY, FORECAST_MODE_ONECALL_DAILY, ] -DEFAULT_FORECAST_MODE = FORECAST_MODE_ONECALL_DAILY +DEFAULT_FORECAST_MODE = FORECAST_MODE_HOURLY LANGUAGES = [ "af", From 3b8f08270eef155931731925e1db044d2cf1f34f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Sep 2022 17:39:23 -1000 Subject: [PATCH 222/231] Bump yalexs to 1.2.2 (#78978) --- 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 c688aa1a775..3aef3f5960d 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.1"], + "requirements": ["yalexs==1.2.2"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 07c76359465..7db9483f389 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2551,7 +2551,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.1 +yalexs==1.2.2 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18d2edeebd9..fc57086753c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1755,7 +1755,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==1.9.2 # homeassistant.components.august -yalexs==1.2.1 +yalexs==1.2.2 # homeassistant.components.yeelight yeelight==0.7.10 From eb80062b2610e868a5b8280b3633494777afa854 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 23 Sep 2022 23:11:06 +0200 Subject: [PATCH 223/231] Fix velbus matching ignored entries in config flow (#78999) * Fix bug #fix78826 * start using async_abort_entries_match * fix/rewrite tests --- .../components/velbus/config_flow.py | 24 +--- tests/components/velbus/test_config_flow.py | 110 ++++++++++-------- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 993146d375c..32f1f3a500d 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -10,21 +10,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import usb from homeassistant.const import CONF_NAME, CONF_PORT -from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.util import slugify from .const import DOMAIN -@callback -def velbus_entries(hass: HomeAssistant) -> set[str]: - """Return connections for Velbus domain.""" - return { - entry.data[CONF_PORT] for entry in hass.config_entries.async_entries(DOMAIN) - } - - class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -51,10 +42,6 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False return True - def _prt_in_configuration_exists(self, prt: str) -> bool: - """Return True if port exists in configuration.""" - return prt in velbus_entries(self.hass) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -63,11 +50,9 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: name = slugify(user_input[CONF_NAME]) prt = user_input[CONF_PORT] - if not self._prt_in_configuration_exists(prt): - if await self._test_connection(prt): - return self._create_device(name, prt) - else: - self._errors[CONF_PORT] = "already_configured" + self._async_abort_entries_match({CONF_PORT: prt}) + if await self._test_connection(prt): + return self._create_device(name, prt) else: user_input = {} user_input[CONF_NAME] = "" @@ -93,8 +78,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): usb.get_serial_by_id, discovery_info.device ) # check if this device is not already configured - if self._prt_in_configuration_exists(dev_path): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_PORT: dev_path}) # check if we can make a valid velbus connection if not await self._test_connection(dev_path): return self.async_abort(reason="cannot_connect") diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 207f745e495..454290b3581 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -7,9 +7,8 @@ from velbusaio.exceptions import VelbusConnectionFailed from homeassistant import data_entry_flow from homeassistant.components import usb -from homeassistant.components.velbus import config_flow from homeassistant.components.velbus.const import DOMAIN -from homeassistant.config_entries import SOURCE_USB +from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant @@ -53,63 +52,76 @@ def mock_controller_connection_failed(): yield -def init_config_flow(hass: HomeAssistant): - """Init a configuration flow.""" - flow = config_flow.VelbusConfigFlow() - flow.hass = hass - return flow - - @pytest.mark.usefixtures("controller") async def test_user(hass: HomeAssistant): """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} + # simple user form + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "velbus_test_serial" - assert result["data"][CONF_PORT] == PORT_SERIAL + assert result + assert result.get("flow_id") + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("step_id") == "user" - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} + # try with a serial port + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "velbus_test_tcp" - assert result["data"][CONF_PORT] == PORT_TCP + assert result + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("title") == "velbus_test_serial" + data = result.get("data") + assert data[CONF_PORT] == PORT_SERIAL + + # try with a ip:port combination + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, + ) + assert result + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result.get("title") == "velbus_test_tcp" + data = result.get("data") + assert data[CONF_PORT] == PORT_TCP @pytest.mark.usefixtures("controller_connection_failed") async def test_user_fail(hass: HomeAssistant): """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {CONF_PORT: "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {CONF_PORT: "cannot_connect"} - result = await flow.async_step_user( - {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {CONF_PORT: "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {CONF_PORT: "cannot_connect"} @pytest.mark.usefixtures("config_entry") async def test_abort_if_already_setup(hass: HomeAssistant): """Test we abort if Velbus is already setup.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user({CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"port": "already_configured"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}, + ) + assert result + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "already_configured" @pytest.mark.usefixtures("controller") @@ -121,14 +133,16 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "discovery_confirm" + assert result + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("step_id") == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY # test an already configured discovery entry = MockConfigEntry( @@ -141,8 +155,9 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "already_configured" @pytest.mark.usefixtures("controller_connection_failed") @@ -154,5 +169,6 @@ async def test_flow_usb_failed(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "cannot_connect" + assert result + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "cannot_connect" From f2fe91dac1e84b13c785a1d000c6a2941a5757e3 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sat, 24 Sep 2022 02:43:03 +0200 Subject: [PATCH 224/231] Bumped boschshcpy 0.2.30 to 0.2.35 (#79017) Bumped to boschshcpy==0.2.35 --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index a5927162e50..df902601e75 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,7 +3,7 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.30"], + "requirements": ["boschshcpy==0.2.35"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], diff --git a/requirements_all.txt b/requirements_all.txt index 7db9483f389..94147ee7202 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -442,7 +442,7 @@ bluetooth-auto-recovery==0.3.3 bond-async==0.1.22 # homeassistant.components.bosch_shc -boschshcpy==0.2.30 +boschshcpy==0.2.35 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc57086753c..e9e55ba5b23 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -353,7 +353,7 @@ bluetooth-auto-recovery==0.3.3 bond-async==0.1.22 # homeassistant.components.bosch_shc -boschshcpy==0.2.30 +boschshcpy==0.2.35 # homeassistant.components.broadlink broadlink==0.18.2 From b55ce96a56c5bc5b2039ef5415e18ed2e0721ba6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 25 Sep 2022 04:03:21 +0200 Subject: [PATCH 225/231] Fix failing LaMetric pairing message during config flow (#79031) --- homeassistant/components/lametric/config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 4bb293b0a4d..59797e3caf1 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -13,6 +13,7 @@ from demetriek import ( Model, Notification, NotificationIconType, + NotificationPriority, NotificationSound, Simple, Sound, @@ -227,6 +228,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): await lametric.notify( notification=Notification( + priority=NotificationPriority.CRITICAL, icon_type=NotificationIconType.INFO, model=Model( cycles=2, From 44109a69222781af23c592faf63d4f967dc39e82 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 25 Sep 2022 04:03:03 +0200 Subject: [PATCH 226/231] Fix MQTT device_tracker generating unique id-s - regression on #78547 (#79033) --- .../mqtt/device_tracker/schema_discovery.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 105442b176e..907d424e8a4 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,7 +1,6 @@ """Support for tracking MQTT enabled devices identified through discovery.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -28,12 +27,7 @@ from .. import subscription from ..config import MQTT_RO_SCHEMA from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..mixins import ( - MQTT_ENTITY_COMMON_SCHEMA, - MqttEntity, - async_get_platform_config_from_yaml, - async_setup_entry_helper, -) +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper from ..models import MqttValueTemplate CONF_PAYLOAD_HOME = "payload_home" @@ -58,16 +52,6 @@ async def async_setup_entry_from_discovery( async_add_entities: AddEntitiesCallback, ) -> None: """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" - # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, device_tracker.DOMAIN - ) - ) - ) - # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) From f2860fd6b3ea459baa1dadce2344815d8f6e31a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 Sep 2022 16:02:17 -1000 Subject: [PATCH 227/231] Bump govee-ble to 0.19.0 (#79038) --- homeassistant/components/govee_ble/manifest.json | 12 +++++++++++- homeassistant/generated/bluetooth.py | 12 ++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 537ae9c7ed5..29af7502ded 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -12,6 +12,11 @@ "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 63391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 26589, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", @@ -32,6 +37,11 @@ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 43682, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 59970, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", @@ -58,7 +68,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.17.3"], + "requirements": ["govee-ble==0.19.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 22bb93d1bda..0d2e4cdca0f 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -56,6 +56,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", "connectable": False }, + { + "domain": "govee_ble", + "manufacturer_id": 63391, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "govee_ble", "manufacturer_id": 26589, @@ -80,6 +86,12 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": False }, + { + "domain": "govee_ble", + "manufacturer_id": 43682, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "govee_ble", "manufacturer_id": 59970, diff --git a/requirements_all.txt b/requirements_all.txt index 94147ee7202..70d1e467639 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -778,7 +778,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.3 +govee-ble==0.19.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9e55ba5b23..7e642ed926a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -579,7 +579,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.17.3 +govee-ble==0.19.0 # homeassistant.components.gree greeclimate==1.3.0 From a0f2571872199e133796326ece1bfea7ce3288af Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 26 Sep 2022 15:59:01 +0200 Subject: [PATCH 228/231] Always install requirements of after_dependencies (#79094) --- homeassistant/setup.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 8d5f9e52025..d85e4043505 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -357,11 +357,10 @@ async def async_process_deps_reqs( if failed_deps := await _async_process_dependencies(hass, config, integration): raise DependencyError(failed_deps) - if not hass.config.skip_pip and integration.requirements: - async with hass.timeout.async_freeze(integration.domain): - await requirements.async_get_integration_with_requirements( - hass, integration.domain - ) + async with hass.timeout.async_freeze(integration.domain): + await requirements.async_get_integration_with_requirements( + hass, integration.domain + ) processed.add(integration.domain) From 943cca3d238820f36e924e8a646050daab31f8af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Sep 2022 09:59:39 -0400 Subject: [PATCH 229/231] Bumped version to 2022.9.7 --- 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 6c3d2a32ea3..68ca7fa33ea 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __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 2f4d1e5315d..c0368421577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.9.6" +version = "2022.9.7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 217ea5e676c8cea69ee75d156f5239ffe1a0d5cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Sep 2022 10:31:26 -0400 Subject: [PATCH 230/231] Hassfest --- homeassistant/generated/bluetooth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 0d2e4cdca0f..6553f9bcfb6 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -60,7 +60,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "govee_ble", "manufacturer_id": 63391, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", - "connectable": False, + "connectable": False }, { "domain": "govee_ble", @@ -90,7 +90,7 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "govee_ble", "manufacturer_id": 43682, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, + "connectable": False }, { "domain": "govee_ble", From 04d4483998612bccea4133e3ff8ad6c60e43e2d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Sep 2022 15:19:06 -0400 Subject: [PATCH 231/231] Pin pyOpenSSL to 22.0.0 (#79066) --- 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 8a0ee6c3205..d74601cef13 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -132,3 +132,6 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 + +# pyopenssl 22.1.0 requires pycryptography > 38 and we have 37 +pyopenssl==22.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d0eb830f088..eec89d4210c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -142,6 +142,9 @@ iso4217!=1.10.20220401 # Pandas 1.4.4 has issues with wheels om armhf + Py3.10 pandas==1.4.3 + +# pyopenssl 22.1.0 requires pycryptography > 38 and we have 37 +pyopenssl==22.0.0 """ IGNORE_PRE_COMMIT_HOOK_ID = (