From e70af204eec1a67a293edbb23213a530bfeee7a2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Jan 2024 18:56:44 +0100 Subject: [PATCH 001/133] Enable strict typing for airthings_ble (#106815) --- .strict-typing | 1 + homeassistant/components/airthings_ble/__init__.py | 6 +++--- mypy.ini | 10 ++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index aa9c801fbf6..1bc3308f533 100644 --- a/.strict-typing +++ b/.strict-typing @@ -49,6 +49,7 @@ homeassistant.components.aftership.* homeassistant.components.air_quality.* homeassistant.components.airly.* homeassistant.components.airnow.* +homeassistant.components.airthings_ble.* homeassistant.components.airvisual.* homeassistant.components.airvisual_pro.* homeassistant.components.airzone.* diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index d7e6bddbcd4..c642ebf9563 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from airthings_ble import AirthingsBluetoothDeviceData +from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry @@ -37,13 +37,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Could not find Airthings device with address {address}" ) - async def _async_update_method(): + async def _async_update_method() -> AirthingsDevice: """Get data from Airthings BLE.""" ble_device = bluetooth.async_ble_device_from_address(hass, address) airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric) try: - data = await airthings.update_device(ble_device) + data = await airthings.update_device(ble_device) # type: ignore[arg-type] except Exception as err: raise UpdateFailed(f"Unable to fetch data: {err}") from err diff --git a/mypy.ini b/mypy.ini index e19c6c6fa92..e4546526722 100644 --- a/mypy.ini +++ b/mypy.ini @@ -250,6 +250,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.airthings_ble.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.airvisual.*] check_untyped_defs = true disallow_incomplete_defs = true From 5a0997bac0e9d8501a270cfb59245ae8ff0767dd Mon Sep 17 00:00:00 2001 From: Joe Neuman Date: Tue, 2 Jan 2024 15:19:00 -0800 Subject: [PATCH 002/133] Fix qBittorrent torrent count when empty (#106903) * Fix qbittorrent torrent cound when empty * lint fix * Change based on comment --- homeassistant/components/qbittorrent/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 9373aec8544..78e8ba59d44 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -165,6 +165,10 @@ def count_torrents_in_states( coordinator: QBittorrentDataCoordinator, states: list[str] ) -> int: """Count the number of torrents in specified states.""" + # When torrents are not in the returned data, there are none, return 0. + if "torrents" not in coordinator.data: + return 0 + if not states: return len(coordinator.data["torrents"]) From d87baba96f8fe228f35ca4ae86fd186651989ace Mon Sep 17 00:00:00 2001 From: Patrick Frazer Date: Wed, 3 Jan 2024 15:10:00 -0500 Subject: [PATCH 003/133] Bump dropmqttapi to 1.0.2 (#106978) --- homeassistant/components/drop_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/drop_connect/manifest.json b/homeassistant/components/drop_connect/manifest.json index f65c1848aff..5df34fce561 100644 --- a/homeassistant/components/drop_connect/manifest.json +++ b/homeassistant/components/drop_connect/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/drop_connect", "iot_class": "local_push", "mqtt": ["drop_connect/discovery/#"], - "requirements": ["dropmqttapi==1.0.1"] + "requirements": ["dropmqttapi==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 46b89f491a9..8b874569d66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -716,7 +716,7 @@ dovado==0.4.1 dremel3dpy==2.1.1 # homeassistant.components.drop_connect -dropmqttapi==1.0.1 +dropmqttapi==1.0.2 # homeassistant.components.dsmr dsmr-parser==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee1a9b2ac35..767511cc958 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -585,7 +585,7 @@ discovery30303==0.2.1 dremel3dpy==2.1.1 # homeassistant.components.drop_connect -dropmqttapi==1.0.1 +dropmqttapi==1.0.2 # homeassistant.components.dsmr dsmr-parser==1.3.1 From 1c94a94ba2a4c25e771aded846b15752a5c6320e Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Thu, 28 Dec 2023 21:59:56 +0100 Subject: [PATCH 004/133] bump openwebifpy to 4.0.3 (#106593) --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 7909db3b7c7..19a2cf863f9 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/enigma2", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.0.2"] + "requirements": ["openwebifpy==4.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b874569d66..88c2f0e8506 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1425,7 +1425,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.0.2 +openwebifpy==4.0.3 # homeassistant.components.luci openwrt-luci-rpc==1.1.16 From aef129afafbe2f9aa2f3e3c441af6b1490337a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Wed, 3 Jan 2024 21:08:58 +0100 Subject: [PATCH 005/133] Close stale connections (Airthings BLE) (#106748) Co-authored-by: J. Nick Koston --- homeassistant/components/airthings_ble/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index c642ebf9563..1d62442f14d 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice +from bleak_retry_connector import close_stale_connections_by_address from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry @@ -30,6 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: is_metric = hass.config.units is METRIC_SYSTEM assert address is not None + await close_stale_connections_by_address(address) + ble_device = bluetooth.async_ble_device_from_address(hass, address) if not ble_device: From ce5455fefc2a7efa83ede07fa4e7366aa60d584c Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Thu, 4 Jan 2024 01:40:59 +0100 Subject: [PATCH 006/133] Bump openwebifpy to 4.0.4 (#107000) --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 19a2cf863f9..42fbcb5b9bc 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/enigma2", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.0.3"] + "requirements": ["openwebifpy==4.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88c2f0e8506..11d12f195a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1425,7 +1425,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.0.3 +openwebifpy==4.0.4 # homeassistant.components.luci openwrt-luci-rpc==1.1.16 From 8c9875c3cc24a34807253669c3d6170aeb6ebe23 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 4 Jan 2024 01:45:59 +0100 Subject: [PATCH 007/133] Get Shelly RPC device `gen` from config entry data (#107019) Use gen from config entry data --- homeassistant/components/shelly/config_flow.py | 17 +++++++++-------- homeassistant/components/shelly/const.py | 2 ++ homeassistant/components/shelly/coordinator.py | 3 ++- homeassistant/components/shelly/utils.py | 5 +++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 68b0f1f8ccc..29daf050163 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -25,6 +25,7 @@ from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig from .const import ( CONF_BLE_SCANNER_MODE, + CONF_GEN, CONF_SLEEP_PERIOD, DOMAIN, LOGGER, @@ -84,7 +85,7 @@ async def validate_input( "title": rpc_device.name, CONF_SLEEP_PERIOD: sleep_period, "model": rpc_device.shelly.get("model"), - "gen": gen, + CONF_GEN: gen, } # Gen1 @@ -99,7 +100,7 @@ async def validate_input( "title": block_device.name, CONF_SLEEP_PERIOD: get_block_device_sleep_period(block_device.settings), "model": block_device.model, - "gen": gen, + CONF_GEN: gen, } @@ -153,7 +154,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): **user_input, CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], "model": device_info["model"], - "gen": device_info["gen"], + CONF_GEN: device_info[CONF_GEN], }, ) errors["base"] = "firmware_not_fully_provisioned" @@ -190,7 +191,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): CONF_HOST: self.host, CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], "model": device_info["model"], - "gen": device_info["gen"], + CONF_GEN: device_info[CONF_GEN], }, ) errors["base"] = "firmware_not_fully_provisioned" @@ -288,7 +289,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): "host": self.host, CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], "model": self.device_info["model"], - "gen": self.device_info["gen"], + CONF_GEN: self.device_info[CONF_GEN], }, ) self._set_confirm_only() @@ -321,7 +322,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") - if self.entry.data.get("gen", 1) != 1: + if self.entry.data.get(CONF_GEN, 1) != 1: user_input[CONF_USERNAME] = "admin" try: await validate_input(self.hass, host, info, user_input) @@ -334,7 +335,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_abort(reason="reauth_successful") - if self.entry.data.get("gen", 1) in BLOCK_GENERATIONS: + if self.entry.data.get(CONF_GEN, 1) in BLOCK_GENERATIONS: schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, @@ -363,7 +364,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" return ( - config_entry.data.get("gen") in RPC_GENERATIONS + config_entry.data.get(CONF_GEN) in RPC_GENERATIONS and not config_entry.data.get(CONF_SLEEP_PERIOD) and config_entry.data.get("model") != MODEL_WALL_DISPLAY ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index ca1c450c9fa..1e2c22691fb 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -214,3 +214,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = ( MODEL_MOTION_2, MODEL_VALVE, ) + +CONF_GEN = "gen" diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index a7659ecc392..77fa0bd2efd 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -33,6 +33,7 @@ from .const import ( ATTR_GENERATION, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, CONF_BLE_SCANNER_MODE, + CONF_GEN, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DOMAIN, @@ -135,7 +136,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): manufacturer="Shelly", model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), sw_version=self.sw_version, - hw_version=f"gen{self.device.gen} ({self.model})", + hw_version=f"gen{self.entry.data[CONF_GEN]} ({self.model})", configuration_url=f"http://{self.entry.data[CONF_HOST]}", ) self.device_id = device_entry.id diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a43d9cb0bcb..d40b22ca50a 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -34,6 +34,7 @@ from homeassistant.util.dt import utcnow from .const import ( BASIC_INPUTS_EVENTS_TYPES, CONF_COAP_PORT, + CONF_GEN, DEFAULT_COAP_PORT, DEVICES_WITHOUT_FIRMWARE_CHANGELOG, DOMAIN, @@ -281,7 +282,7 @@ def get_info_auth(info: dict[str, Any]) -> bool: def get_info_gen(info: dict[str, Any]) -> int: """Return the device generation from shelly info.""" - return int(info.get("gen", 1)) + return int(info.get(CONF_GEN, 1)) def get_model_name(info: dict[str, Any]) -> str: @@ -325,7 +326,7 @@ def get_rpc_entity_name( def get_device_entry_gen(entry: ConfigEntry) -> int: """Return the device generation from config entry.""" - return entry.data.get("gen", 1) + return entry.data.get(CONF_GEN, 1) def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: From 427077a4c9b52c3845381c356067b953c30b8ab7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Jan 2024 14:41:50 -1000 Subject: [PATCH 008/133] Fix missing backwards compatiblity layer for humidifier supported_features (#107026) fixes #107018 --- homeassistant/components/humidifier/__init__.py | 2 +- tests/components/humidifier/test_init.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 75d4f0fd225..0d8f2e29561 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -214,7 +214,7 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT if self.target_humidity is not None: data[ATTR_HUMIDITY] = self.target_humidity - if HumidifierEntityFeature.MODES in self.supported_features: + if HumidifierEntityFeature.MODES in self.supported_features_compat: data[ATTR_MODE] = self.mode return data diff --git a/tests/components/humidifier/test_init.py b/tests/components/humidifier/test_init.py index 45da5ba750f..24cf4b6d962 100644 --- a/tests/components/humidifier/test_init.py +++ b/tests/components/humidifier/test_init.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components import humidifier from homeassistant.components.humidifier import ( + ATTR_MODE, HumidifierEntity, HumidifierEntityFeature, ) @@ -75,6 +76,8 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> """Test deprecated supported features ints.""" class MockHumidifierEntity(HumidifierEntity): + _attr_mode = "mode1" + @property def supported_features(self) -> int: """Return supported features.""" @@ -89,3 +92,5 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> caplog.clear() assert entity.supported_features_compat is HumidifierEntityFeature(1) assert "is using deprecated supported features values" not in caplog.text + + assert entity.state_attributes[ATTR_MODE] == "mode1" From e2acc70128ec070a8d6f42e490ed2c23fad9ff78 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 4 Jan 2024 09:11:44 +0100 Subject: [PATCH 009/133] Use async_register in streamlabswater (#107060) --- homeassistant/components/streamlabswater/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 986b5de8049..82e8777a7e1 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) client.update_location(location_id, away_mode) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, schema=SET_AWAY_MODE_SCHEMA ) From 5529a85a2bdf60bdeaced2d0c95110c03865cdaa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 4 Jan 2024 09:13:34 +0100 Subject: [PATCH 010/133] Fix data access in streamlabs water (#107062) * Fix data access in streamlabs water * Fix data access in streamlabs water --- homeassistant/components/streamlabswater/coordinator.py | 2 +- homeassistant/components/streamlabswater/sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/streamlabswater/coordinator.py b/homeassistant/components/streamlabswater/coordinator.py index a11eced5a6e..dc57ae78810 100644 --- a/homeassistant/components/streamlabswater/coordinator.py +++ b/homeassistant/components/streamlabswater/coordinator.py @@ -44,7 +44,7 @@ class StreamlabsCoordinator(DataUpdateCoordinator[dict[str, StreamlabsData]]): def _update_data(self) -> dict[str, StreamlabsData]: locations = self.client.get_locations() res = {} - for location in locations: + for location in locations["locations"]: location_id = location["locationId"] water_usage = self.client.get_water_usage_summary(location_id) res[location_id] = StreamlabsData( diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index 0b249b7c4e5..6c869a6d1bc 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry( entities = [] - for location_id in coordinator.data.values(): + for location_id in coordinator.data: entities.extend( [ StreamLabsDailyUsage(coordinator, location_id), From 80b45edb2e64632d95ad1712559639ac4ee00577 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 5 Jan 2024 10:53:59 +0100 Subject: [PATCH 011/133] Fix mobile_app cloudhook creation (#107068) --- homeassistant/components/cloud/__init__.py | 17 +++++ .../components/mobile_app/__init__.py | 13 ++-- .../components/mobile_app/http_api.py | 5 +- homeassistant/components/mobile_app/util.py | 20 ++++++ tests/components/cloud/conftest.py | 1 + tests/components/cloud/test_init.py | 64 ++++++++++++++++++- tests/components/mobile_app/test_init.py | 10 +-- 7 files changed, 113 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index d7d57835e3a..6e5cddd0f28 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta from enum import Enum +from typing import cast from hass_nabucasa import Cloud import voluptuous as vol @@ -176,6 +177,22 @@ def async_active_subscription(hass: HomeAssistant) -> bool: return async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired +async def async_get_or_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: + """Get or create a cloudhook.""" + if not async_is_connected(hass): + raise CloudNotConnected + + if not async_is_logged_in(hass): + raise CloudNotAvailable + + cloud: Cloud[CloudClient] = hass.data[DOMAIN] + cloudhooks = cloud.client.cloudhooks + if hook := cloudhooks.get(webhook_id): + return cast(str, hook["cloudhook_url"]) + + return await async_create_cloudhook(hass, webhook_id) + + @bind_hass async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: """Create a cloudhook.""" diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index cb5c0ae5c3d..124ef750baa 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -36,6 +36,7 @@ from .const import ( ) from .helpers import savable_state from .http_api import RegistrationsView +from .util import async_create_cloud_hook from .webhook import handle_webhook PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] @@ -103,26 +104,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - async def create_cloud_hook() -> None: - """Create a cloud hook.""" - hook = await cloud.async_create_cloudhook(hass, webhook_id) - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} - ) - async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: if ( state is cloud.CloudConnectionState.CLOUD_CONNECTED and CONF_CLOUDHOOK_URL not in entry.data ): - await create_cloud_hook() + await async_create_cloud_hook(hass, webhook_id, entry) if ( CONF_CLOUDHOOK_URL not in entry.data and cloud.async_active_subscription(hass) and cloud.async_is_connected(hass) ): - await create_cloud_hook() + await async_create_cloud_hook(hass, webhook_id, entry) + entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 3c34a291df1..92bb473d51a 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -35,6 +35,7 @@ from .const import ( SCHEMA_APP_DATA, ) from .helpers import supports_encryption +from .util import async_create_cloud_hook class RegistrationsView(HomeAssistantView): @@ -69,8 +70,8 @@ class RegistrationsView(HomeAssistantView): webhook_id = secrets.token_hex() if cloud.async_active_subscription(hass): - data[CONF_CLOUDHOOK_URL] = await cloud.async_create_cloudhook( - hass, webhook_id + data[CONF_CLOUDHOOK_URL] = await async_create_cloud_hook( + hass, webhook_id, None ) data[CONF_WEBHOOK_ID] = webhook_id diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py index 45641861e5c..a7871d935ed 100644 --- a/homeassistant/components/mobile_app/util.py +++ b/homeassistant/components/mobile_app/util.py @@ -1,8 +1,11 @@ """Mobile app utility functions.""" from __future__ import annotations +import asyncio from typing import TYPE_CHECKING +from homeassistant.components import cloud +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from .const import ( @@ -10,6 +13,7 @@ from .const import ( ATTR_PUSH_TOKEN, ATTR_PUSH_URL, ATTR_PUSH_WEBSOCKET_CHANNEL, + CONF_CLOUDHOOK_URL, DATA_CONFIG_ENTRIES, DATA_DEVICES, DATA_NOTIFY, @@ -53,3 +57,19 @@ def get_notify_service(hass: HomeAssistant, webhook_id: str) -> str | None: return target_service return None + + +_CLOUD_HOOK_LOCK = asyncio.Lock() + + +async def async_create_cloud_hook( + hass: HomeAssistant, webhook_id: str, entry: ConfigEntry | None +) -> str: + """Create a cloud hook.""" + async with _CLOUD_HOOK_LOCK: + hook = await cloud.async_get_or_create_cloudhook(hass, webhook_id) + if entry: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_CLOUDHOOK_URL: hook} + ) + return hook diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index ef8cb037cdb..42852b15206 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -109,6 +109,7 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]: is_connected = PropertyMock(side_effect=mock_is_connected) type(mock_cloud).is_connected = is_connected + type(mock_cloud.iot).connected = is_connected # Properties that we mock as attributes. mock_cloud.expiration_date = utcnow() diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e12775d5a4a..850f8e12e02 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,12 +1,18 @@ """Test the cloud component.""" +from collections.abc import Callable, Coroutine from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch from hass_nabucasa import Cloud import pytest from homeassistant.components import cloud -from homeassistant.components.cloud.const import DOMAIN +from homeassistant.components.cloud import ( + CloudNotAvailable, + CloudNotConnected, + async_get_or_create_cloudhook, +) +from homeassistant.components.cloud.const import DOMAIN, PREF_CLOUDHOOKS from homeassistant.components.cloud.prefs import STORAGE_KEY from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context, HomeAssistant @@ -214,3 +220,57 @@ async def test_remote_ui_url(hass: HomeAssistant, mock_cloud_fixture) -> None: cl.client.prefs._prefs["remote_domain"] = "example.com" assert cloud.async_remote_ui_url(hass) == "https://example.com" + + +async def test_async_get_or_create_cloudhook( + hass: HomeAssistant, + cloud: MagicMock, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], +) -> None: + """Test async_get_or_create_cloudhook.""" + assert await async_setup_component(hass, "cloud", {"cloud": {}}) + await hass.async_block_till_done() + + webhook_id = "mock-webhook-id" + cloudhook_url = "https://cloudhook.nabu.casa/abcdefg" + + with patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value=cloudhook_url, + ) as async_create_cloudhook_mock: + # create cloudhook as it does not exist + assert (await async_get_or_create_cloudhook(hass, webhook_id)) == cloudhook_url + async_create_cloudhook_mock.assert_called_once_with(hass, webhook_id) + + await set_cloud_prefs( + { + PREF_CLOUDHOOKS: { + webhook_id: { + "webhook_id": webhook_id, + "cloudhook_id": "random-id", + "cloudhook_url": cloudhook_url, + "managed": True, + } + } + } + ) + + async_create_cloudhook_mock.reset_mock() + + # get cloudhook as it exists + assert await async_get_or_create_cloudhook(hass, webhook_id) == cloudhook_url + async_create_cloudhook_mock.assert_not_called() + + # Simulate logged out + cloud.id_token = None + + # Not logged in + with pytest.raises(CloudNotAvailable): + await async_get_or_create_cloudhook(hass, webhook_id) + + # Simulate disconnected + cloud.iot.state = "disconnected" + + # Not connected + with pytest.raises(CloudNotConnected): + await async_get_or_create_cloudhook(hass, webhook_id) diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py index d504703c222..6a365e84fb0 100644 --- a/tests/components/mobile_app/test_init.py +++ b/tests/components/mobile_app/test_init.py @@ -88,15 +88,17 @@ async def _test_create_cloud_hook( ), patch( "homeassistant.components.cloud.async_is_connected", return_value=True ), patch( - "homeassistant.components.cloud.async_create_cloudhook", autospec=True - ) as mock_create_cloudhook: + "homeassistant.components.cloud.async_get_or_create_cloudhook", autospec=True + ) as mock_async_get_or_create_cloudhook: cloud_hook = "https://hook-url" - mock_create_cloudhook.return_value = cloud_hook + mock_async_get_or_create_cloudhook.return_value = cloud_hook assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - await additional_steps(config_entry, mock_create_cloudhook, cloud_hook) + await additional_steps( + config_entry, mock_async_get_or_create_cloudhook, cloud_hook + ) async def test_create_cloud_hook_on_setup( From c56d118e8b9622c0c57d2f3be43e78e3b04ba7ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 4 Jan 2024 13:25:09 +0100 Subject: [PATCH 012/133] Deduplicate handling of duplicated constants (#107074) * Deduplicate handling of duplicated constants * Use DeprecatedConstant + DeprecatedConstantEnum * Fixup * Remove test cases with unnamed tuples --- homeassistant/const.py | 383 +++++++++++++++------------ homeassistant/core.py | 46 +--- homeassistant/helpers/deprecation.py | 25 +- tests/helpers/test_deprecation.py | 32 --- 4 files changed, 236 insertions(+), 250 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6afa0430ba3..a6927aa8165 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,15 @@ from __future__ import annotations from enum import StrEnum -from typing import Any, Final +from functools import partial +from typing import Final + +from .helpers.deprecation import ( + DeprecatedConstant, + DeprecatedConstantEnum, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 @@ -307,146 +315,135 @@ EVENT_SHOPPING_LIST_UPDATED: Final = "shopping_list_updated" # #### DEVICE CLASSES #### # DEVICE_CLASS_* below are deprecated as of 2021.12 # use the SensorDeviceClass enum instead. -_DEPRECATED_DEVICE_CLASS_AQI: Final = ("aqi", "SensorDeviceClass.AQI", "2025.1") -_DEPRECATED_DEVICE_CLASS_BATTERY: Final = ( +_DEPRECATED_DEVICE_CLASS_AQI: Final = DeprecatedConstant( + "aqi", "SensorDeviceClass.AQI", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_BATTERY: Final = DeprecatedConstant( "battery", "SensorDeviceClass.BATTERY", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_CO: Final = ( +_DEPRECATED_DEVICE_CLASS_CO: Final = DeprecatedConstant( "carbon_monoxide", "SensorDeviceClass.CO", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_CO2: Final = ( +_DEPRECATED_DEVICE_CLASS_CO2: Final = DeprecatedConstant( "carbon_dioxide", "SensorDeviceClass.CO2", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_CURRENT: Final = ( +_DEPRECATED_DEVICE_CLASS_CURRENT: Final = DeprecatedConstant( "current", "SensorDeviceClass.CURRENT", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_DATE: Final = ("date", "SensorDeviceClass.DATE", "2025.1") -_DEPRECATED_DEVICE_CLASS_ENERGY: Final = ( +_DEPRECATED_DEVICE_CLASS_DATE: Final = DeprecatedConstant( + "date", "SensorDeviceClass.DATE", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_ENERGY: Final = DeprecatedConstant( "energy", "SensorDeviceClass.ENERGY", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_FREQUENCY: Final = ( +_DEPRECATED_DEVICE_CLASS_FREQUENCY: Final = DeprecatedConstant( "frequency", "SensorDeviceClass.FREQUENCY", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_GAS: Final = ("gas", "SensorDeviceClass.GAS", "2025.1") -_DEPRECATED_DEVICE_CLASS_HUMIDITY: Final = ( +_DEPRECATED_DEVICE_CLASS_GAS: Final = DeprecatedConstant( + "gas", "SensorDeviceClass.GAS", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_HUMIDITY: Final = DeprecatedConstant( "humidity", "SensorDeviceClass.HUMIDITY", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_ILLUMINANCE: Final = ( +_DEPRECATED_DEVICE_CLASS_ILLUMINANCE: Final = DeprecatedConstant( "illuminance", "SensorDeviceClass.ILLUMINANCE", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_MONETARY: Final = ( +_DEPRECATED_DEVICE_CLASS_MONETARY: Final = DeprecatedConstant( "monetary", "SensorDeviceClass.MONETARY", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_NITROGEN_DIOXIDE = ( +_DEPRECATED_DEVICE_CLASS_NITROGEN_DIOXIDE: Final = DeprecatedConstant( "nitrogen_dioxide", "SensorDeviceClass.NITROGEN_DIOXIDE", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_NITROGEN_MONOXIDE = ( +_DEPRECATED_DEVICE_CLASS_NITROGEN_MONOXIDE: Final = DeprecatedConstant( "nitrogen_monoxide", "SensorDeviceClass.NITROGEN_MONOXIDE", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_NITROUS_OXIDE = ( +_DEPRECATED_DEVICE_CLASS_NITROUS_OXIDE: Final = DeprecatedConstant( "nitrous_oxide", "SensorDeviceClass.NITROUS_OXIDE", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_OZONE: Final = ("ozone", "SensorDeviceClass.OZONE", "2025.1") -_DEPRECATED_DEVICE_CLASS_PM1: Final = ("pm1", "SensorDeviceClass.PM1", "2025.1") -_DEPRECATED_DEVICE_CLASS_PM10: Final = ("pm10", "SensorDeviceClass.PM10", "2025.1") -_DEPRECATED_DEVICE_CLASS_PM25: Final = ("pm25", "SensorDeviceClass.PM25", "2025.1") -_DEPRECATED_DEVICE_CLASS_POWER_FACTOR: Final = ( +_DEPRECATED_DEVICE_CLASS_OZONE: Final = DeprecatedConstant( + "ozone", "SensorDeviceClass.OZONE", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_PM1: Final = DeprecatedConstant( + "pm1", "SensorDeviceClass.PM1", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_PM10: Final = DeprecatedConstant( + "pm10", "SensorDeviceClass.PM10", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_PM25: Final = DeprecatedConstant( + "pm25", "SensorDeviceClass.PM25", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_POWER_FACTOR: Final = DeprecatedConstant( "power_factor", "SensorDeviceClass.POWER_FACTOR", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_POWER: Final = ("power", "SensorDeviceClass.POWER", "2025.1") -_DEPRECATED_DEVICE_CLASS_PRESSURE: Final = ( +_DEPRECATED_DEVICE_CLASS_POWER: Final = DeprecatedConstant( + "power", "SensorDeviceClass.POWER", "2025.1" +) +_DEPRECATED_DEVICE_CLASS_PRESSURE: Final = DeprecatedConstant( "pressure", "SensorDeviceClass.PRESSURE", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_SIGNAL_STRENGTH: Final = ( +_DEPRECATED_DEVICE_CLASS_SIGNAL_STRENGTH: Final = DeprecatedConstant( "signal_strength", "SensorDeviceClass.SIGNAL_STRENGTH", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_SULPHUR_DIOXIDE = ( +_DEPRECATED_DEVICE_CLASS_SULPHUR_DIOXIDE: Final = DeprecatedConstant( "sulphur_dioxide", "SensorDeviceClass.SULPHUR_DIOXIDE", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_TEMPERATURE: Final = ( +_DEPRECATED_DEVICE_CLASS_TEMPERATURE: Final = DeprecatedConstant( "temperature", "SensorDeviceClass.TEMPERATURE", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_TIMESTAMP: Final = ( +_DEPRECATED_DEVICE_CLASS_TIMESTAMP: Final = DeprecatedConstant( "timestamp", "SensorDeviceClass.TIMESTAMP", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = ( +_DEPRECATED_DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS: Final = DeprecatedConstant( "volatile_organic_compounds", "SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS", "2025.1", ) -_DEPRECATED_DEVICE_CLASS_VOLTAGE: Final = ( +_DEPRECATED_DEVICE_CLASS_VOLTAGE: Final = DeprecatedConstant( "voltage", "SensorDeviceClass.VOLTAGE", "2025.1", ) -# Can be removed if no deprecated constant are in this module anymore -def __getattr__(name: str) -> Any: - """Check if the not found name is a deprecated constant. - - If it is, print a deprecation warning and return the value of the constant. - Otherwise raise AttributeError. - """ - module_globals = globals() - if f"_DEPRECATED_{name}" not in module_globals: - raise AttributeError(f"Module {__name__} has no attribute {name!r}") - - # Avoid circular import - from .helpers.deprecation import ( # pylint: disable=import-outside-toplevel - check_if_deprecated_constant, - ) - - return check_if_deprecated_constant(name, module_globals) - - -# Can be removed if no deprecated constant are in this module anymore -def __dir__() -> list[str]: - """Return dir() with deprecated constants.""" - # Copied method from homeassistant.helpers.deprecattion#dir_with_deprecated_constants to avoid import cycle - module_globals = globals() - - return list(module_globals) + [ - name.removeprefix("_DEPRECATED_") - for name in module_globals - if name.startswith("_DEPRECATED_") - ] +# Both can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) # #### STATES #### @@ -621,7 +618,7 @@ class UnitOfApparentPower(StrEnum): VOLT_AMPERE = "VA" -_DEPRECATED_POWER_VOLT_AMPERE: Final = ( +_DEPRECATED_POWER_VOLT_AMPERE: Final = DeprecatedConstantEnum( UnitOfApparentPower.VOLT_AMPERE, "2025.1", ) @@ -637,17 +634,17 @@ class UnitOfPower(StrEnum): BTU_PER_HOUR = "BTU/h" -_DEPRECATED_POWER_WATT: Final = ( +_DEPRECATED_POWER_WATT: Final = DeprecatedConstantEnum( UnitOfPower.WATT, "2025.1", ) """Deprecated: please use UnitOfPower.WATT.""" -_DEPRECATED_POWER_KILO_WATT: Final = ( +_DEPRECATED_POWER_KILO_WATT: Final = DeprecatedConstantEnum( UnitOfPower.KILO_WATT, "2025.1", ) """Deprecated: please use UnitOfPower.KILO_WATT.""" -_DEPRECATED_POWER_BTU_PER_HOUR: Final = ( +_DEPRECATED_POWER_BTU_PER_HOUR: Final = DeprecatedConstantEnum( UnitOfPower.BTU_PER_HOUR, "2025.1", ) @@ -668,17 +665,17 @@ class UnitOfEnergy(StrEnum): WATT_HOUR = "Wh" -_DEPRECATED_ENERGY_KILO_WATT_HOUR: Final = ( +_DEPRECATED_ENERGY_KILO_WATT_HOUR: Final = DeprecatedConstantEnum( UnitOfEnergy.KILO_WATT_HOUR, "2025.1", ) """Deprecated: please use UnitOfEnergy.KILO_WATT_HOUR.""" -_DEPRECATED_ENERGY_MEGA_WATT_HOUR: Final = ( +_DEPRECATED_ENERGY_MEGA_WATT_HOUR: Final = DeprecatedConstantEnum( UnitOfEnergy.MEGA_WATT_HOUR, "2025.1", ) """Deprecated: please use UnitOfEnergy.MEGA_WATT_HOUR.""" -_DEPRECATED_ENERGY_WATT_HOUR: Final = ( +_DEPRECATED_ENERGY_WATT_HOUR: Final = DeprecatedConstantEnum( UnitOfEnergy.WATT_HOUR, "2025.1", ) @@ -693,12 +690,12 @@ class UnitOfElectricCurrent(StrEnum): AMPERE = "A" -_DEPRECATED_ELECTRIC_CURRENT_MILLIAMPERE: Final = ( +_DEPRECATED_ELECTRIC_CURRENT_MILLIAMPERE: Final = DeprecatedConstantEnum( UnitOfElectricCurrent.MILLIAMPERE, "2025.1", ) """Deprecated: please use UnitOfElectricCurrent.MILLIAMPERE.""" -_DEPRECATED_ELECTRIC_CURRENT_AMPERE: Final = ( +_DEPRECATED_ELECTRIC_CURRENT_AMPERE: Final = DeprecatedConstantEnum( UnitOfElectricCurrent.AMPERE, "2025.1", ) @@ -713,12 +710,12 @@ class UnitOfElectricPotential(StrEnum): VOLT = "V" -_DEPRECATED_ELECTRIC_POTENTIAL_MILLIVOLT: Final = ( +_DEPRECATED_ELECTRIC_POTENTIAL_MILLIVOLT: Final = DeprecatedConstantEnum( UnitOfElectricPotential.MILLIVOLT, "2025.1", ) """Deprecated: please use UnitOfElectricPotential.MILLIVOLT.""" -_DEPRECATED_ELECTRIC_POTENTIAL_VOLT: Final = ( +_DEPRECATED_ELECTRIC_POTENTIAL_VOLT: Final = DeprecatedConstantEnum( UnitOfElectricPotential.VOLT, "2025.1", ) @@ -742,17 +739,17 @@ class UnitOfTemperature(StrEnum): KELVIN = "K" -_DEPRECATED_TEMP_CELSIUS: Final = ( +_DEPRECATED_TEMP_CELSIUS: Final = DeprecatedConstantEnum( UnitOfTemperature.CELSIUS, "2025.1", ) """Deprecated: please use UnitOfTemperature.CELSIUS""" -_DEPRECATED_TEMP_FAHRENHEIT: Final = ( +_DEPRECATED_TEMP_FAHRENHEIT: Final = DeprecatedConstantEnum( UnitOfTemperature.FAHRENHEIT, "2025.1", ) """Deprecated: please use UnitOfTemperature.FAHRENHEIT""" -_DEPRECATED_TEMP_KELVIN: Final = ( +_DEPRECATED_TEMP_KELVIN: Final = DeprecatedConstantEnum( UnitOfTemperature.KELVIN, "2025.1", ) @@ -774,47 +771,47 @@ class UnitOfTime(StrEnum): YEARS = "y" -_DEPRECATED_TIME_MICROSECONDS: Final = ( +_DEPRECATED_TIME_MICROSECONDS: Final = DeprecatedConstantEnum( UnitOfTime.MICROSECONDS, "2025.1", ) """Deprecated: please use UnitOfTime.MICROSECONDS.""" -_DEPRECATED_TIME_MILLISECONDS: Final = ( +_DEPRECATED_TIME_MILLISECONDS: Final = DeprecatedConstantEnum( UnitOfTime.MILLISECONDS, "2025.1", ) """Deprecated: please use UnitOfTime.MILLISECONDS.""" -_DEPRECATED_TIME_SECONDS: Final = ( +_DEPRECATED_TIME_SECONDS: Final = DeprecatedConstantEnum( UnitOfTime.SECONDS, "2025.1", ) """Deprecated: please use UnitOfTime.SECONDS.""" -_DEPRECATED_TIME_MINUTES: Final = ( +_DEPRECATED_TIME_MINUTES: Final = DeprecatedConstantEnum( UnitOfTime.MINUTES, "2025.1", ) """Deprecated: please use UnitOfTime.MINUTES.""" -_DEPRECATED_TIME_HOURS: Final = ( +_DEPRECATED_TIME_HOURS: Final = DeprecatedConstantEnum( UnitOfTime.HOURS, "2025.1", ) """Deprecated: please use UnitOfTime.HOURS.""" -_DEPRECATED_TIME_DAYS: Final = ( +_DEPRECATED_TIME_DAYS: Final = DeprecatedConstantEnum( UnitOfTime.DAYS, "2025.1", ) """Deprecated: please use UnitOfTime.DAYS.""" -_DEPRECATED_TIME_WEEKS: Final = ( +_DEPRECATED_TIME_WEEKS: Final = DeprecatedConstantEnum( UnitOfTime.WEEKS, "2025.1", ) """Deprecated: please use UnitOfTime.WEEKS.""" -_DEPRECATED_TIME_MONTHS: Final = ( +_DEPRECATED_TIME_MONTHS: Final = DeprecatedConstantEnum( UnitOfTime.MONTHS, "2025.1", ) """Deprecated: please use UnitOfTime.MONTHS.""" -_DEPRECATED_TIME_YEARS: Final = ( +_DEPRECATED_TIME_YEARS: Final = DeprecatedConstantEnum( UnitOfTime.YEARS, "2025.1", ) @@ -835,42 +832,42 @@ class UnitOfLength(StrEnum): MILES = "mi" -_DEPRECATED_LENGTH_MILLIMETERS: Final = ( +_DEPRECATED_LENGTH_MILLIMETERS: Final = DeprecatedConstantEnum( UnitOfLength.MILLIMETERS, "2025.1", ) """Deprecated: please use UnitOfLength.MILLIMETERS.""" -_DEPRECATED_LENGTH_CENTIMETERS: Final = ( +_DEPRECATED_LENGTH_CENTIMETERS: Final = DeprecatedConstantEnum( UnitOfLength.CENTIMETERS, "2025.1", ) """Deprecated: please use UnitOfLength.CENTIMETERS.""" -_DEPRECATED_LENGTH_METERS: Final = ( +_DEPRECATED_LENGTH_METERS: Final = DeprecatedConstantEnum( UnitOfLength.METERS, "2025.1", ) """Deprecated: please use UnitOfLength.METERS.""" -_DEPRECATED_LENGTH_KILOMETERS: Final = ( +_DEPRECATED_LENGTH_KILOMETERS: Final = DeprecatedConstantEnum( UnitOfLength.KILOMETERS, "2025.1", ) """Deprecated: please use UnitOfLength.KILOMETERS.""" -_DEPRECATED_LENGTH_INCHES: Final = ( +_DEPRECATED_LENGTH_INCHES: Final = DeprecatedConstantEnum( UnitOfLength.INCHES, "2025.1", ) """Deprecated: please use UnitOfLength.INCHES.""" -_DEPRECATED_LENGTH_FEET: Final = ( +_DEPRECATED_LENGTH_FEET: Final = DeprecatedConstantEnum( UnitOfLength.FEET, "2025.1", ) """Deprecated: please use UnitOfLength.FEET.""" -_DEPRECATED_LENGTH_YARD: Final = ( +_DEPRECATED_LENGTH_YARD: Final = DeprecatedConstantEnum( UnitOfLength.YARDS, "2025.1", ) """Deprecated: please use UnitOfLength.YARDS.""" -_DEPRECATED_LENGTH_MILES: Final = ( +_DEPRECATED_LENGTH_MILES: Final = DeprecatedConstantEnum( UnitOfLength.MILES, "2025.1", ) @@ -887,22 +884,22 @@ class UnitOfFrequency(StrEnum): GIGAHERTZ = "GHz" -_DEPRECATED_FREQUENCY_HERTZ: Final = ( +_DEPRECATED_FREQUENCY_HERTZ: Final = DeprecatedConstantEnum( UnitOfFrequency.HERTZ, "2025.1", ) """Deprecated: please use UnitOfFrequency.HERTZ""" -_DEPRECATED_FREQUENCY_KILOHERTZ: Final = ( +_DEPRECATED_FREQUENCY_KILOHERTZ: Final = DeprecatedConstantEnum( UnitOfFrequency.KILOHERTZ, "2025.1", ) """Deprecated: please use UnitOfFrequency.KILOHERTZ""" -_DEPRECATED_FREQUENCY_MEGAHERTZ: Final = ( +_DEPRECATED_FREQUENCY_MEGAHERTZ: Final = DeprecatedConstantEnum( UnitOfFrequency.MEGAHERTZ, "2025.1", ) """Deprecated: please use UnitOfFrequency.MEGAHERTZ""" -_DEPRECATED_FREQUENCY_GIGAHERTZ: Final = ( +_DEPRECATED_FREQUENCY_GIGAHERTZ: Final = DeprecatedConstantEnum( UnitOfFrequency.GIGAHERTZ, "2025.1", ) @@ -924,47 +921,47 @@ class UnitOfPressure(StrEnum): PSI = "psi" -_DEPRECATED_PRESSURE_PA: Final = ( +_DEPRECATED_PRESSURE_PA: Final = DeprecatedConstantEnum( UnitOfPressure.PA, "2025.1", ) """Deprecated: please use UnitOfPressure.PA""" -_DEPRECATED_PRESSURE_HPA: Final = ( +_DEPRECATED_PRESSURE_HPA: Final = DeprecatedConstantEnum( UnitOfPressure.HPA, "2025.1", ) """Deprecated: please use UnitOfPressure.HPA""" -_DEPRECATED_PRESSURE_KPA: Final = ( +_DEPRECATED_PRESSURE_KPA: Final = DeprecatedConstantEnum( UnitOfPressure.KPA, "2025.1", ) """Deprecated: please use UnitOfPressure.KPA""" -_DEPRECATED_PRESSURE_BAR: Final = ( +_DEPRECATED_PRESSURE_BAR: Final = DeprecatedConstantEnum( UnitOfPressure.BAR, "2025.1", ) """Deprecated: please use UnitOfPressure.BAR""" -_DEPRECATED_PRESSURE_CBAR: Final = ( +_DEPRECATED_PRESSURE_CBAR: Final = DeprecatedConstantEnum( UnitOfPressure.CBAR, "2025.1", ) """Deprecated: please use UnitOfPressure.CBAR""" -_DEPRECATED_PRESSURE_MBAR: Final = ( +_DEPRECATED_PRESSURE_MBAR: Final = DeprecatedConstantEnum( UnitOfPressure.MBAR, "2025.1", ) """Deprecated: please use UnitOfPressure.MBAR""" -_DEPRECATED_PRESSURE_MMHG: Final = ( +_DEPRECATED_PRESSURE_MMHG: Final = DeprecatedConstantEnum( UnitOfPressure.MMHG, "2025.1", ) """Deprecated: please use UnitOfPressure.MMHG""" -_DEPRECATED_PRESSURE_INHG: Final = ( +_DEPRECATED_PRESSURE_INHG: Final = DeprecatedConstantEnum( UnitOfPressure.INHG, "2025.1", ) """Deprecated: please use UnitOfPressure.INHG""" -_DEPRECATED_PRESSURE_PSI: Final = ( +_DEPRECATED_PRESSURE_PSI: Final = DeprecatedConstantEnum( UnitOfPressure.PSI, "2025.1", ) @@ -979,12 +976,12 @@ class UnitOfSoundPressure(StrEnum): WEIGHTED_DECIBEL_A = "dBA" -_DEPRECATED_SOUND_PRESSURE_DB: Final = ( +_DEPRECATED_SOUND_PRESSURE_DB: Final = DeprecatedConstantEnum( UnitOfSoundPressure.DECIBEL, "2025.1", ) """Deprecated: please use UnitOfSoundPressure.DECIBEL""" -_DEPRECATED_SOUND_PRESSURE_WEIGHTED_DBA: Final = ( +_DEPRECATED_SOUND_PRESSURE_WEIGHTED_DBA: Final = DeprecatedConstantEnum( UnitOfSoundPressure.WEIGHTED_DECIBEL_A, "2025.1", ) @@ -1010,33 +1007,33 @@ class UnitOfVolume(StrEnum): British/Imperial fluid ounces are not yet supported""" -_DEPRECATED_VOLUME_LITERS: Final = ( +_DEPRECATED_VOLUME_LITERS: Final = DeprecatedConstantEnum( UnitOfVolume.LITERS, "2025.1", ) """Deprecated: please use UnitOfVolume.LITERS""" -_DEPRECATED_VOLUME_MILLILITERS: Final = ( +_DEPRECATED_VOLUME_MILLILITERS: Final = DeprecatedConstantEnum( UnitOfVolume.MILLILITERS, "2025.1", ) """Deprecated: please use UnitOfVolume.MILLILITERS""" -_DEPRECATED_VOLUME_CUBIC_METERS: Final = ( +_DEPRECATED_VOLUME_CUBIC_METERS: Final = DeprecatedConstantEnum( UnitOfVolume.CUBIC_METERS, "2025.1", ) """Deprecated: please use UnitOfVolume.CUBIC_METERS""" -_DEPRECATED_VOLUME_CUBIC_FEET: Final = ( +_DEPRECATED_VOLUME_CUBIC_FEET: Final = DeprecatedConstantEnum( UnitOfVolume.CUBIC_FEET, "2025.1", ) """Deprecated: please use UnitOfVolume.CUBIC_FEET""" -_DEPRECATED_VOLUME_GALLONS: Final = ( +_DEPRECATED_VOLUME_GALLONS: Final = DeprecatedConstantEnum( UnitOfVolume.GALLONS, "2025.1", ) """Deprecated: please use UnitOfVolume.GALLONS""" -_DEPRECATED_VOLUME_FLUID_OUNCE: Final = ( +_DEPRECATED_VOLUME_FLUID_OUNCE: Final = DeprecatedConstantEnum( UnitOfVolume.FLUID_OUNCES, "2025.1", ) @@ -1051,12 +1048,12 @@ class UnitOfVolumeFlowRate(StrEnum): CUBIC_FEET_PER_MINUTE = "ft³/m" -_DEPRECATED_VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = ( +_DEPRECATED_VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = DeprecatedConstantEnum( UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, "2025.1", ) """Deprecated: please use UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR""" -_DEPRECATED_VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = ( +_DEPRECATED_VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = DeprecatedConstantEnum( UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, "2025.1", ) @@ -1079,32 +1076,32 @@ class UnitOfMass(StrEnum): STONES = "st" -_DEPRECATED_MASS_GRAMS: Final = ( +_DEPRECATED_MASS_GRAMS: Final = DeprecatedConstantEnum( UnitOfMass.GRAMS, "2025.1", ) """Deprecated: please use UnitOfMass.GRAMS""" -_DEPRECATED_MASS_KILOGRAMS: Final = ( +_DEPRECATED_MASS_KILOGRAMS: Final = DeprecatedConstantEnum( UnitOfMass.KILOGRAMS, "2025.1", ) """Deprecated: please use UnitOfMass.KILOGRAMS""" -_DEPRECATED_MASS_MILLIGRAMS: Final = ( +_DEPRECATED_MASS_MILLIGRAMS: Final = DeprecatedConstantEnum( UnitOfMass.MILLIGRAMS, "2025.1", ) """Deprecated: please use UnitOfMass.MILLIGRAMS""" -_DEPRECATED_MASS_MICROGRAMS: Final = ( +_DEPRECATED_MASS_MICROGRAMS: Final = DeprecatedConstantEnum( UnitOfMass.MICROGRAMS, "2025.1", ) """Deprecated: please use UnitOfMass.MICROGRAMS""" -_DEPRECATED_MASS_OUNCES: Final = ( +_DEPRECATED_MASS_OUNCES: Final = DeprecatedConstantEnum( UnitOfMass.OUNCES, "2025.1", ) """Deprecated: please use UnitOfMass.OUNCES""" -_DEPRECATED_MASS_POUNDS: Final = ( +_DEPRECATED_MASS_POUNDS: Final = DeprecatedConstantEnum( UnitOfMass.POUNDS, "2025.1", ) @@ -1135,12 +1132,12 @@ class UnitOfIrradiance(StrEnum): # Irradiation units -_DEPRECATED_IRRADIATION_WATTS_PER_SQUARE_METER: Final = ( +_DEPRECATED_IRRADIATION_WATTS_PER_SQUARE_METER: Final = DeprecatedConstantEnum( UnitOfIrradiance.WATTS_PER_SQUARE_METER, "2025.1", ) """Deprecated: please use UnitOfIrradiance.WATTS_PER_SQUARE_METER""" -_DEPRECATED_IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = ( +_DEPRECATED_IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT: Final = DeprecatedConstantEnum( UnitOfIrradiance.BTUS_PER_HOUR_SQUARE_FOOT, "2025.1", ) @@ -1185,19 +1182,21 @@ class UnitOfPrecipitationDepth(StrEnum): # Precipitation units -_DEPRECATED_PRECIPITATION_INCHES: Final = (UnitOfPrecipitationDepth.INCHES, "2025.1") +_DEPRECATED_PRECIPITATION_INCHES: Final = DeprecatedConstantEnum( + UnitOfPrecipitationDepth.INCHES, "2025.1" +) """Deprecated: please use UnitOfPrecipitationDepth.INCHES""" -_DEPRECATED_PRECIPITATION_MILLIMETERS: Final = ( +_DEPRECATED_PRECIPITATION_MILLIMETERS: Final = DeprecatedConstantEnum( UnitOfPrecipitationDepth.MILLIMETERS, "2025.1", ) """Deprecated: please use UnitOfPrecipitationDepth.MILLIMETERS""" -_DEPRECATED_PRECIPITATION_MILLIMETERS_PER_HOUR: Final = ( +_DEPRECATED_PRECIPITATION_MILLIMETERS_PER_HOUR: Final = DeprecatedConstantEnum( UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, "2025.1", ) """Deprecated: please use UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR""" -_DEPRECATED_PRECIPITATION_INCHES_PER_HOUR: Final = ( +_DEPRECATED_PRECIPITATION_INCHES_PER_HOUR: Final = DeprecatedConstantEnum( UnitOfVolumetricFlux.INCHES_PER_HOUR, "2025.1", ) @@ -1223,33 +1222,39 @@ class UnitOfSpeed(StrEnum): MILES_PER_HOUR = "mph" -_DEPRECATED_SPEED_FEET_PER_SECOND: Final = (UnitOfSpeed.FEET_PER_SECOND, "2025.1") +_DEPRECATED_SPEED_FEET_PER_SECOND: Final = DeprecatedConstantEnum( + UnitOfSpeed.FEET_PER_SECOND, "2025.1" +) """Deprecated: please use UnitOfSpeed.FEET_PER_SECOND""" -_DEPRECATED_SPEED_METERS_PER_SECOND: Final = (UnitOfSpeed.METERS_PER_SECOND, "2025.1") +_DEPRECATED_SPEED_METERS_PER_SECOND: Final = DeprecatedConstantEnum( + UnitOfSpeed.METERS_PER_SECOND, "2025.1" +) """Deprecated: please use UnitOfSpeed.METERS_PER_SECOND""" -_DEPRECATED_SPEED_KILOMETERS_PER_HOUR: Final = ( +_DEPRECATED_SPEED_KILOMETERS_PER_HOUR: Final = DeprecatedConstantEnum( UnitOfSpeed.KILOMETERS_PER_HOUR, "2025.1", ) """Deprecated: please use UnitOfSpeed.KILOMETERS_PER_HOUR""" -_DEPRECATED_SPEED_KNOTS: Final = (UnitOfSpeed.KNOTS, "2025.1") +_DEPRECATED_SPEED_KNOTS: Final = DeprecatedConstantEnum(UnitOfSpeed.KNOTS, "2025.1") """Deprecated: please use UnitOfSpeed.KNOTS""" -_DEPRECATED_SPEED_MILES_PER_HOUR: Final = (UnitOfSpeed.MILES_PER_HOUR, "2025.1") +_DEPRECATED_SPEED_MILES_PER_HOUR: Final = DeprecatedConstantEnum( + UnitOfSpeed.MILES_PER_HOUR, "2025.1" +) """Deprecated: please use UnitOfSpeed.MILES_PER_HOUR""" -_DEPRECATED_SPEED_MILLIMETERS_PER_DAY: Final = ( +_DEPRECATED_SPEED_MILLIMETERS_PER_DAY: Final = DeprecatedConstantEnum( UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, "2025.1", ) """Deprecated: please use UnitOfVolumetricFlux.MILLIMETERS_PER_DAY""" -_DEPRECATED_SPEED_INCHES_PER_DAY: Final = ( +_DEPRECATED_SPEED_INCHES_PER_DAY: Final = DeprecatedConstantEnum( UnitOfVolumetricFlux.INCHES_PER_DAY, "2025.1", ) """Deprecated: please use UnitOfVolumetricFlux.INCHES_PER_DAY""" -_DEPRECATED_SPEED_INCHES_PER_HOUR: Final = ( +_DEPRECATED_SPEED_INCHES_PER_HOUR: Final = DeprecatedConstantEnum( UnitOfVolumetricFlux.INCHES_PER_HOUR, "2025.1", ) @@ -1288,47 +1293,87 @@ class UnitOfInformation(StrEnum): YOBIBYTES = "YiB" -_DEPRECATED_DATA_BITS: Final = (UnitOfInformation.BITS, "2025.1") +_DEPRECATED_DATA_BITS: Final = DeprecatedConstantEnum(UnitOfInformation.BITS, "2025.1") """Deprecated: please use UnitOfInformation.BITS""" -_DEPRECATED_DATA_KILOBITS: Final = (UnitOfInformation.KILOBITS, "2025.1") +_DEPRECATED_DATA_KILOBITS: Final = DeprecatedConstantEnum( + UnitOfInformation.KILOBITS, "2025.1" +) """Deprecated: please use UnitOfInformation.KILOBITS""" -_DEPRECATED_DATA_MEGABITS: Final = (UnitOfInformation.MEGABITS, "2025.1") +_DEPRECATED_DATA_MEGABITS: Final = DeprecatedConstantEnum( + UnitOfInformation.MEGABITS, "2025.1" +) """Deprecated: please use UnitOfInformation.MEGABITS""" -_DEPRECATED_DATA_GIGABITS: Final = (UnitOfInformation.GIGABITS, "2025.1") +_DEPRECATED_DATA_GIGABITS: Final = DeprecatedConstantEnum( + UnitOfInformation.GIGABITS, "2025.1" +) """Deprecated: please use UnitOfInformation.GIGABITS""" -_DEPRECATED_DATA_BYTES: Final = (UnitOfInformation.BYTES, "2025.1") +_DEPRECATED_DATA_BYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.BYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.BYTES""" -_DEPRECATED_DATA_KILOBYTES: Final = (UnitOfInformation.KILOBYTES, "2025.1") +_DEPRECATED_DATA_KILOBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.KILOBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.KILOBYTES""" -_DEPRECATED_DATA_MEGABYTES: Final = (UnitOfInformation.MEGABYTES, "2025.1") +_DEPRECATED_DATA_MEGABYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.MEGABYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.MEGABYTES""" -_DEPRECATED_DATA_GIGABYTES: Final = (UnitOfInformation.GIGABYTES, "2025.1") +_DEPRECATED_DATA_GIGABYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.GIGABYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.GIGABYTES""" -_DEPRECATED_DATA_TERABYTES: Final = (UnitOfInformation.TERABYTES, "2025.1") +_DEPRECATED_DATA_TERABYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.TERABYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.TERABYTES""" -_DEPRECATED_DATA_PETABYTES: Final = (UnitOfInformation.PETABYTES, "2025.1") +_DEPRECATED_DATA_PETABYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.PETABYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.PETABYTES""" -_DEPRECATED_DATA_EXABYTES: Final = (UnitOfInformation.EXABYTES, "2025.1") +_DEPRECATED_DATA_EXABYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.EXABYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.EXABYTES""" -_DEPRECATED_DATA_ZETTABYTES: Final = (UnitOfInformation.ZETTABYTES, "2025.1") +_DEPRECATED_DATA_ZETTABYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.ZETTABYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.ZETTABYTES""" -_DEPRECATED_DATA_YOTTABYTES: Final = (UnitOfInformation.YOTTABYTES, "2025.1") +_DEPRECATED_DATA_YOTTABYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.YOTTABYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.YOTTABYTES""" -_DEPRECATED_DATA_KIBIBYTES: Final = (UnitOfInformation.KIBIBYTES, "2025.1") +_DEPRECATED_DATA_KIBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.KIBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.KIBIBYTES""" -_DEPRECATED_DATA_MEBIBYTES: Final = (UnitOfInformation.MEBIBYTES, "2025.1") +_DEPRECATED_DATA_MEBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.MEBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.MEBIBYTES""" -_DEPRECATED_DATA_GIBIBYTES: Final = (UnitOfInformation.GIBIBYTES, "2025.1") +_DEPRECATED_DATA_GIBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.GIBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.GIBIBYTES""" -_DEPRECATED_DATA_TEBIBYTES: Final = (UnitOfInformation.TEBIBYTES, "2025.1") +_DEPRECATED_DATA_TEBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.TEBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.TEBIBYTES""" -_DEPRECATED_DATA_PEBIBYTES: Final = (UnitOfInformation.PEBIBYTES, "2025.1") +_DEPRECATED_DATA_PEBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.PEBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.PEBIBYTES""" -_DEPRECATED_DATA_EXBIBYTES: Final = (UnitOfInformation.EXBIBYTES, "2025.1") +_DEPRECATED_DATA_EXBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.EXBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.EXBIBYTES""" -_DEPRECATED_DATA_ZEBIBYTES: Final = (UnitOfInformation.ZEBIBYTES, "2025.1") +_DEPRECATED_DATA_ZEBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.ZEBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.ZEBIBYTES""" -_DEPRECATED_DATA_YOBIBYTES: Final = (UnitOfInformation.YOBIBYTES, "2025.1") +_DEPRECATED_DATA_YOBIBYTES: Final = DeprecatedConstantEnum( + UnitOfInformation.YOBIBYTES, "2025.1" +) """Deprecated: please use UnitOfInformation.YOBIBYTES""" @@ -1349,57 +1394,57 @@ class UnitOfDataRate(StrEnum): GIBIBYTES_PER_SECOND = "GiB/s" -_DEPRECATED_DATA_RATE_BITS_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_BITS_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.BITS_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.BITS_PER_SECOND""" -_DEPRECATED_DATA_RATE_KILOBITS_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_KILOBITS_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.KILOBITS_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.KILOBITS_PER_SECOND""" -_DEPRECATED_DATA_RATE_MEGABITS_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_MEGABITS_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.MEGABITS_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.MEGABITS_PER_SECOND""" -_DEPRECATED_DATA_RATE_GIGABITS_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_GIGABITS_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.GIGABITS_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.GIGABITS_PER_SECOND""" -_DEPRECATED_DATA_RATE_BYTES_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_BYTES_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.BYTES_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.BYTES_PER_SECOND""" -_DEPRECATED_DATA_RATE_KILOBYTES_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_KILOBYTES_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.KILOBYTES_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.KILOBYTES_PER_SECOND""" -_DEPRECATED_DATA_RATE_MEGABYTES_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_MEGABYTES_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.MEGABYTES_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.MEGABYTES_PER_SECOND""" -_DEPRECATED_DATA_RATE_GIGABYTES_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_GIGABYTES_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.GIGABYTES_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.GIGABYTES_PER_SECOND""" -_DEPRECATED_DATA_RATE_KIBIBYTES_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_KIBIBYTES_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.KIBIBYTES_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.KIBIBYTES_PER_SECOND""" -_DEPRECATED_DATA_RATE_MEBIBYTES_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_MEBIBYTES_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.MEBIBYTES_PER_SECOND, "2025.1", ) """Deprecated: please use UnitOfDataRate.MEBIBYTES_PER_SECOND""" -_DEPRECATED_DATA_RATE_GIBIBYTES_PER_SECOND: Final = ( +_DEPRECATED_DATA_RATE_GIBIBYTES_PER_SECOND: Final = DeprecatedConstantEnum( UnitOfDataRate.GIBIBYTES_PER_SECOND, "2025.1", ) @@ -1540,8 +1585,12 @@ class EntityCategory(StrEnum): # ENTITY_CATEGOR* below are deprecated as of 2021.12 # use the EntityCategory enum instead. -_DEPRECATED_ENTITY_CATEGORY_CONFIG: Final = (EntityCategory.CONFIG, "2025.1") -_DEPRECATED_ENTITY_CATEGORY_DIAGNOSTIC: Final = (EntityCategory.DIAGNOSTIC, "2025.1") +_DEPRECATED_ENTITY_CATEGORY_CONFIG: Final = DeprecatedConstantEnum( + EntityCategory.CONFIG, "2025.1" +) +_DEPRECATED_ENTITY_CATEGORY_DIAGNOSTIC: Final = DeprecatedConstantEnum( + EntityCategory.DIAGNOSTIC, "2025.1" +) ENTITY_CATEGORIES: Final[list[str]] = [cls.value for cls in EntityCategory] # The ID of the Home Assistant Media Player Cast App diff --git a/homeassistant/core.py b/homeassistant/core.py index 51cb3d4e496..b15d393c63e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -81,6 +81,11 @@ from .exceptions import ( ServiceNotFound, Unauthorized, ) +from .helpers.deprecation import ( + DeprecatedConstantEnum, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) from .helpers.json import json_dumps from .util import dt as dt_util, location from .util.async_ import ( @@ -147,41 +152,16 @@ class ConfigSource(enum.StrEnum): # SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead -_DEPRECATED_SOURCE_DISCOVERED = (ConfigSource.DISCOVERED, "2025.1") -_DEPRECATED_SOURCE_STORAGE = (ConfigSource.STORAGE, "2025.1") -_DEPRECATED_SOURCE_YAML = (ConfigSource.YAML, "2025.1") +_DEPRECATED_SOURCE_DISCOVERED = DeprecatedConstantEnum( + ConfigSource.DISCOVERED, "2025.1" +) +_DEPRECATED_SOURCE_STORAGE = DeprecatedConstantEnum(ConfigSource.STORAGE, "2025.1") +_DEPRECATED_SOURCE_YAML = DeprecatedConstantEnum(ConfigSource.YAML, "2025.1") -# Can be removed if no deprecated constant are in this module anymore -def __getattr__(name: str) -> Any: - """Check if the not found name is a deprecated constant. - - If it is, print a deprecation warning and return the value of the constant. - Otherwise raise AttributeError. - """ - module_globals = globals() - if f"_DEPRECATED_{name}" not in module_globals: - raise AttributeError(f"Module {__name__} has no attribute {name!r}") - - # Avoid circular import - from .helpers.deprecation import ( # pylint: disable=import-outside-toplevel - check_if_deprecated_constant, - ) - - return check_if_deprecated_constant(name, module_globals) - - -# Can be removed if no deprecated constant are in this module anymore -def __dir__() -> list[str]: - """Return dir() with deprecated constants.""" - # Copied method from homeassistant.helpers.deprecattion#dir_with_deprecated_constants to avoid import cycle - module_globals = globals() - - return list(module_globals) + [ - name.removeprefix("_DEPRECATED_") - for name in module_globals - if name.startswith("_DEPRECATED_") - ] +# Both can be removed if no deprecated constant are in this module anymore +__getattr__ = functools.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = functools.partial(dir_with_deprecated_constants, module_globals=globals()) # How long to wait until things that run on startup have to finish. diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index efd0363732a..72b26e90b84 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -9,12 +9,6 @@ import inspect import logging from typing import Any, NamedTuple, ParamSpec, TypeVar -from homeassistant.core import HomeAssistant, async_get_hass -from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import async_suggest_report_issue - -from .frame import MissingIntegrationFrame, get_integration_frame - _ObjectT = TypeVar("_ObjectT", bound=object) _R = TypeVar("_R") _P = ParamSpec("_P") @@ -175,6 +169,13 @@ def _print_deprecation_warning_internal( *, log_when_no_integration_is_found: bool, ) -> None: + # pylint: disable=import-outside-toplevel + from homeassistant.core import HomeAssistant, async_get_hass + from homeassistant.exceptions import HomeAssistantError + from homeassistant.loader import async_suggest_report_issue + + from .frame import MissingIntegrationFrame, get_integration_frame + logger = logging.getLogger(module_name) if breaks_in_ha_version: breaks_in = f" which will be removed in HA Core {breaks_in_ha_version}" @@ -265,18 +266,6 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A f"{deprecated_const.enum.__class__.__name__}.{deprecated_const.enum.name}" ) breaks_in_ha_version = deprecated_const.breaks_in_ha_version - elif isinstance(deprecated_const, tuple): - # Use DeprecatedConstant and DeprecatedConstant instead, where possible - # Used to avoid import cycles. - if len(deprecated_const) == 3: - value = deprecated_const[0] - replacement = deprecated_const[1] - breaks_in_ha_version = deprecated_const[2] - elif len(deprecated_const) == 2 and isinstance(deprecated_const[0], Enum): - enum = deprecated_const[0] - value = enum.value - replacement = f"{enum.__class__.__name__}.{enum.name}" - breaks_in_ha_version = deprecated_const[1] if value is None or replacement is None: msg = ( diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index bd3546afb12..017e541bb08 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -299,22 +299,6 @@ def _get_value(obj: DeprecatedConstant | DeprecatedConstantEnum | tuple) -> Any: DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", ), - ( - ("value", "NEW_CONSTANT", None), - ". Use NEW_CONSTANT instead", - ), - ( - (1, "NEW_CONSTANT", "2099.1"), - " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", - ), - ( - (TestDeprecatedConstantEnum.TEST, None), - ". Use TestDeprecatedConstantEnum.TEST instead", - ), - ( - (TestDeprecatedConstantEnum.TEST, "2099.1"), - " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", - ), ], ) @pytest.mark.parametrize( @@ -391,22 +375,6 @@ def test_check_if_deprecated_constant( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", ), - ( - ("value", "NEW_CONSTANT", None), - ". Use NEW_CONSTANT instead", - ), - ( - (1, "NEW_CONSTANT", "2099.1"), - " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", - ), - ( - (TestDeprecatedConstantEnum.TEST, None), - ". Use TestDeprecatedConstantEnum.TEST instead", - ), - ( - (TestDeprecatedConstantEnum.TEST, "2099.1"), - " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", - ), ], ) @pytest.mark.parametrize( From d600b7680174854a41c7c37129434457744acc6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jan 2024 02:21:52 -1000 Subject: [PATCH 013/133] Fix missing backwards compatibility layer for water_heater supported_features (#107091) --- .../components/water_heater/__init__.py | 4 ++-- tests/components/water_heater/test_init.py | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index f2744416900..e5cf2cc2d3c 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -241,7 +241,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ), } - if WaterHeaterEntityFeature.OPERATION_MODE in self.supported_features: + if WaterHeaterEntityFeature.OPERATION_MODE in self.supported_features_compat: data[ATTR_OPERATION_LIST] = self.operation_list return data @@ -277,7 +277,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ), } - supported_features = self.supported_features + supported_features = self.supported_features_compat if WaterHeaterEntityFeature.OPERATION_MODE in supported_features: data[ATTR_OPERATION_MODE] = self.current_operation diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index 0d33f3a9e93..861be192340 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -8,10 +8,13 @@ import voluptuous as vol from homeassistant.components import water_heater from homeassistant.components.water_heater import ( + ATTR_OPERATION_LIST, + ATTR_OPERATION_MODE, SET_TEMPERATURE_SCHEMA, WaterHeaterEntity, WaterHeaterEntityFeature, ) +from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from tests.common import async_mock_service, import_and_test_deprecated_constant_enum @@ -117,21 +120,26 @@ def test_deprecated_constants( ) -def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> None: +def test_deprecated_supported_features_ints( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test deprecated supported features ints.""" class MockWaterHeaterEntity(WaterHeaterEntity): - @property - def supported_features(self) -> int: - """Return supported features.""" - return 1 + _attr_operation_list = ["mode1", "mode2"] + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_current_operation = "mode1" + _attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE.value entity = MockWaterHeaterEntity() - assert entity.supported_features_compat is WaterHeaterEntityFeature(1) + entity.hass = hass + assert entity.supported_features_compat is WaterHeaterEntityFeature(2) assert "MockWaterHeaterEntity" in caplog.text assert "is using deprecated supported features values" in caplog.text assert "Instead it should use" in caplog.text - assert "WaterHeaterEntityFeature.TARGET_TEMPERATURE" in caplog.text + assert "WaterHeaterEntityFeature.OPERATION_MODE" in caplog.text caplog.clear() - assert entity.supported_features_compat is WaterHeaterEntityFeature(1) + assert entity.supported_features_compat is WaterHeaterEntityFeature(2) assert "is using deprecated supported features values" not in caplog.text + assert entity.state_attributes[ATTR_OPERATION_MODE] == "mode1" + assert entity.capability_attributes[ATTR_OPERATION_LIST] == ["mode1", "mode2"] From a7aa5c0e522a0c01bfc6aa152035e4a974149702 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jan 2024 02:45:47 -1000 Subject: [PATCH 014/133] Bump habluetooth to 2.0.2 (#107097) --- .../components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_wrappers.py | 32 +++++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c5dec12fe40..7308f3a83ff 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -20,6 +20,6 @@ "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.0", - "habluetooth==2.0.1" + "habluetooth==2.0.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0f069a0e0b5..c5715c1c155 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ dbus-fast==2.21.0 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 -habluetooth==2.0.1 +habluetooth==2.0.2 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 diff --git a/requirements_all.txt b/requirements_all.txt index 11d12f195a8..7f5ef2a7b42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -998,7 +998,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.0.1 +habluetooth==2.0.2 # homeassistant.components.cloud hass-nabucasa==0.75.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 767511cc958..0d488cb8654 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -803,7 +803,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.0.1 +habluetooth==2.0.2 # homeassistant.components.cloud hass-nabucasa==0.75.1 diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index cc837f381d4..e3531a57447 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -367,6 +367,38 @@ async def test_we_switch_adapters_on_failure( cancel_hci1() +async def test_passing_subclassed_str_as_address( + hass: HomeAssistant, + two_adapters: None, + enable_bluetooth: None, + install_bleak_catcher, +) -> None: + """Ensure the client wrapper can handle a subclassed str as the address.""" + _, cancel_hci0, cancel_hci1 = _generate_scanners_with_fake_devices(hass) + + class SubclassedStr(str): + pass + + address = SubclassedStr("00:00:00:00:00:01") + client = bleak.BleakClient(address) + + class FakeBleakClient(BaseFakeBleakClient): + """Fake bleak client.""" + + async def connect(self, *args, **kwargs): + """Connect.""" + return True + + with patch( + "habluetooth.wrappers.get_platform_client_backend_type", + return_value=FakeBleakClient, + ): + assert await client.connect() is True + + cancel_hci0() + cancel_hci1() + + async def test_raise_after_shutdown( hass: HomeAssistant, two_adapters: None, From b8576b8091339cc7d485cc3d5166b83fd0d7108e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Jan 2024 11:46:45 +0100 Subject: [PATCH 015/133] Include deprecated constants in wildcard imports (#107114) --- .../components/alarm_control_panel/__init__.py | 17 +++++++++++------ .../components/alarm_control_panel/const.py | 12 ++++++++---- .../components/automation/__init__.py | 13 +++++++++---- .../components/binary_sensor/__init__.py | 13 +++++++++---- homeassistant/components/camera/__init__.py | 13 +++++++++---- homeassistant/components/camera/const.py | 8 ++++++-- homeassistant/components/climate/__init__.py | 17 +++++++++++------ homeassistant/components/climate/const.py | 8 ++++++-- homeassistant/components/cover/__init__.py | 13 +++++++++---- .../components/device_tracker/__init__.py | 17 +++++++++++------ .../components/device_tracker/const.py | 12 ++++++++---- homeassistant/components/fan/__init__.py | 13 +++++++++---- .../components/humidifier/__init__.py | 17 +++++++++++------ homeassistant/components/humidifier/const.py | 8 ++++++-- homeassistant/components/lock/__init__.py | 13 +++++++++---- homeassistant/components/number/const.py | 12 ++++++++---- homeassistant/components/remote/__init__.py | 13 +++++++++---- homeassistant/components/sensor/__init__.py | 17 +++++++++++------ homeassistant/components/sensor/const.py | 12 ++++++++---- homeassistant/components/siren/__init__.py | 17 +++++++++++------ homeassistant/components/siren/const.py | 8 ++++++-- homeassistant/components/switch/__init__.py | 13 +++++++++---- .../components/water_heater/__init__.py | 13 +++++++++---- homeassistant/const.py | 14 ++++++++------ homeassistant/core.py | 14 +++++++++----- homeassistant/data_entry_flow.py | 13 +++++++++---- homeassistant/helpers/deprecation.py | 18 +++++++++++++++--- homeassistant/helpers/device_registry.py | 13 +++++++++---- pyproject.toml | 6 +++++- tests/common.py | 14 ++++++++++++-- .../alarm_control_panel/test_init.py | 11 ++++++++++- tests/components/automation/test_init.py | 6 ++++++ tests/components/binary_sensor/test_init.py | 6 ++++++ tests/components/camera/test_init.py | 11 ++++++++++- tests/components/climate/test_init.py | 10 ++++++++++ tests/components/cover/test_init.py | 7 ++++++- tests/components/device_tracker/test_init.py | 10 ++++++++++ tests/components/fan/test_init.py | 7 ++++++- tests/components/humidifier/test_init.py | 11 ++++++++++- tests/components/lock/test_init.py | 7 ++++++- tests/components/number/test_const.py | 7 ++++++- tests/components/remote/test_init.py | 11 ++++++++++- tests/components/sensor/test_init.py | 10 ++++++++++ tests/components/siren/test_init.py | 11 ++++++++++- tests/components/switch/test_init.py | 11 ++++++++++- tests/components/water_heater/test_init.py | 11 ++++++++++- tests/helpers/test_deprecation.py | 6 +++--- tests/helpers/test_device_registry.py | 6 ++++++ tests/test_const.py | 6 ++++++ tests/test_core.py | 6 ++++++ tests/test_data_entry_flow.py | 11 ++++++++++- .../test_constant_deprecation/__init__.py | 2 +- 52 files changed, 438 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 9c53f2b7fd0..45e1d63e0c2 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.deprecation import ( + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -52,12 +53,6 @@ if TYPE_CHECKING: else: from homeassistant.backports.functools import cached_property -# As we import constants of the cost module here, we need to add the following -# functions to check for deprecated constants again -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - _LOGGER: Final = logging.getLogger(__name__) SCAN_INTERVAL: Final = timedelta(seconds=30) @@ -249,3 +244,13 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A ATTR_CHANGED_BY: self.changed_by, ATTR_CODE_ARM_REQUIRED: self.code_arm_required, } + + +# As we import constants of the const module here, we need to add the following +# functions to check for deprecated constants again +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index 90bbcba1314..fe4be649e19 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -5,6 +5,7 @@ from typing import Final from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -60,10 +61,6 @@ _DEPRECATED_SUPPORT_ALARM_ARM_VACATION: Final = DeprecatedConstantEnum( AlarmControlPanelEntityFeature.ARM_VACATION, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - CONDITION_TRIGGERED: Final = "is_triggered" CONDITION_DISARMED: Final = "is_disarmed" CONDITION_ARMED_HOME: Final = "is_armed_home" @@ -71,3 +68,10 @@ CONDITION_ARMED_AWAY: Final = "is_armed_away" CONDITION_ARMED_NIGHT: Final = "is_armed_night" CONDITION_ARMED_VACATION: Final = "is_armed_vacation" CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass" + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 4e6fa477ed2..efad44b15ef 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -58,6 +58,7 @@ from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import ( DeprecatedConstant, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -147,10 +148,6 @@ _DEPRECATED_AutomationTriggerInfo = DeprecatedConstant( TriggerInfo, "TriggerInfo", "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: @@ -1108,3 +1105,11 @@ def websocket_config( "config": automation.raw_config, }, ) + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 3a32a1afb57..06185489419 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -218,10 +219,6 @@ _DEPRECATED_DEVICE_CLASS_WINDOW = DeprecatedConstantEnum( BinarySensorDeviceClass.WINDOW, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - # mypy: disallow-any-generics @@ -303,3 +300,11 @@ class BinarySensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) if (is_on := self.is_on) is None: return None return STATE_ON if is_on else STATE_OFF + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 7a56292f7bb..ce75f064d47 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -54,6 +54,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -123,10 +124,6 @@ _DEPRECATED_SUPPORT_STREAM: Final = DeprecatedConstantEnum( CameraEntityFeature.STREAM, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"} DEFAULT_CONTENT_TYPE: Final = "image/jpeg" @@ -1082,3 +1079,11 @@ async def async_handle_record_service( duration=service_call.data[CONF_DURATION], lookback=service_call.data[CONF_LOOKBACK], ) + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index da41c0b9fab..09c4c7c1fb2 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -5,6 +5,7 @@ from typing import Final from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -47,6 +48,9 @@ _DEPRECATED_STREAM_TYPE_HLS = DeprecatedConstantEnum(StreamType.HLS, "2025.1") _DEPRECATED_STREAM_TYPE_WEB_RTC = DeprecatedConstantEnum(StreamType.WEB_RTC, "2025.1") -# Both can be removed if no deprecated constant are in this module anymore +# These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 78cb92944cb..c315765925f 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 make_entity_service_schema, ) from homeassistant.helpers.deprecation import ( + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -141,12 +142,6 @@ SET_TEMPERATURE_SCHEMA = vol.All( ), ) -# As we import deprecated constants from the const module, we need to add these two functions -# otherwise this module will be logged for using deprecated constants and not the custom component -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial(dir_with_deprecated_constants, module_globals=globals()) - # mypy: disallow-any-generics @@ -734,3 +729,13 @@ async def async_service_temperature_set( kwargs[value] = temp await entity.async_set_temperature(**kwargs) + + +# As we import deprecated constants from the const module, we need to add these two functions +# otherwise this module will be logged for using deprecated constants and not the custom component +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = ft.partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 615dc7d48dd..9c9153d9f63 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -5,6 +5,7 @@ from functools import partial from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -188,6 +189,9 @@ _DEPRECATED_SUPPORT_AUX_HEAT = DeprecatedConstantEnum( ClimateEntityFeature.AUX_HEAT, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore +# These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 3e438fb4ca1..945585de522 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -34,6 +34,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -143,10 +144,6 @@ _DEPRECATED_SUPPORT_SET_TILT_POSITION = DeprecatedConstantEnum( CoverEntityFeature.SET_TILT_POSITION, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial(dir_with_deprecated_constants, module_globals=globals()) - ATTR_CURRENT_POSITION = "current_position" ATTR_CURRENT_TILT_POSITION = "current_tilt_position" ATTR_POSITION = "position" @@ -493,3 +490,11 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if self._cover_is_last_toggle_direction_open: return fns["close"] return fns["open"] + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = ft.partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b5ad4660cde..adcc90cccbf 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -6,6 +6,7 @@ from functools import partial from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401 from homeassistant.core import HomeAssistant from homeassistant.helpers.deprecation import ( + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -57,12 +58,6 @@ from .legacy import ( # noqa: F401 see, ) -# As we import deprecated constants from the const module, we need to add these two functions -# otherwise this module will be logged for using deprecated constants and not the custom component -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: @@ -83,3 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_setup_legacy_integration(hass, config) return True + + +# As we import deprecated constants from the const module, we need to add these two functions +# otherwise this module will be logged for using deprecated constants and not the custom component +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 10c16e09107..67a90ab0f95 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -9,6 +9,7 @@ from typing import Final from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -44,10 +45,6 @@ _DEPRECATED_SOURCE_TYPE_BLUETOOTH_LE: Final = DeprecatedConstantEnum( SourceType.BLUETOOTH_LE, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - CONF_SCAN_INTERVAL: Final = "interval_seconds" SCAN_INTERVAL: Final = timedelta(seconds=12) @@ -71,3 +68,10 @@ ATTR_CONSIDER_HOME: Final = "consider_home" ATTR_IP: Final = "ip" CONNECTED_DEVICE_REGISTERED: Final = "device_tracker_connected_device_registered" + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index dedaedfe600..c35d828e398 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -76,10 +77,6 @@ _DEPRECATED_SUPPORT_PRESET_MODE = DeprecatedConstantEnum( FanEntityFeature.PRESET_MODE, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial(dir_with_deprecated_constants, module_globals=globals()) - SERVICE_INCREASE_SPEED = "increase_speed" SERVICE_DECREASE_SPEED = "decrease_speed" SERVICE_OSCILLATE = "oscillate" @@ -471,3 +468,11 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if hasattr(self, "_attr_preset_modes"): return self._attr_preset_modes return None + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = ft.partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 0d8f2e29561..65a8cd9d1d0 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.deprecation import ( + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -81,12 +82,6 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(HumidifierDeviceClass)) # use the HumidifierDeviceClass enum instead. DEVICE_CLASSES = [cls.value for cls in HumidifierDeviceClass] -# As we import deprecated constants from the const module, we need to add these two functions -# otherwise this module will be logged for using deprecated constants and not the custom component -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - # mypy: disallow-any-generics @@ -293,3 +288,13 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT self._report_deprecated_supported_features_values(new_features) return new_features return features + + +# As we import deprecated constants from the const module, we need to add these two functions +# otherwise this module will be logged for using deprecated constants and not the custom component +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index a1a219ddce7..66ac0fcf18d 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -5,6 +5,7 @@ from functools import partial from homeassistant.helpers.deprecation import ( DeprecatedConstant, DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -66,6 +67,9 @@ _DEPRECATED_SUPPORT_MODES = DeprecatedConstantEnum( HumidifierEntityFeature.MODES, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore +# These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index a9370f8d092..a4e7c4b7d1a 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -33,6 +33,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -70,10 +71,6 @@ class LockEntityFeature(IntFlag): # Please use the LockEntityFeature enum instead. _DEPRECATED_SUPPORT_OPEN = DeprecatedConstantEnum(LockEntityFeature.OPEN, "2025.1") -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial(dir_with_deprecated_constants, module_globals=globals()) - PROP_TO_ATTR = {"changed_by": ATTR_CHANGED_BY, "code_format": ATTR_CODE_FORMAT} # mypy: disallow-any-generics @@ -315,3 +312,11 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self._lock_option_default_code = "" + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = ft.partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 55d22c86648..a2d7c066af7 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -38,6 +38,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -70,10 +71,6 @@ _DEPRECATED_MODE_AUTO: Final = DeprecatedConstantEnum(NumberMode.AUTO, "2025.1") _DEPRECATED_MODE_BOX: Final = DeprecatedConstantEnum(NumberMode.BOX, "2025.1") _DEPRECATED_MODE_SLIDER: Final = DeprecatedConstantEnum(NumberMode.SLIDER, "2025.1") -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - class NumberDeviceClass(StrEnum): """Device class for numbers.""" @@ -481,3 +478,10 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { NumberDeviceClass.TEMPERATURE: TemperatureConverter, } + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 7e9ebfe12b9..c5facb9785c 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -27,6 +27,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -92,10 +93,6 @@ _DEPRECATED_SUPPORT_ACTIVITY = DeprecatedConstantEnum( ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial(dir_with_deprecated_constants, module_globals=globals()) - REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} ) @@ -262,3 +259,11 @@ class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) await self.hass.async_add_executor_job( ft.partial(self.delete_command, **kwargs) ) + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = ft.partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index d7c5cddc5db..6498b92b03e 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -59,6 +59,7 @@ from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.deprecation import ( + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -120,12 +121,6 @@ __all__ = [ "SensorStateClass", ] -# As we import deprecated constants from the const module, we need to add these two functions -# otherwise this module will be logged for using deprecated constants and not the custom component -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - # mypy: disallow-any-generics @@ -955,3 +950,13 @@ def async_rounded_state(hass: HomeAssistant, entity_id: str, state: State) -> st value = f"{numerical_value:z.{precision}f}" return value + + +# As we import deprecated constants from the const module, we need to add these two functions +# otherwise this module will be logged for using deprecated constants and not the custom component +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index d57a09981ef..b1cb120e3fe 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -38,6 +38,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -468,10 +469,6 @@ _DEPRECATED_STATE_CLASS_TOTAL_INCREASING: Final = DeprecatedConstantEnum( ) STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, SensorDeviceClass.CURRENT: ElectricCurrentConverter, @@ -631,3 +628,10 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorDeviceClass.WEIGHT: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.WIND_SPEED: {SensorStateClass.MEASUREMENT}, } + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index 29ad238ac00..fb41d5f7b48 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -17,6 +17,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.deprecation import ( + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -53,12 +54,6 @@ TURN_ON_SCHEMA = { vol.Optional(ATTR_VOLUME_LEVEL): cv.small_float, } -# As we import deprecated constants from the const module, we need to add these two functions -# otherwise this module will be logged for using deprecated constants and not the custom component -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - class SirenTurnOnServiceParameters(TypedDict, total=False): """Represent possible parameters to siren.turn_on service data dict type.""" @@ -218,3 +213,13 @@ class SirenEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self._report_deprecated_supported_features_values(new_features) return new_features return features + + +# As we import deprecated constants from the const module, we need to add these two functions +# otherwise this module will be logged for using deprecated constants and not the custom component +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/siren/const.py b/homeassistant/components/siren/const.py index 50c3af61c8d..9e46d8dc997 100644 --- a/homeassistant/components/siren/const.py +++ b/homeassistant/components/siren/const.py @@ -6,6 +6,7 @@ from typing import Final from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -47,6 +48,9 @@ _DEPRECATED_SUPPORT_DURATION: Final = DeprecatedConstantEnum( SirenEntityFeature.DURATION, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore +# These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index a318f763fcb..ce9b1477ad6 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -66,10 +67,6 @@ _DEPRECATED_DEVICE_CLASS_SWITCH = DeprecatedConstantEnum( SwitchDeviceClass.SWITCH, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - # mypy: disallow-any-generics @@ -133,3 +130,11 @@ class SwitchEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) if hasattr(self, "entity_description"): return self.entity_description.device_class return None + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index e5cf2cc2d3c..82a853125ff 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -86,10 +87,6 @@ _DEPRECATED_SUPPORT_AWAY_MODE = DeprecatedConstantEnum( WaterHeaterEntityFeature.AWAY_MODE, "2025.1" ) -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = ft.partial(dir_with_deprecated_constants, module_globals=globals()) - ATTR_MAX_TEMP = "max_temp" ATTR_MIN_TEMP = "min_temp" ATTR_AWAY_MODE = "away_mode" @@ -441,3 +438,11 @@ async def async_service_temperature_set( kwargs[value] = temp await entity.async_set_temperature(**kwargs) + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = ft.partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/const.py b/homeassistant/const.py index a6927aa8165..9e0505fadf3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,6 +8,7 @@ from typing import Final from .helpers.deprecation import ( DeprecatedConstant, DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -440,12 +441,6 @@ _DEPRECATED_DEVICE_CLASS_VOLTAGE: Final = DeprecatedConstant( "2025.1", ) - -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - - # #### STATES #### STATE_ON: Final = "on" STATE_OFF: Final = "off" @@ -1607,3 +1602,10 @@ SIGNAL_BOOTSTRAP_INTEGRATIONS = "bootstrap_integrations" FORMAT_DATE: Final = "%Y-%m-%d" FORMAT_TIME: Final = "%H:%M:%S" FORMAT_DATETIME: Final = f"{FORMAT_DATE} {FORMAT_TIME}" + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/core.py b/homeassistant/core.py index b15d393c63e..e843481f79d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -83,6 +83,7 @@ from .exceptions import ( ) from .helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -159,11 +160,6 @@ _DEPRECATED_SOURCE_STORAGE = DeprecatedConstantEnum(ConfigSource.STORAGE, "2025. _DEPRECATED_SOURCE_YAML = DeprecatedConstantEnum(ConfigSource.YAML, "2025.1") -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = functools.partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = functools.partial(dir_with_deprecated_constants, module_globals=globals()) - - # How long to wait until things that run on startup have to finish. TIMEOUT_EVENT_START = 15 @@ -2534,3 +2530,11 @@ class Config: if self._original_unit_system: data["unit_system"] = self._original_unit_system return await super().async_save(data) + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = functools.partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = functools.partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 5c9c0ff1ce4..63ba565582a 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -17,6 +17,7 @@ from .core import HomeAssistant, callback from .exceptions import HomeAssistantError from .helpers.deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -59,10 +60,6 @@ _DEPRECATED_RESULT_TYPE_SHOW_PROGRESS_DONE = DeprecatedConstantEnum( ) _DEPRECATED_RESULT_TYPE_MENU = DeprecatedConstantEnum(FlowResultType.MENU, "2025.1") -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - # Event that is fired when a flow is progressed via external or progress source. EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" @@ -700,3 +697,11 @@ def _create_abort_data( reason=reason, description_placeholders=description_placeholders, ) + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 72b26e90b84..18a42ce9bcf 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -292,10 +292,22 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A return value -def dir_with_deprecated_constants(module_globals: dict[str, Any]) -> list[str]: +def dir_with_deprecated_constants(module_globals_keys: list[str]) -> list[str]: """Return dir() with deprecated constants.""" - return list(module_globals) + [ + return module_globals_keys + [ name.removeprefix(_PREFIX_DEPRECATED) - for name in module_globals + for name in module_globals_keys + if name.startswith(_PREFIX_DEPRECATED) + ] + + +def all_with_deprecated_constants(module_globals: dict[str, Any]) -> list[str]: + """Generate a list for __all___ with deprecated constants.""" + # Iterate over a copy in case the globals dict is mutated by another thread + # while we loop over it. + module_globals_keys = list(module_globals) + return [itm for itm in module_globals_keys if not itm.startswith("_")] + [ + name.removeprefix(_PREFIX_DEPRECATED) + for name in module_globals_keys if name.startswith(_PREFIX_DEPRECATED) ] diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index bd509cb47ec..cfe3b78ebab 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -24,6 +24,7 @@ from . import storage from .debounce import Debouncer from .deprecation import ( DeprecatedConstantEnum, + all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, ) @@ -75,10 +76,6 @@ _DEPRECATED_DISABLED_INTEGRATION = DeprecatedConstantEnum( ) _DEPRECATED_DISABLED_USER = DeprecatedConstantEnum(DeviceEntryDisabler.USER, "2025.1") -# Both can be removed if no deprecated constant are in this module anymore -__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) -__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) - class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" @@ -1113,3 +1110,11 @@ def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, (key, format_mac(value)) if key == CONNECTION_NETWORK_MAC else (key, value) for key, value in connections } + + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/pyproject.toml b/pyproject.toml index ec313a5bcf6..2fc5f594157 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,7 +176,8 @@ disable = [ "duplicate-bases", # PLE0241 "format-needs-mapping", # F502 "function-redefined", # F811 - "invalid-all-format", # PLE0605 + # Needed because ruff does not understand type of __all__ generated by a function + # "invalid-all-format", # PLE0605 "invalid-all-object", # PLE0604 "invalid-character-backspace", # PLE2510 "invalid-character-esc", # PLE2513 @@ -673,6 +674,9 @@ ignore = [ "COM819", "ISC001", "ISC002", + + # Disabled because ruff does not understand type of __all__ generated by a function + "PLE0605", ] [tool.ruff.flake8-import-conventions.extend-aliases] diff --git a/tests/common.py b/tests/common.py index b07788dc3d7..85193022e4f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -92,7 +92,7 @@ import homeassistant.util.uuid as uuid_util import homeassistant.util.yaml.loader as yaml_loader from tests.testing_config.custom_components.test_constant_deprecation import ( - import_deprecated_costant, + import_deprecated_constant, ) _LOGGER = logging.getLogger(__name__) @@ -1482,6 +1482,7 @@ def import_and_test_deprecated_constant_enum( - Assert value is the same as the replacement - Assert a warning is logged - Assert the deprecated constant is included in the modules.__dir__() + - Assert the deprecated constant is included in the modules.__all__() """ import_and_test_deprecated_constant( caplog, @@ -1507,8 +1508,9 @@ def import_and_test_deprecated_constant( - Assert value is the same as the replacement - Assert a warning is logged - Assert the deprecated constant is included in the modules.__dir__() + - Assert the deprecated constant is included in the modules.__all__() """ - value = import_deprecated_costant(module, constant_name) + value = import_deprecated_constant(module, constant_name) assert value == replacement assert ( module.__name__, @@ -1523,3 +1525,11 @@ def import_and_test_deprecated_constant( # verify deprecated constant is included in dir() assert constant_name in dir(module) + assert constant_name in module.__all__ + + +def help_test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + assert set(module.__all__) == { + itm for itm in module.__dir__() if not itm.startswith("_") + } diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py index 1e6fce6def6..42a532cbb1a 100644 --- a/tests/components/alarm_control_panel/test_init.py +++ b/tests/components/alarm_control_panel/test_init.py @@ -6,7 +6,16 @@ import pytest from homeassistant.components import alarm_control_panel -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum + + +@pytest.mark.parametrize( + "module", + [alarm_control_panel, alarm_control_panel.const], +) +def test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + help_test_all(module) @pytest.mark.parametrize( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 235ca48f095..6bb1b89259a 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -59,6 +59,7 @@ from tests.common import ( async_capture_events, async_fire_time_changed, async_mock_service, + help_test_all, import_and_test_deprecated_constant, mock_restore_cache, ) @@ -2569,6 +2570,11 @@ async def test_websocket_config( assert msg["error"]["code"] == "not_found" +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(automation) + + @pytest.mark.parametrize( ("constant_name", "replacement"), [ diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 014722d94a4..6ca189113b9 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -14,6 +14,7 @@ from tests.common import ( MockConfigEntry, MockModule, MockPlatform, + help_test_all, import_and_test_deprecated_constant_enum, mock_config_flow, mock_integration, @@ -197,6 +198,11 @@ async def test_entity_category_config_raises_error( ) +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(binary_sensor) + + @pytest.mark.parametrize( "device_class", list(binary_sensor.BinarySensorDeviceClass), diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 0e761f2f437..f1e3a4fdef5 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -27,7 +27,7 @@ from homeassistant.setup import async_setup_component from .common import EMPTY_8_6_JPEG, WEBRTC_ANSWER, mock_turbo_jpeg -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum from tests.typing import ClientSessionGenerator, WebSocketGenerator STREAM_SOURCE = "rtsp://127.0.0.1/stream" @@ -962,6 +962,15 @@ async def test_use_stream_for_stills( assert await resp.read() == b"stream_keyframe_image" +@pytest.mark.parametrize( + "module", + [camera, camera.const], +) +def test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + help_test_all(module) + + @pytest.mark.parametrize( "enum", list(camera.const.StreamType), diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 8fc82365c23..89826c98086 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -36,6 +36,7 @@ from tests.common import ( MockModule, MockPlatform, async_mock_service, + help_test_all, import_and_test_deprecated_constant, import_and_test_deprecated_constant_enum, mock_integration, @@ -157,6 +158,15 @@ def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: return result +@pytest.mark.parametrize( + "module", + [climate, climate.const], +) +def test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + help_test_all(module) + + @pytest.mark.parametrize( ("enum", "constant_prefix"), _create_tuples(climate.ClimateEntityFeature, "SUPPORT_") diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 1b08658d983..480d1ef83aa 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -> None: @@ -127,6 +127,11 @@ def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: return result +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(cover) + + @pytest.mark.parametrize( ("enum", "constant_prefix"), _create_tuples(cover.CoverEntityFeature, "SUPPORT_") diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 024187a33f6..eb8fde8f0e2 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -34,6 +34,7 @@ from . import common from tests.common import ( assert_setup_component, async_fire_time_changed, + help_test_all, import_and_test_deprecated_constant_enum, mock_registry, mock_restore_cache, @@ -685,6 +686,15 @@ def test_see_schema_allowing_ios_calls() -> None: ) +@pytest.mark.parametrize( + "module", + [device_tracker, device_tracker.const], +) +def test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + help_test_all(module) + + @pytest.mark.parametrize(("enum"), list(SourceType)) @pytest.mark.parametrize( "module", diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 828c13b6f16..1beea47c6fa 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum from tests.testing_config.custom_components.test.fan import MockFan @@ -150,6 +150,11 @@ async def test_preset_mode_validation( assert exc.value.translation_key == "not_valid_preset_mode" +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(fan) + + @pytest.mark.parametrize(("enum"), list(fan.FanEntityFeature)) def test_deprecated_constants( caplog: pytest.LogCaptureFixture, diff --git a/tests/components/humidifier/test_init.py b/tests/components/humidifier/test_init.py index 24cf4b6d962..3ef3fca8589 100644 --- a/tests/components/humidifier/test_init.py +++ b/tests/components/humidifier/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.humidifier import ( ) from homeassistant.core import HomeAssistant -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum class MockHumidifierEntity(HumidifierEntity): @@ -54,6 +54,15 @@ def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: return result +@pytest.mark.parametrize( + "module", + [humidifier, humidifier.const], +) +def test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + help_test_all(module) + + @pytest.mark.parametrize( ("enum", "constant_prefix"), _create_tuples(humidifier.HumidifierEntityFeature, "SUPPORT_") diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index 854b89fd1d8..7ebb5bf3027 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -28,7 +28,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from .conftest import MockLock -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum async def help_test_async_lock_service( @@ -371,6 +371,11 @@ async def test_lock_with_illegal_default_code( assert exc.value.translation_key == "add_default_code" +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(lock) + + @pytest.mark.parametrize(("enum"), list(LockEntityFeature)) def test_deprecated_constants( caplog: pytest.LogCaptureFixture, diff --git a/tests/components/number/test_const.py b/tests/components/number/test_const.py index e4b47e17e6e..13d94e2eeaf 100644 --- a/tests/components/number/test_const.py +++ b/tests/components/number/test_const.py @@ -4,7 +4,12 @@ import pytest from homeassistant.components.number import const -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum + + +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(const) @pytest.mark.parametrize(("enum"), list(const.NumberMode)) diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index a75ff858483..be4a4843097 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -22,7 +22,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from tests.common import async_mock_service, import_and_test_deprecated_constant_enum +from tests.common import ( + async_mock_service, + help_test_all, + import_and_test_deprecated_constant_enum, +) TEST_PLATFORM = {DOMAIN: {CONF_PLATFORM: "test"}} SERVICE_SEND_COMMAND = "send_command" @@ -143,6 +147,11 @@ async def test_delete_command(hass: HomeAssistant) -> None: assert call.data[ATTR_ENTITY_ID] == ENTITY_ID +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(remote) + + @pytest.mark.parametrize(("enum"), list(remote.RemoteEntityFeature)) def test_deprecated_constants( caplog: pytest.LogCaptureFixture, diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 829bb5af827..522afe3b992 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -52,6 +52,7 @@ from tests.common import ( MockModule, MockPlatform, async_mock_restore_state_shutdown_restart, + help_test_all, import_and_test_deprecated_constant_enum, mock_config_flow, mock_integration, @@ -2524,6 +2525,15 @@ async def test_entity_category_config_raises_error( assert not hass.states.get("sensor.test") +@pytest.mark.parametrize( + "module", + [sensor, sensor.const], +) +def test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + help_test_all(module) + + @pytest.mark.parametrize(("enum"), list(sensor.SensorStateClass)) @pytest.mark.parametrize(("module"), [sensor, sensor.const]) def test_deprecated_constants( diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index abc5b0fac38..1cf44d16ea0 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.siren import ( from homeassistant.components.siren.const import SirenEntityFeature from homeassistant.core import HomeAssistant -from tests.common import import_and_test_deprecated_constant_enum +from tests.common import help_test_all, import_and_test_deprecated_constant_enum class MockSirenEntity(SirenEntity): @@ -110,6 +110,15 @@ async def test_missing_tones_dict(hass: HomeAssistant) -> None: process_turn_on_params(siren, {"tone": 3}) +@pytest.mark.parametrize( + "module", + [siren, siren.const], +) +def test_all(module: ModuleType) -> None: + """Test module.__all__ is correctly set.""" + help_test_all(module) + + @pytest.mark.parametrize(("enum"), list(SirenEntityFeature)) @pytest.mark.parametrize(("module"), [siren, siren.const]) def test_deprecated_constants( diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 7a43e0bf50e..deb7acb512a 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -9,7 +9,11 @@ from homeassistant.setup import async_setup_component from . import common -from tests.common import MockUser, import_and_test_deprecated_constant_enum +from tests.common import ( + MockUser, + help_test_all, + import_and_test_deprecated_constant_enum, +) @pytest.fixture(autouse=True) @@ -82,6 +86,11 @@ async def test_switch_context( assert state2.context.user_id == hass_admin_user.id +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(switch) + + @pytest.mark.parametrize(("enum"), list(switch.SwitchDeviceClass)) def test_deprecated_constants( caplog: pytest.LogCaptureFixture, diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index 861be192340..b81ef369452 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -17,7 +17,11 @@ from homeassistant.components.water_heater import ( from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant -from tests.common import async_mock_service, import_and_test_deprecated_constant_enum +from tests.common import ( + async_mock_service, + help_test_all, + import_and_test_deprecated_constant_enum, +) async def test_set_temp_schema_no_req( @@ -102,6 +106,11 @@ async def test_sync_turn_off(hass: HomeAssistant) -> None: assert water_heater.async_turn_off.call_count == 1 +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(water_heater) + + @pytest.mark.parametrize( ("enum"), [ diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index 017e541bb08..25b37e2073f 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -429,7 +429,7 @@ def test_test_check_if_deprecated_constant_invalid( @pytest.mark.parametrize( - ("module_global", "expected"), + ("module_globals", "expected"), [ ({"CONSTANT": 1}, ["CONSTANT"]), ({"_DEPRECATED_CONSTANT": 1}, ["_DEPRECATED_CONSTANT", "CONSTANT"]), @@ -440,7 +440,7 @@ def test_test_check_if_deprecated_constant_invalid( ], ) def test_dir_with_deprecated_constants( - module_global: dict[str, Any], expected: list[str] + module_globals: dict[str, Any], expected: list[str] ) -> None: """Test dir() with deprecated constants.""" - assert dir_with_deprecated_constants(module_global) == expected + assert dir_with_deprecated_constants([*module_globals.keys()]) == expected diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 43540a52f7d..240afa2cbab 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -20,6 +20,7 @@ from homeassistant.helpers import ( from tests.common import ( MockConfigEntry, flush_store, + help_test_all, import_and_test_deprecated_constant_enum, ) @@ -2018,6 +2019,11 @@ async def test_loading_invalid_configuration_url_from_storage( assert entry.configuration_url == "invalid" +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(dr) + + @pytest.mark.parametrize(("enum"), list(dr.DeviceEntryDisabler)) def test_deprecated_constants( caplog: pytest.LogCaptureFixture, diff --git a/tests/test_const.py b/tests/test_const.py index fedf35ae6d1..4b9be4f27f1 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -9,6 +9,7 @@ from homeassistant import const from homeassistant.components import sensor from tests.common import ( + help_test_all, import_and_test_deprecated_constant, import_and_test_deprecated_constant_enum, ) @@ -23,6 +24,11 @@ def _create_tuples( return result +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(const) + + @pytest.mark.parametrize( ("enum", "constant_prefix"), _create_tuples(const.EntityCategory, "ENTITY_CATEGORY_") diff --git a/tests/test_core.py b/tests/test_core.py index 90b87068a5d..bbd27151243 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -61,6 +61,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM from .common import ( async_capture_events, async_mock_service, + help_test_all, import_and_test_deprecated_constant_enum, ) @@ -2630,6 +2631,11 @@ async def test_cancel_shutdown_job(hass: HomeAssistant) -> None: assert not evt.is_set() +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(ha) + + @pytest.mark.parametrize( ("enum"), [ diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index eb507febe8a..602b21c15bc 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -10,7 +10,11 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.core import HomeAssistant from homeassistant.util.decorator import Registry -from .common import async_capture_events, import_and_test_deprecated_constant_enum +from .common import ( + async_capture_events, + help_test_all, + import_and_test_deprecated_constant_enum, +) @pytest.fixture @@ -804,6 +808,11 @@ async def test_find_flows_by_init_data_type( assert len(manager.async_progress()) == 0 +def test_all() -> None: + """Test module.__all__ is correctly set.""" + help_test_all(data_entry_flow) + + @pytest.mark.parametrize(("enum"), list(data_entry_flow.FlowResultType)) def test_deprecated_constants( caplog: pytest.LogCaptureFixture, diff --git a/tests/testing_config/custom_components/test_constant_deprecation/__init__.py b/tests/testing_config/custom_components/test_constant_deprecation/__init__.py index 4367cbed7b1..b061b9c35fc 100644 --- a/tests/testing_config/custom_components/test_constant_deprecation/__init__.py +++ b/tests/testing_config/custom_components/test_constant_deprecation/__init__.py @@ -4,6 +4,6 @@ from types import ModuleType from typing import Any -def import_deprecated_costant(module: ModuleType, constant_name: str) -> Any: +def import_deprecated_constant(module: ModuleType, constant_name: str) -> Any: """Import and return deprecated constant.""" return getattr(module, constant_name) From 04bf56930891420c0b61997aa20960e7ea5aad3c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 4 Jan 2024 19:45:18 +0100 Subject: [PATCH 016/133] Update frontend to 20240104.0 (#107155) --- 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 52f3932237b..ad24f6bb12d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240103.3"] + "requirements": ["home-assistant-frontend==20240104.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c5715c1c155..655f46a8838 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ habluetooth==2.0.2 hass-nabucasa==0.75.1 hassil==1.5.1 home-assistant-bluetooth==1.11.0 -home-assistant-frontend==20240103.3 +home-assistant-frontend==20240104.0 home-assistant-intents==2024.1.2 httpx==0.26.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7f5ef2a7b42..0c61bd87ebe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240103.3 +home-assistant-frontend==20240104.0 # homeassistant.components.conversation home-assistant-intents==2024.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d488cb8654..6602d21b6a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -831,7 +831,7 @@ hole==0.8.0 holidays==0.39 # homeassistant.components.frontend -home-assistant-frontend==20240103.3 +home-assistant-frontend==20240104.0 # homeassistant.components.conversation home-assistant-intents==2024.1.2 From c78d691d30c5cb39aa4729e65c8f673a21d99297 Mon Sep 17 00:00:00 2001 From: Matt Emerick-Law Date: Thu, 4 Jan 2024 19:41:12 +0000 Subject: [PATCH 017/133] Bump Orvibo to 1.1.2 (#107162) * Bump python-orvibo version Fixes https://github.com/home-assistant/core/issues/106923 * Add version number * Remove version * Bump python-orvibo version --- homeassistant/components/orvibo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 72cdc4118df..05ce5edd8bd 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/orvibo", "iot_class": "local_push", "loggers": ["orvibo"], - "requirements": ["orvibo==1.1.1"] + "requirements": ["orvibo==1.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0c61bd87ebe..48658c9dda1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,7 +1443,7 @@ oralb-ble==0.17.6 oru==0.1.11 # homeassistant.components.orvibo -orvibo==1.1.1 +orvibo==1.1.2 # homeassistant.components.ourgroceries ourgroceries==1.5.4 From 3215dfee6db673b752872523ef1552dd06adc143 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Jan 2024 11:21:01 -1000 Subject: [PATCH 018/133] Bump aiohomekit to 3.1.2 (#107177) --- 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 edb81c14a72..4af79a6f811 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.1.1"], + "requirements": ["aiohomekit==3.1.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 48658c9dda1..36944f26f18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.1 +aiohomekit==3.1.2 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6602d21b6a4..1fa9f2d122e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.1 +aiohomekit==3.1.2 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 From d7e1a4fa2090c389700e567f2dd909093e62abdb Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 4 Jan 2024 23:41:56 +0100 Subject: [PATCH 019/133] Bump to PyTado 0.17.3 (#107181) --- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 467697fc810..bae637f3180 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.17.0"] + "requirements": ["python-tado==0.17.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 36944f26f18..b3ad3fac35c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2241,7 +2241,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.17.0 +python-tado==0.17.3 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fa9f2d122e..fea1828bd2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1696,7 +1696,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.17.0 +python-tado==0.17.3 # homeassistant.components.telegram_bot python-telegram-bot==13.1 From 4e126d68b79c496beb09feb61727019d97c1cbaa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 4 Jan 2024 23:36:36 +0100 Subject: [PATCH 020/133] Fix switch states in AVM FRITZ!Box Tools (#107183) --- homeassistant/components/fritz/common.py | 1 + homeassistant/components/fritz/switch.py | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 63f9f593ea8..bad73d91320 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -1063,6 +1063,7 @@ class SwitchInfo(TypedDict): type: str callback_update: Callable callback_switch: Callable + init_state: bool class FritzBoxBaseEntity: diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 026c0f3d6fb..c3da6b5af0b 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -166,9 +166,7 @@ async def _async_wifi_entities_list( _LOGGER.debug("WiFi networks list: %s", networks) return [ - FritzBoxWifiSwitch( - avm_wrapper, device_friendly_name, index, data["switch_name"] - ) + FritzBoxWifiSwitch(avm_wrapper, device_friendly_name, index, data) for index, data in networks.items() ] @@ -310,18 +308,16 @@ class FritzBoxBaseCoordinatorSwitch(CoordinatorEntity[AvmWrapper], SwitchEntity) await self._async_handle_turn_on_off(turn_on=False) -class FritzBoxBaseSwitch(FritzBoxBaseEntity): +class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity): """Fritz switch base class.""" - _attr_is_on: bool | None = False - def __init__( self, avm_wrapper: AvmWrapper, device_friendly_name: str, switch_info: SwitchInfo, ) -> None: - """Init Fritzbox port switch.""" + """Init Fritzbox base switch.""" super().__init__(avm_wrapper, device_friendly_name) self._description = switch_info["description"] @@ -330,6 +326,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity): self._type = switch_info["type"] self._update = switch_info["callback_update"] self._switch = switch_info["callback_switch"] + self._attr_is_on = switch_info["init_state"] self._name = f"{self._friendly_name} {self._description}" self._unique_id = f"{self._avm_wrapper.unique_id}-{slugify(self._description)}" @@ -381,7 +378,7 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity): self._attr_is_on = turn_on -class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): +class FritzBoxPortSwitch(FritzBoxBaseSwitch): """Defines a FRITZ!Box Tools PortForward switch.""" def __init__( @@ -412,6 +409,7 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): type=SWITCH_TYPE_PORTFORWARD, callback_update=self._async_fetch_update, callback_switch=self._async_switch_on_off_executor, + init_state=port_mapping["NewEnabled"], ) super().__init__(avm_wrapper, device_friendly_name, switch_info) @@ -553,7 +551,7 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): return True -class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): +class FritzBoxWifiSwitch(FritzBoxBaseSwitch): """Defines a FRITZ!Box Tools Wifi switch.""" def __init__( @@ -561,7 +559,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): avm_wrapper: AvmWrapper, device_friendly_name: str, network_num: int, - network_name: str, + network_data: dict, ) -> None: """Init Fritz Wifi switch.""" self._avm_wrapper = avm_wrapper @@ -571,12 +569,13 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): self._network_num = network_num switch_info = SwitchInfo( - description=f"Wi-Fi {network_name}", + description=f"Wi-Fi {network_data['switch_name']}", friendly_name=device_friendly_name, icon="mdi:wifi", type=SWITCH_TYPE_WIFINETWORK, callback_update=self._async_fetch_update, callback_switch=self._async_switch_on_off_executor, + init_state=network_data["enabled"], ) super().__init__(self._avm_wrapper, device_friendly_name, switch_info) From c242dcd1f20b48db9e3734bbab7d5374e3216828 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 5 Jan 2024 17:23:43 +1000 Subject: [PATCH 021/133] Hotfix cache logic bug in Tessie (#107187) --- homeassistant/components/tessie/coordinator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index c2f53da53bc..75cac088bde 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -41,7 +41,6 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.vin = vin self.session = async_get_clientsession(hass) self.data = self._flatten(data) - self.did_first_update = False async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using Tessie API.""" @@ -50,7 +49,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): session=self.session, api_key=self.api_key, vin=self.vin, - use_cache=self.did_first_update, + use_cache=False, ) except ClientResponseError as e: if e.status == HTTPStatus.UNAUTHORIZED: @@ -58,7 +57,6 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from e raise e - self.did_first_update = True if vehicle["state"] == TessieStatus.ONLINE: # Vehicle is online, all data is fresh return self._flatten(vehicle) From 4ade5e46d9e3c4b5a8a3b3d88b40286cd1ae304b Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 4 Jan 2024 17:07:15 -0800 Subject: [PATCH 022/133] Disable IPv6 in the opower integration to fix AEP utilities (#107203) --- homeassistant/components/opower/config_flow.py | 3 ++- homeassistant/components/opower/coordinator.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index d456fc536e5..ab1fbbe36e3 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging +import socket from typing import Any from opower import ( @@ -38,7 +39,7 @@ async def _validate_login( ) -> dict[str, str]: """Validate login data and return any errors.""" api = Opower( - async_create_clientsession(hass), + async_create_clientsession(hass, family=socket.AF_INET), login_data[CONF_UTILITY], login_data[CONF_USERNAME], login_data[CONF_PASSWORD], diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index a474255e34d..73c60068cd4 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -1,6 +1,7 @@ """Coordinator to handle Opower connections.""" from datetime import datetime, timedelta import logging +import socket from types import MappingProxyType from typing import Any, cast @@ -51,7 +52,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): update_interval=timedelta(hours=12), ) self.api = Opower( - aiohttp_client.async_get_clientsession(hass), + aiohttp_client.async_get_clientsession(hass, family=socket.AF_INET), entry_data[CONF_UTILITY], entry_data[CONF_USERNAME], entry_data[CONF_PASSWORD], From c3963b26e70ef795861ce89baef9a92208fb72b7 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Fri, 5 Jan 2024 08:18:25 +0000 Subject: [PATCH 023/133] Fix entity property cache creation arguments (#107221) --- homeassistant/helpers/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 3c3c8474e67..743d3675a3b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -292,7 +292,7 @@ class CachedProperties(type): Pop cached_properties and store it in the namespace. """ namespace["_CachedProperties__cached_properties"] = cached_properties or set() - return super().__new__(mcs, name, bases, namespace) + return super().__new__(mcs, name, bases, namespace, **kwargs) def __init__( cls, From 056701d21822de717fee8ecc48184600cf494cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 5 Jan 2024 10:38:54 +0100 Subject: [PATCH 024/133] Use supported_features_compat in update.install service (#107224) --- homeassistant/components/update/__init__.py | 4 +- tests/components/update/test_init.py | 72 +++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 40431332aaf..8ec14b6e3a8 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -140,7 +140,7 @@ async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None # If version is specified, but not supported by the entity. if ( version is not None - and UpdateEntityFeature.SPECIFIC_VERSION not in entity.supported_features + and UpdateEntityFeature.SPECIFIC_VERSION not in entity.supported_features_compat ): raise HomeAssistantError( f"Installing a specific version is not supported for {entity.entity_id}" @@ -149,7 +149,7 @@ async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None # If backup is requested, but not supported by the entity. if ( backup := service_call.data[ATTR_BACKUP] - ) and UpdateEntityFeature.BACKUP not in entity.supported_features: + ) and UpdateEntityFeature.BACKUP not in entity.supported_features_compat: raise HomeAssistantError(f"Backup is not supported for {entity.entity_id}") # Update is already in progress. diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 92e63af4b6f..67661d6936e 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -885,3 +885,75 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> caplog.clear() assert entity.supported_features_compat is UpdateEntityFeature(1) assert "is using deprecated supported features values" not in caplog.text + + +async def test_deprecated_supported_features_ints_with_service_call( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test deprecated supported features ints with install service.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + class MockUpdateEntity(UpdateEntity): + _attr_supported_features = 1 | 2 + + def install(self, version: str | None = None, backup: bool = False) -> None: + """Install an update.""" + + entity = MockUpdateEntity() + entity.entity_id = ( + "update.test_deprecated_supported_features_ints_with_service_call" + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test update platform via config entry.""" + async_add_entities([entity]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert "is using deprecated supported features values" in caplog.text + + assert isinstance(entity.supported_features, int) + + with pytest.raises( + HomeAssistantError, + match="Backup is not supported for update.test_deprecated_supported_features_ints_with_service_call", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + { + ATTR_VERSION: "0.9.9", + ATTR_BACKUP: True, + ATTR_ENTITY_ID: "update.test_deprecated_supported_features_ints_with_service_call", + }, + blocking=True, + ) From d012817190520c43890616c4a96701fcc1158920 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 5 Jan 2024 06:38:00 -0500 Subject: [PATCH 025/133] Bump zwave-js-server-python to 0.55.3 (#107225) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_events.py | 11 ++++++++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 9a66dae8e93..a06de5cb8ee 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.55.2"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.55.3"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index b3ad3fac35c..012c601b785 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2893,7 +2893,7 @@ zigpy==0.60.4 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.2 +zwave-js-server-python==0.55.3 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fea1828bd2d..96775e05791 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2189,7 +2189,7 @@ zigpy-znp==0.12.1 zigpy==0.60.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.2 +zwave-js-server-python==0.55.3 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 4fbaa97f118..1e91b9338fa 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -348,7 +348,11 @@ async def test_power_level_notification( async def test_unknown_notification( - hass: HomeAssistant, hank_binary_switch, integration, client + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + hank_binary_switch, + integration, + client, ) -> None: """Test behavior of unknown notification type events.""" # just pick a random node to fake the notification event @@ -358,8 +362,9 @@ async def test_unknown_notification( # by the lib. We will use a class that is guaranteed not to be recognized notification_obj = AsyncMock() notification_obj.node = node - with pytest.raises(TypeError): - node.emit("notification", {"notification": notification_obj}) + node.emit("notification", {"notification": notification_obj}) + + assert f"Unhandled notification type: {notification_obj}" in caplog.text notification_events = async_capture_events(hass, "zwave_js_notification") From 658f1cf5c5e83946e9ca394542ccbc70de212344 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Jan 2024 13:01:35 +0100 Subject: [PATCH 026/133] Bump version to 2024.1.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 9e0505fadf3..cea73ec243b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 2fc5f594157..3bec11ced3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.0" +version = "2024.1.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 97674cee88dd090e289d2bf8d9c61cf136a59dc0 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 6 Jan 2024 09:06:23 +0000 Subject: [PATCH 027/133] Fix support for play/pause functionality in System Bridge (#103423) Fix support for play/pause functionality --- homeassistant/components/system_bridge/media_player.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/system_bridge/media_player.py b/homeassistant/components/system_bridge/media_player.py index ea9e8ab070d..02670d36fe3 100644 --- a/homeassistant/components/system_bridge/media_player.py +++ b/homeassistant/components/system_bridge/media_player.py @@ -118,10 +118,8 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): features |= MediaPlayerEntityFeature.PREVIOUS_TRACK if data.media.is_next_enabled: features |= MediaPlayerEntityFeature.NEXT_TRACK - if data.media.is_pause_enabled: - features |= MediaPlayerEntityFeature.PAUSE - if data.media.is_play_enabled: - features |= MediaPlayerEntityFeature.PLAY + if data.media.is_pause_enabled or data.media.is_play_enabled: + features |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY if data.media.is_stop_enabled: features |= MediaPlayerEntityFeature.STOP From 5ff6284e0fc968172626f481add6c4b0b3920f5d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 6 Jan 2024 10:50:06 +0100 Subject: [PATCH 028/133] Fix passing correct location id to streamlabs water (#107291) --- homeassistant/components/streamlabswater/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 82e8777a7e1..c3bbe5a96d4 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -107,9 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def set_away_mode(service: ServiceCall) -> None: """Set the StreamLabsWater Away Mode.""" away_mode = service.data.get(ATTR_AWAY_MODE) - location_id = ( - service.data.get(CONF_LOCATION_ID) or list(coordinator.data.values())[0] - ) + location_id = service.data.get(CONF_LOCATION_ID) or list(coordinator.data)[0] client.update_location(location_id, away_mode) hass.services.async_register( From 0dbb4105bc0c49ff52ea43ea29c334a2d2177b80 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 6 Jan 2024 01:32:04 +0200 Subject: [PATCH 029/133] Fix Shelly missing Gen value for older devices (#107294) --- homeassistant/components/shelly/config_flow.py | 7 ++++--- homeassistant/components/shelly/coordinator.py | 9 ++++++--- tests/components/shelly/__init__.py | 6 ++++-- tests/components/shelly/test_init.py | 8 ++++++++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 29daf050163..59ae6eed196 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -36,6 +36,7 @@ from .coordinator import async_reconnect_soon from .utils import ( get_block_device_sleep_period, get_coap_context, + get_device_entry_gen, get_info_auth, get_info_gen, get_model_name, @@ -322,7 +323,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") - if self.entry.data.get(CONF_GEN, 1) != 1: + if get_device_entry_gen(self.entry) != 1: user_input[CONF_USERNAME] = "admin" try: await validate_input(self.hass, host, info, user_input) @@ -335,7 +336,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_abort(reason="reauth_successful") - if self.entry.data.get(CONF_GEN, 1) in BLOCK_GENERATIONS: + if get_device_entry_gen(self.entry) in BLOCK_GENERATIONS: schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, @@ -364,7 +365,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" return ( - config_entry.data.get(CONF_GEN) in RPC_GENERATIONS + get_device_entry_gen(config_entry) in RPC_GENERATIONS and not config_entry.data.get(CONF_SLEEP_PERIOD) and config_entry.data.get("model") != MODEL_WALL_DISPLAY ) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 77fa0bd2efd..7f88cce1134 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -33,7 +33,6 @@ from .const import ( ATTR_GENERATION, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, CONF_BLE_SCANNER_MODE, - CONF_GEN, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DOMAIN, @@ -58,7 +57,11 @@ from .const import ( UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) -from .utils import get_rpc_device_wakeup_period, update_device_fw_info +from .utils import ( + get_device_entry_gen, + get_rpc_device_wakeup_period, + update_device_fw_info, +) _DeviceT = TypeVar("_DeviceT", bound="BlockDevice|RpcDevice") @@ -136,7 +139,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): manufacturer="Shelly", model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), sw_version=self.sw_version, - hw_version=f"gen{self.entry.data[CONF_GEN]} ({self.model})", + hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})", configuration_url=f"http://{self.entry.data[CONF_HOST]}", ) self.device_id = device_entry.id diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 0384e9255a3..26040e13557 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -12,6 +12,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.shelly.const import ( + CONF_GEN, CONF_SLEEP_PERIOD, DOMAIN, REST_SENSORS_UPDATE_INTERVAL, @@ -30,7 +31,7 @@ MOCK_MAC = "123456789ABC" async def init_integration( hass: HomeAssistant, - gen: int, + gen: int | None, model=MODEL_25, sleep_period=0, options: dict[str, Any] | None = None, @@ -41,8 +42,9 @@ async def init_integration( CONF_HOST: "192.168.1.37", CONF_SLEEP_PERIOD: sleep_period, "model": model, - "gen": gen, } + if gen is not None: + data[CONF_GEN] = gen entry = MockConfigEntry( domain=DOMAIN, data=data, unique_id=MOCK_MAC, options=options diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 8f6599b39e4..643fc775cc4 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -301,3 +301,11 @@ async def test_no_attempt_to_stop_scanner_with_sleepy_devices( mock_rpc_device.mock_update() await hass.async_block_till_done() assert not mock_stop_scanner.call_count + + +async def test_entry_missing_gen(hass: HomeAssistant, mock_block_device) -> None: + """Test successful Gen1 device init when gen is missing in entry data.""" + entry = await init_integration(hass, None) + + assert entry.state is ConfigEntryState.LOADED + assert hass.states.get("switch.test_name_channel_1").state is STATE_ON From 5a01b55fd1b4f6fb08f95f31f5bb3a2da098426d Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sat, 6 Jan 2024 11:18:32 +0100 Subject: [PATCH 030/133] enigma2: fix exception when device in deep sleep, fix previous track (#107296) enigma2: fix exception when device in deep sleep; previous track --- homeassistant/components/enigma2/media_player.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 432823d781b..e4283eeef9d 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -1,6 +1,7 @@ """Support for Enigma2 media players.""" from __future__ import annotations +from aiohttp.client_exceptions import ClientConnectorError from openwebif.api import OpenWebIfDevice from openwebif.enums import RemoteControlCodes import voluptuous as vol @@ -20,6 +21,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -96,9 +98,13 @@ async def async_setup_platform( source_bouquet=config.get(CONF_SOURCE_BOUQUET), ) - async_add_entities( - [Enigma2Device(config[CONF_NAME], device, await device.get_about())] - ) + try: + about = await device.get_about() + except ClientConnectorError as err: + await device.close() + raise PlatformNotReady from err + + async_add_entities([Enigma2Device(config[CONF_NAME], device, about)]) class Enigma2Device(MediaPlayerEntity): @@ -169,8 +175,8 @@ class Enigma2Device(MediaPlayerEntity): await self._device.send_remote_control_action(RemoteControlCodes.CHANNEL_UP) async def async_media_previous_track(self) -> None: - """Send next track command.""" - self._device.send_remote_control_action(RemoteControlCodes.CHANNEL_DOWN) + """Send previous track command.""" + await self._device.send_remote_control_action(RemoteControlCodes.CHANNEL_DOWN) async def async_mute_volume(self, mute: bool) -> None: """Mute or unmute.""" From 003d2be47754d31351918e956e7d5b72c2a5fbdf Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 5 Jan 2024 16:53:43 -0500 Subject: [PATCH 031/133] Fix assertion error when unloading ZHA with pollable entities (#107311) --- homeassistant/components/zha/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 027e710e30c..ea5d09dd6f4 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -216,9 +216,9 @@ class PollableSensor(Sensor): async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" - assert self._cancel_refresh_handle - self._cancel_refresh_handle() - self._cancel_refresh_handle = None + if self._cancel_refresh_handle is not None: + self._cancel_refresh_handle() + self._cancel_refresh_handle = None self.debug("stopped polling during device removal") await super().async_will_remove_from_hass() From cab833160dcbeaf4f23c89595f3851dac98840f0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 6 Jan 2024 12:25:05 +0100 Subject: [PATCH 032/133] Bump version to 2024.1.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 cea73ec243b..c91743e7ba9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 3bec11ced3b..bbf45725716 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.1" +version = "2024.1.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f9dacedf0f93b998713b7bc5c5d5014371a418a8 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler <2292715+bdr99@users.noreply.github.com> Date: Sat, 6 Jan 2024 11:16:24 -0500 Subject: [PATCH 033/133] Add diagnostics to A. O. Smith integration (#106343) * Add diagnostics to A. O. Smith integration * Bump py-aosmith to 1.0.4 * remove redactions from test fixture --- .../components/aosmith/diagnostics.py | 39 +++ .../components/aosmith/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aosmith/conftest.py | 6 + .../aosmith/fixtures/get_all_device_info.json | 247 +++++++++++++++++ .../aosmith/snapshots/test_diagnostics.ambr | 252 ++++++++++++++++++ tests/components/aosmith/test_diagnostics.py | 23 ++ 8 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/aosmith/diagnostics.py create mode 100644 tests/components/aosmith/fixtures/get_all_device_info.json create mode 100644 tests/components/aosmith/snapshots/test_diagnostics.ambr create mode 100644 tests/components/aosmith/test_diagnostics.py diff --git a/homeassistant/components/aosmith/diagnostics.py b/homeassistant/components/aosmith/diagnostics.py new file mode 100644 index 00000000000..a821c980faa --- /dev/null +++ b/homeassistant/components/aosmith/diagnostics.py @@ -0,0 +1,39 @@ +"""Diagnostics support for A. O. Smith.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import AOSmithData +from .const import DOMAIN + +TO_REDACT = { + "address", + "city", + "contactId", + "dsn", + "email", + "firstName", + "heaterSsid", + "id", + "lastName", + "phone", + "postalCode", + "registeredOwner", + "serial", + "ssid", + "state", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data: AOSmithData = hass.data[DOMAIN][config_entry.entry_id] + + all_device_info = await data.client.get_all_device_info() + return async_redact_data(all_device_info, TO_REDACT) diff --git a/homeassistant/components/aosmith/manifest.json b/homeassistant/components/aosmith/manifest.json index 895b03cf7fd..7651086e138 100644 --- a/homeassistant/components/aosmith/manifest.json +++ b/homeassistant/components/aosmith/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aosmith", "iot_class": "cloud_polling", - "requirements": ["py-aosmith==1.0.1"] + "requirements": ["py-aosmith==1.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 012c601b785..add3e51aac2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1548,7 +1548,7 @@ pushover_complete==1.1.1 pvo==2.1.1 # homeassistant.components.aosmith -py-aosmith==1.0.1 +py-aosmith==1.0.4 # homeassistant.components.canary py-canary==0.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96775e05791..f4884091cad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1195,7 +1195,7 @@ pushover_complete==1.1.1 pvo==2.1.1 # homeassistant.components.aosmith -py-aosmith==1.0.1 +py-aosmith==1.0.4 # homeassistant.components.canary py-canary==0.5.3 diff --git a/tests/components/aosmith/conftest.py b/tests/components/aosmith/conftest.py index 61c1fc9a562..f2c3ffc9c3c 100644 --- a/tests/components/aosmith/conftest.py +++ b/tests/components/aosmith/conftest.py @@ -54,10 +54,16 @@ async def mock_client(get_devices_fixture: str) -> Generator[MagicMock, None, No get_energy_use_fixture = load_json_object_fixture( "get_energy_use_data.json", DOMAIN ) + get_all_device_info_fixture = load_json_object_fixture( + "get_all_device_info.json", DOMAIN + ) client_mock = MagicMock(AOSmithAPIClient) client_mock.get_devices = AsyncMock(return_value=get_devices_fixture) client_mock.get_energy_use_data = AsyncMock(return_value=get_energy_use_fixture) + client_mock.get_all_device_info = AsyncMock( + return_value=get_all_device_info_fixture + ) return client_mock diff --git a/tests/components/aosmith/fixtures/get_all_device_info.json b/tests/components/aosmith/fixtures/get_all_device_info.json new file mode 100644 index 00000000000..4d19a80a3ad --- /dev/null +++ b/tests/components/aosmith/fixtures/get_all_device_info.json @@ -0,0 +1,247 @@ +{ + "devices": [ + { + "alertSettings": { + "faultCode": { + "major": { + "email": true, + "sms": false + }, + "minor": { + "email": false, + "sms": false + } + }, + "operatingSetPoint": { + "email": false, + "sms": false + }, + "tankTemperature": { + "highTemperature": { + "email": false, + "sms": false, + "value": 160 + }, + "lowTemperature": { + "email": false, + "sms": false, + "value": 120 + } + } + }, + "brand": "aosmith", + "deviceType": "NEXT_GEN_HEAT_PUMP", + "dsn": "dsn", + "hardware": { + "hasBluetooth": true, + "interface": "CONTROL_PANEL" + }, + "id": "id", + "install": { + "address": "sample_address", + "city": "sample_city", + "country": "United States", + "date": "2023-09-29", + "email": "sample_email", + "group": "Residential", + "location": "Basement", + "phone": "sample_phone", + "postalCode": "sample_postal_code", + "professional": false, + "registeredOwner": "sample_owner", + "registrationDate": "2023-12-24", + "state": "sample_state" + }, + "isRegistered": true, + "junctionId": "junctionId", + "lastUpdate": 1703386473737, + "model": "HPTS-50 200 202172000", + "name": "Water Heater", + "permissions": "USER", + "productId": "100350404", + "serial": "sample_serial", + "users": [ + { + "contactId": "sample_contact_id", + "email": "sample_email", + "firstName": "sample_first_name", + "isSelf": true, + "lastName": "sample_last_name", + "permissions": "USER" + } + ], + "data": { + "activeAlerts": [], + "alertHistory": [], + "isOnline": true, + "isWifi": true, + "lastUpdate": 1703138389000, + "signalStrength": null, + "heaterSsid": "sample_heater_ssid", + "ssid": "sample_ssid", + "temperatureSetpoint": 145, + "temperatureSetpointPending": false, + "temperatureSetpointPrevious": 145, + "temperatureSetpointMaximum": 145, + "error": "", + "modes": [ + { + "mode": "HYBRID", + "controls": null + }, + { + "mode": "HEAT_PUMP", + "controls": null + }, + { + "mode": "ELECTRIC", + "controls": "SELECT_DAYS" + }, + { + "mode": "VACATION", + "controls": "SELECT_DAYS" + } + ], + "firmwareVersion": "2.14", + "hotWaterStatus": "HIGH", + "isAdvancedLoadUpMore": false, + "isCtaUcmPresent": false, + "isDemandResponsePaused": false, + "isEnrolled": false, + "mode": "HEAT_PUMP", + "modePending": false, + "vacationModeRemainingDays": 0, + "electricModeRemainingDays": 100, + "isLowes": false, + "canEditTimeOfUse": false, + "timeOfUseData": null, + "consumerScheduleData": null + } + } + ], + "energy_use_data": { + "junctionId": { + "average": 2.4744000000000006, + "graphData": [ + { + "date": "2023-11-26T04:00:00.000Z", + "kwh": 0.936 + }, + { + "date": "2023-11-27T04:00:00.000Z", + "kwh": 4.248 + }, + { + "date": "2023-11-28T04:00:00.000Z", + "kwh": 1.002 + }, + { + "date": "2023-11-29T04:00:00.000Z", + "kwh": 3.078 + }, + { + "date": "2023-11-30T04:00:00.000Z", + "kwh": 1.896 + }, + { + "date": "2023-12-01T04:00:00.000Z", + "kwh": 1.98 + }, + { + "date": "2023-12-02T04:00:00.000Z", + "kwh": 2.112 + }, + { + "date": "2023-12-03T04:00:00.000Z", + "kwh": 3.222 + }, + { + "date": "2023-12-04T04:00:00.000Z", + "kwh": 4.254 + }, + { + "date": "2023-12-05T04:00:00.000Z", + "kwh": 4.05 + }, + { + "date": "2023-12-06T04:00:00.000Z", + "kwh": 3.312 + }, + { + "date": "2023-12-07T04:00:00.000Z", + "kwh": 2.334 + }, + { + "date": "2023-12-08T04:00:00.000Z", + "kwh": 2.418 + }, + { + "date": "2023-12-09T04:00:00.000Z", + "kwh": 2.19 + }, + { + "date": "2023-12-10T04:00:00.000Z", + "kwh": 3.786 + }, + { + "date": "2023-12-11T04:00:00.000Z", + "kwh": 5.292 + }, + { + "date": "2023-12-12T04:00:00.000Z", + "kwh": 1.38 + }, + { + "date": "2023-12-13T04:00:00.000Z", + "kwh": 3.324 + }, + { + "date": "2023-12-14T04:00:00.000Z", + "kwh": 1.092 + }, + { + "date": "2023-12-15T04:00:00.000Z", + "kwh": 0.606 + }, + { + "date": "2023-12-16T04:00:00.000Z", + "kwh": 0 + }, + { + "date": "2023-12-17T04:00:00.000Z", + "kwh": 2.838 + }, + { + "date": "2023-12-18T04:00:00.000Z", + "kwh": 2.382 + }, + { + "date": "2023-12-19T04:00:00.000Z", + "kwh": 2.904 + }, + { + "date": "2023-12-20T04:00:00.000Z", + "kwh": 1.914 + }, + { + "date": "2023-12-21T04:00:00.000Z", + "kwh": 3.93 + }, + { + "date": "2023-12-22T04:00:00.000Z", + "kwh": 3.666 + }, + { + "date": "2023-12-23T04:00:00.000Z", + "kwh": 2.766 + }, + { + "date": "2023-12-24T04:00:00.000Z", + "kwh": 1.32 + } + ], + "lifetimeKwh": 203.259, + "startDate": "Nov 26" + } + } +} diff --git a/tests/components/aosmith/snapshots/test_diagnostics.ambr b/tests/components/aosmith/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..8704cdaa214 --- /dev/null +++ b/tests/components/aosmith/snapshots/test_diagnostics.ambr @@ -0,0 +1,252 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'devices': list([ + dict({ + 'alertSettings': dict({ + 'faultCode': dict({ + 'major': dict({ + 'email': '**REDACTED**', + 'sms': False, + }), + 'minor': dict({ + 'email': '**REDACTED**', + 'sms': False, + }), + }), + 'operatingSetPoint': dict({ + 'email': '**REDACTED**', + 'sms': False, + }), + 'tankTemperature': dict({ + 'highTemperature': dict({ + 'email': '**REDACTED**', + 'sms': False, + 'value': 160, + }), + 'lowTemperature': dict({ + 'email': '**REDACTED**', + 'sms': False, + 'value': 120, + }), + }), + }), + 'brand': 'aosmith', + 'data': dict({ + 'activeAlerts': list([ + ]), + 'alertHistory': list([ + ]), + 'canEditTimeOfUse': False, + 'consumerScheduleData': None, + 'electricModeRemainingDays': 100, + 'error': '', + 'firmwareVersion': '2.14', + 'heaterSsid': '**REDACTED**', + 'hotWaterStatus': 'HIGH', + 'isAdvancedLoadUpMore': False, + 'isCtaUcmPresent': False, + 'isDemandResponsePaused': False, + 'isEnrolled': False, + 'isLowes': False, + 'isOnline': True, + 'isWifi': True, + 'lastUpdate': 1703138389000, + 'mode': 'HEAT_PUMP', + 'modePending': False, + 'modes': list([ + dict({ + 'controls': None, + 'mode': 'HYBRID', + }), + dict({ + 'controls': None, + 'mode': 'HEAT_PUMP', + }), + dict({ + 'controls': 'SELECT_DAYS', + 'mode': 'ELECTRIC', + }), + dict({ + 'controls': 'SELECT_DAYS', + 'mode': 'VACATION', + }), + ]), + 'signalStrength': None, + 'ssid': '**REDACTED**', + 'temperatureSetpoint': 145, + 'temperatureSetpointMaximum': 145, + 'temperatureSetpointPending': False, + 'temperatureSetpointPrevious': 145, + 'timeOfUseData': None, + 'vacationModeRemainingDays': 0, + }), + 'deviceType': 'NEXT_GEN_HEAT_PUMP', + 'dsn': '**REDACTED**', + 'hardware': dict({ + 'hasBluetooth': True, + 'interface': 'CONTROL_PANEL', + }), + 'id': '**REDACTED**', + 'install': dict({ + 'address': '**REDACTED**', + 'city': '**REDACTED**', + 'country': 'United States', + 'date': '2023-09-29', + 'email': '**REDACTED**', + 'group': 'Residential', + 'location': 'Basement', + 'phone': '**REDACTED**', + 'postalCode': '**REDACTED**', + 'professional': False, + 'registeredOwner': '**REDACTED**', + 'registrationDate': '2023-12-24', + 'state': '**REDACTED**', + }), + 'isRegistered': True, + 'junctionId': 'junctionId', + 'lastUpdate': 1703386473737, + 'model': 'HPTS-50 200 202172000', + 'name': 'Water Heater', + 'permissions': 'USER', + 'productId': '100350404', + 'serial': '**REDACTED**', + 'users': list([ + dict({ + 'contactId': '**REDACTED**', + 'email': '**REDACTED**', + 'firstName': '**REDACTED**', + 'isSelf': True, + 'lastName': '**REDACTED**', + 'permissions': 'USER', + }), + ]), + }), + ]), + 'energy_use_data': dict({ + 'junctionId': dict({ + 'average': 2.4744000000000006, + 'graphData': list([ + dict({ + 'date': '2023-11-26T04:00:00.000Z', + 'kwh': 0.936, + }), + dict({ + 'date': '2023-11-27T04:00:00.000Z', + 'kwh': 4.248, + }), + dict({ + 'date': '2023-11-28T04:00:00.000Z', + 'kwh': 1.002, + }), + dict({ + 'date': '2023-11-29T04:00:00.000Z', + 'kwh': 3.078, + }), + dict({ + 'date': '2023-11-30T04:00:00.000Z', + 'kwh': 1.896, + }), + dict({ + 'date': '2023-12-01T04:00:00.000Z', + 'kwh': 1.98, + }), + dict({ + 'date': '2023-12-02T04:00:00.000Z', + 'kwh': 2.112, + }), + dict({ + 'date': '2023-12-03T04:00:00.000Z', + 'kwh': 3.222, + }), + dict({ + 'date': '2023-12-04T04:00:00.000Z', + 'kwh': 4.254, + }), + dict({ + 'date': '2023-12-05T04:00:00.000Z', + 'kwh': 4.05, + }), + dict({ + 'date': '2023-12-06T04:00:00.000Z', + 'kwh': 3.312, + }), + dict({ + 'date': '2023-12-07T04:00:00.000Z', + 'kwh': 2.334, + }), + dict({ + 'date': '2023-12-08T04:00:00.000Z', + 'kwh': 2.418, + }), + dict({ + 'date': '2023-12-09T04:00:00.000Z', + 'kwh': 2.19, + }), + dict({ + 'date': '2023-12-10T04:00:00.000Z', + 'kwh': 3.786, + }), + dict({ + 'date': '2023-12-11T04:00:00.000Z', + 'kwh': 5.292, + }), + dict({ + 'date': '2023-12-12T04:00:00.000Z', + 'kwh': 1.38, + }), + dict({ + 'date': '2023-12-13T04:00:00.000Z', + 'kwh': 3.324, + }), + dict({ + 'date': '2023-12-14T04:00:00.000Z', + 'kwh': 1.092, + }), + dict({ + 'date': '2023-12-15T04:00:00.000Z', + 'kwh': 0.606, + }), + dict({ + 'date': '2023-12-16T04:00:00.000Z', + 'kwh': 0, + }), + dict({ + 'date': '2023-12-17T04:00:00.000Z', + 'kwh': 2.838, + }), + dict({ + 'date': '2023-12-18T04:00:00.000Z', + 'kwh': 2.382, + }), + dict({ + 'date': '2023-12-19T04:00:00.000Z', + 'kwh': 2.904, + }), + dict({ + 'date': '2023-12-20T04:00:00.000Z', + 'kwh': 1.914, + }), + dict({ + 'date': '2023-12-21T04:00:00.000Z', + 'kwh': 3.93, + }), + dict({ + 'date': '2023-12-22T04:00:00.000Z', + 'kwh': 3.666, + }), + dict({ + 'date': '2023-12-23T04:00:00.000Z', + 'kwh': 2.766, + }), + dict({ + 'date': '2023-12-24T04:00:00.000Z', + 'kwh': 1.32, + }), + ]), + 'lifetimeKwh': 203.259, + 'startDate': 'Nov 26', + }), + }), + }) +# --- diff --git a/tests/components/aosmith/test_diagnostics.py b/tests/components/aosmith/test_diagnostics.py new file mode 100644 index 00000000000..9090ef5e7b7 --- /dev/null +++ b/tests/components/aosmith/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Tests for the diagnostics data provided by the A. O. Smith integration.""" + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From 9fe351f363d234293ae30e5747081d34abd2c7c1 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Mon, 8 Jan 2024 22:28:04 +0100 Subject: [PATCH 034/133] Catch missing inverter in Enphase Envoy (#106730) * bug: prevent invalid key when empty invereter arrays is returned. Some envoy fw versions return an empty inverter array every 4 hours when no production is taking place. Prevent collection failure due to this as other data seems fine. Inveretrs will show unknown during this cycle. * refactor: replace try/catch with test and make warning debug * Update homeassistant/components/enphase_envoy/sensor.py * Update homeassistant/components/enphase_envoy/sensor.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/enphase_envoy/sensor.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 1dfd72dcaf3..2ae9dca63ba 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -479,10 +479,20 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity): ) @property - def native_value(self) -> datetime.datetime | float: + def native_value(self) -> datetime.datetime | float | None: """Return the state of the sensor.""" inverters = self.data.inverters assert inverters is not None + # Some envoy fw versions return an empty inverter array every 4 hours when + # no production is taking place. Prevent collection failure due to this + # as other data seems fine. Inverters will show unknown during this cycle. + if self._serial_number not in inverters: + _LOGGER.debug( + "Inverter %s not in returned inverters array (size: %s)", + self._serial_number, + len(inverters), + ) + return None return self.entity_description.value_fn(inverters[self._serial_number]) From 1acae5a62d2d177076cb9e0d5584b89f16682a20 Mon Sep 17 00:00:00 2001 From: vexofp Date: Tue, 9 Jan 2024 06:32:27 -0500 Subject: [PATCH 035/133] Prevent toggle from calling stop on covers which do not support it (#106848) * Prevent toggle from calling stop on covers which do not support it * Update homeassistant/components/cover/__init__.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/cover/__init__.py | 2 +- tests/components/cover/test_init.py | 22 +++++++++- .../custom_components/test/cover.py | 44 +++++++++++++------ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 945585de522..89f79ca9d7a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -481,7 +481,7 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def _get_toggle_function( self, fns: dict[str, Callable[_P, _R]] ) -> Callable[_P, _R]: - if CoverEntityFeature.STOP | self.supported_features and ( + if self.supported_features & CoverEntityFeature.STOP and ( self.is_closing or self.is_opening ): return fns["stop"] diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 480d1ef83aa..0503017f634 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -34,7 +34,8 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - # ent3 = cover with simple tilt functions and no position # ent4 = cover with all tilt functions but no position # ent5 = cover with all functions - ent1, ent2, ent3, ent4, ent5 = platform.ENTITIES + # ent6 = cover with only open/close, but also reports opening/closing + ent1, ent2, ent3, ent4, ent5, ent6 = platform.ENTITIES # Test init all covers should be open assert is_open(hass, ent1) @@ -42,6 +43,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - assert is_open(hass, ent3) assert is_open(hass, ent4) assert is_open(hass, ent5) + assert is_open(hass, ent6) # call basic toggle services await call_service(hass, SERVICE_TOGGLE, ent1) @@ -49,13 +51,15 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - await call_service(hass, SERVICE_TOGGLE, ent3) await call_service(hass, SERVICE_TOGGLE, ent4) await call_service(hass, SERVICE_TOGGLE, ent5) + await call_service(hass, SERVICE_TOGGLE, ent6) - # entities without stop should be closed and with stop should be closing + # entities should be either closed or closing, depending on if they report transitional states assert is_closed(hass, ent1) assert is_closing(hass, ent2) assert is_closed(hass, ent3) assert is_closed(hass, ent4) assert is_closing(hass, ent5) + assert is_closing(hass, ent6) # call basic toggle services and set different cover position states await call_service(hass, SERVICE_TOGGLE, ent1) @@ -65,6 +69,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - await call_service(hass, SERVICE_TOGGLE, ent4) set_cover_position(ent5, 15) await call_service(hass, SERVICE_TOGGLE, ent5) + await call_service(hass, SERVICE_TOGGLE, ent6) # entities should be in correct state depending on the SUPPORT_STOP feature and cover position assert is_open(hass, ent1) @@ -72,6 +77,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - assert is_open(hass, ent3) assert is_open(hass, ent4) assert is_open(hass, ent5) + assert is_opening(hass, ent6) # call basic toggle services await call_service(hass, SERVICE_TOGGLE, ent1) @@ -79,6 +85,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - await call_service(hass, SERVICE_TOGGLE, ent3) await call_service(hass, SERVICE_TOGGLE, ent4) await call_service(hass, SERVICE_TOGGLE, ent5) + await call_service(hass, SERVICE_TOGGLE, ent6) # entities should be in correct state depending on the SUPPORT_STOP feature and cover position assert is_closed(hass, ent1) @@ -86,6 +93,12 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - assert is_closed(hass, ent3) assert is_closed(hass, ent4) assert is_opening(hass, ent5) + assert is_closing(hass, ent6) + + # Without STOP but still reports opening/closing has a 4th possible toggle state + set_state(ent6, STATE_CLOSED) + await call_service(hass, SERVICE_TOGGLE, ent6) + assert is_opening(hass, ent6) def call_service(hass, service, ent): @@ -100,6 +113,11 @@ def set_cover_position(ent, position) -> None: ent._values["current_cover_position"] = position +def set_state(ent, state) -> None: + """Set the state of a cover.""" + ent._values["state"] = state + + def is_open(hass, ent): """Return if the cover is closed based on the statemachine.""" return hass.states.is_state(ent.entity_id, STATE_OPEN) diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index 2a57412ea9e..dc89b95981b 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -2,6 +2,8 @@ Call init before using it in your tests to ensure clean test data. """ +from typing import Any + from homeassistant.components.cover import CoverEntity, CoverEntityFeature from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING @@ -70,6 +72,13 @@ def init(empty=False): | CoverEntityFeature.STOP_TILT | CoverEntityFeature.SET_TILT_POSITION, ), + MockCover( + name="Simple with opening/closing cover", + is_on=True, + unique_id="unique_opening_closing_cover", + supported_features=CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, + reports_opening_closing=True, + ), ] ) @@ -84,50 +93,59 @@ async def async_setup_platform( class MockCover(MockEntity, CoverEntity): """Mock Cover class.""" + def __init__( + self, reports_opening_closing: bool | None = None, **values: Any + ) -> None: + """Initialize a mock cover entity.""" + + super().__init__(**values) + self._reports_opening_closing = ( + reports_opening_closing + if reports_opening_closing is not None + else CoverEntityFeature.STOP in self.supported_features + ) + @property def is_closed(self): """Return if the cover is closed or not.""" - if self.supported_features & CoverEntityFeature.STOP: - return self.current_cover_position == 0 + if "state" in self._values and self._values["state"] == STATE_CLOSED: + return True - if "state" in self._values: - return self._values["state"] == STATE_CLOSED - return False + return self.current_cover_position == 0 @property def is_opening(self): """Return if the cover is opening or not.""" - if self.supported_features & CoverEntityFeature.STOP: - if "state" in self._values: - return self._values["state"] == STATE_OPENING + if "state" in self._values: + return self._values["state"] == STATE_OPENING return False @property def is_closing(self): """Return if the cover is closing or not.""" - if self.supported_features & CoverEntityFeature.STOP: - if "state" in self._values: - return self._values["state"] == STATE_CLOSING + if "state" in self._values: + return self._values["state"] == STATE_CLOSING return False def open_cover(self, **kwargs) -> None: """Open cover.""" - if self.supported_features & CoverEntityFeature.STOP: + if self._reports_opening_closing: self._values["state"] = STATE_OPENING else: self._values["state"] = STATE_OPEN def close_cover(self, **kwargs) -> None: """Close cover.""" - if self.supported_features & CoverEntityFeature.STOP: + if self._reports_opening_closing: self._values["state"] = STATE_CLOSING else: self._values["state"] = STATE_CLOSED def stop_cover(self, **kwargs) -> None: """Stop cover.""" + assert CoverEntityFeature.STOP in self.supported_features self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN @property From 4e991388fb3693cb61c8dd884b3cc9fa6258d449 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Fri, 12 Jan 2024 12:55:09 +0100 Subject: [PATCH 036/133] Fix missing unique_id for spt integration (#107087) Co-authored-by: Franck Nijhof Co-authored-by: Joost Lekkerkerker --- .../swiss_public_transport/__init__.py | 49 +++++++++++ .../swiss_public_transport/config_flow.py | 7 ++ .../swiss_public_transport/test_init.py | 85 +++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 tests/components/swiss_public_transport/test_init.py diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index 9e01a07416f..a510b5b7414 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -10,6 +10,7 @@ from opendata_transport.exceptions import ( from homeassistant import config_entries, core from homeassistant.const import Platform from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_DESTINATION, CONF_START, DOMAIN @@ -65,3 +66,51 @@ async def async_unload_entry( hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +) -> bool: + """Migrate config entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.minor_version > 3: + # This means the user has downgraded from a future version + return False + + if config_entry.minor_version == 1: + # Remove wrongly registered devices and entries + new_unique_id = ( + f"{config_entry.data[CONF_START]} {config_entry.data[CONF_DESTINATION]}" + ) + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry_id=config_entry.entry_id + ) + for dev in device_entries: + device_registry.async_remove_device(dev.id) + + entity_id = entity_registry.async_get_entity_id( + Platform.SENSOR, DOMAIN, "None_departure" + ) + if entity_id: + entity_registry.async_update_entity( + entity_id=entity_id, + new_unique_id=f"{new_unique_id}_departure", + ) + _LOGGER.debug( + "Faulty entity with unique_id 'None_departure' migrated to new unique_id '%s'", + f"{new_unique_id}_departure", + ) + + # Set a valid unique id for config entries + config_entry.unique_id = new_unique_id + config_entry.minor_version = 2 + hass.config_entries.async_update_entry(config_entry) + + _LOGGER.debug( + "Migration to minor version %s successful", config_entry.minor_version + ) + + return True diff --git a/homeassistant/components/swiss_public_transport/config_flow.py b/homeassistant/components/swiss_public_transport/config_flow.py index 63eca1efe96..ceb6f46806d 100644 --- a/homeassistant/components/swiss_public_transport/config_flow.py +++ b/homeassistant/components/swiss_public_transport/config_flow.py @@ -31,6 +31,7 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Swiss public transport config flow.""" VERSION = 1 + MINOR_VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -59,6 +60,9 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unknown error") errors["base"] = "unknown" else: + await self.async_set_unique_id( + f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}" + ) return self.async_create_entry( title=f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}", data=user_input, @@ -98,6 +102,9 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="unknown") + await self.async_set_unique_id( + f"{import_input[CONF_START]} {import_input[CONF_DESTINATION]}" + ) return self.async_create_entry( title=import_input[CONF_NAME], data=import_input, diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py new file mode 100644 index 00000000000..f2b4e41ed71 --- /dev/null +++ b/tests/components/swiss_public_transport/test_init.py @@ -0,0 +1,85 @@ +"""Test the swiss_public_transport config flow.""" +from unittest.mock import AsyncMock, patch + +from homeassistant.components.swiss_public_transport.const import ( + CONF_DESTINATION, + CONF_START, + DOMAIN, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + +MOCK_DATA_STEP = { + CONF_START: "test_start", + CONF_DESTINATION: "test_destination", +} + +CONNECTIONS = [ + { + "departure": "2024-01-06T18:03:00+0100", + "number": 0, + "platform": 0, + "transfers": 0, + "duration": "10", + "delay": 0, + }, + { + "departure": "2024-01-06T18:04:00+0100", + "number": 1, + "platform": 1, + "transfers": 0, + "duration": "10", + "delay": 0, + }, + { + "departure": "2024-01-06T18:05:00+0100", + "number": 2, + "platform": 2, + "transfers": 0, + "duration": "10", + "delay": 0, + }, +] + + +async def test_migration_1_to_2( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test successful setup.""" + + with patch( + "homeassistant.components.swiss_public_transport.OpendataTransport", + return_value=AsyncMock(), + ) as mock: + mock().connections = CONNECTIONS + + config_entry_faulty = MockConfigEntry( + domain=DOMAIN, + data=MOCK_DATA_STEP, + title="MIGRATION_TEST", + minor_version=1, + ) + config_entry_faulty.add_to_hass(hass) + + # Setup the config entry + await hass.config_entries.async_setup(config_entry_faulty.entry_id) + await hass.async_block_till_done() + assert entity_registry.async_is_registered( + entity_registry.entities.get_entity_id( + (Platform.SENSOR, DOMAIN, "test_start test_destination_departure") + ) + ) + + # Check change in config entry + assert config_entry_faulty.minor_version == 2 + assert config_entry_faulty.unique_id == "test_start test_destination" + + # Check "None" is gone + assert not entity_registry.async_is_registered( + entity_registry.entities.get_entity_id( + (Platform.SENSOR, DOMAIN, "None_departure") + ) + ) From 54e62b40951a5777dda095d0f18882db22d93dd1 Mon Sep 17 00:00:00 2001 From: Ben Morton Date: Sat, 6 Jan 2024 16:22:46 +0000 Subject: [PATCH 037/133] Add support for the Spotify DJ (#107268) * Add support for the Spotify DJ playlist by mocking the playlist response Add error handling for playlist lookup to ensure it doesn't break current playback state loading * Run linters Add exception type to playlist lookup error handling * Fix typo in comment Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .../components/spotify/media_player.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 6ef2697ba77..0204cc30fbb 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -61,6 +61,10 @@ REPEAT_MODE_MAPPING_TO_SPOTIFY = { value: key for key, value in REPEAT_MODE_MAPPING_TO_HA.items() } +# This is a minimal representation of the DJ playlist that Spotify now offers +# The DJ is not fully integrated with the playlist API, so needs to have the playlist response mocked in order to maintain functionality +SPOTIFY_DJ_PLAYLIST = {"uri": "spotify:playlist:37i9dQZF1EYkqdzj48dyYq", "name": "DJ"} + async def async_setup_entry( hass: HomeAssistant, @@ -423,7 +427,19 @@ class SpotifyMediaPlayer(MediaPlayerEntity): if context and (self._playlist is None or self._playlist["uri"] != uri): self._playlist = None if context["type"] == MediaType.PLAYLIST: - self._playlist = self.data.client.playlist(uri) + # The Spotify API does not currently support doing a lookup for the DJ playlist, so just use the minimal mock playlist object + if uri == SPOTIFY_DJ_PLAYLIST["uri"]: + self._playlist = SPOTIFY_DJ_PLAYLIST + else: + # Make sure any playlist lookups don't break the current playback state update + try: + self._playlist = self.data.client.playlist(uri) + except SpotifyException: + _LOGGER.debug( + "Unable to load spotify playlist '%s'. Continuing without playlist data", + uri, + ) + self._playlist = None device = self._currently_playing.get("device") if device is not None: From 409a254fe5a9634a55fdc57367ba5a5616a95e5b Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 7 Jan 2024 19:04:14 +0000 Subject: [PATCH 038/133] Fix evohome high_precision temps not retreived consistently (#107366) * initial commit * doctweak * remove hint * doctweak --- homeassistant/components/evohome/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 06712a83b6a..390bdeb3f33 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -497,7 +497,6 @@ class EvoBroker: session_id = get_session_id(self.client_v1) - self.temps = {} # these are now stale, will fall back to v2 temps try: temps = await self.client_v1.get_temperatures() @@ -523,6 +522,11 @@ class EvoBroker: ), err, ) + self.temps = {} # high-precision temps now considered stale + + except Exception: + self.temps = {} # high-precision temps now considered stale + raise else: if str(self.client_v1.location_id) != self._location.locationId: @@ -654,6 +658,7 @@ class EvoChild(EvoDevice): assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check if self._evo_broker.temps.get(self._evo_id) is not None: + # use high-precision temps if available return self._evo_broker.temps[self._evo_id] return self._evo_device.temperature From 53ab8925759554858c8efd4152578a44a2feedb5 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 8 Jan 2024 04:51:58 -0500 Subject: [PATCH 039/133] Reduce polling rate in Blink (#107386) --- homeassistant/components/blink/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/blink/coordinator.py b/homeassistant/components/blink/coordinator.py index d53d23c4344..aaf666208a6 100644 --- a/homeassistant/components/blink/coordinator.py +++ b/homeassistant/components/blink/coordinator.py @@ -13,7 +13,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = 30 +SCAN_INTERVAL = 300 class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): From 9bc0217738f6c49714547b3c4e1a6ba2d4d6d51b Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 7 Jan 2024 20:32:17 +0100 Subject: [PATCH 040/133] Fix KNX telegram device trigger not firing after integration reload (#107388) --- homeassistant/components/knx/const.py | 3 +++ homeassistant/components/knx/device_trigger.py | 10 ++++++---- homeassistant/components/knx/telegrams.py | 4 +++- tests/components/knx/test_device_trigger.py | 3 --- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 3d1e3c62a34..aa48bcdf557 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -82,6 +82,9 @@ DATA_HASS_CONFIG: Final = "knx_hass_config" ATTR_COUNTER: Final = "counter" ATTR_SOURCE: Final = "source" +# dispatcher signal for KNX interface device triggers +SIGNAL_KNX_TELEGRAM_DICT: Final = "knx_telegram_dict" + AsyncMessageCallbackType = Callable[[Telegram], Awaitable[None]] MessageCallbackType = Callable[[Telegram], None] diff --git a/homeassistant/components/knx/device_trigger.py b/homeassistant/components/knx/device_trigger.py index 1abafb221db..867a7c075b0 100644 --- a/homeassistant/components/knx/device_trigger.py +++ b/homeassistant/components/knx/device_trigger.py @@ -9,11 +9,12 @@ from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEM from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import selector +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import KNXModule -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_KNX_TELEGRAM_DICT from .project import KNXProject from .schema import ga_list_validator from .telegrams import TelegramDict @@ -87,7 +88,6 @@ async def async_attach_trigger( trigger_data = trigger_info["trigger_data"] dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, []) job = HassJob(action, f"KNX device trigger {trigger_info}") - knx: KNXModule = hass.data[DOMAIN] @callback def async_call_trigger_action(telegram: TelegramDict) -> None: @@ -99,6 +99,8 @@ async def async_attach_trigger( {"trigger": {**trigger_data, **telegram}}, ) - return knx.telegrams.async_listen_telegram( - async_call_trigger_action, name="KNX device trigger call" + return async_dispatcher_connect( + hass, + signal=SIGNAL_KNX_TELEGRAM_DICT, + target=async_call_trigger_action, ) diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 87c1a8b6052..95250d99f85 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -11,10 +11,11 @@ from xknx.telegram import Telegram from xknx.telegram.apci import GroupValueResponse, GroupValueWrite from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store import homeassistant.util.dt as dt_util -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_KNX_TELEGRAM_DICT from .project import KNXProject STORAGE_VERSION: Final = 1 @@ -87,6 +88,7 @@ class Telegrams: """Handle incoming and outgoing telegrams from xknx.""" telegram_dict = self.telegram_to_dict(telegram) self.recent_telegrams.append(telegram_dict) + async_dispatcher_send(self.hass, SIGNAL_KNX_TELEGRAM_DICT, telegram_dict) for job in self._jobs: self.hass.async_run_hass_job(job, telegram_dict) diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index e901fd7f29e..f3448947cf8 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -150,7 +150,6 @@ async def test_remove_device_trigger( }, ) - assert len(hass.data[DOMAIN].telegrams._jobs) == 1 await knx.receive_write("0/0/1", (0x03, 0x2F)) assert len(calls) == 1 assert calls.pop().data["catch_all"] == "telegram - 0/0/1" @@ -161,8 +160,6 @@ async def test_remove_device_trigger( {ATTR_ENTITY_ID: f"automation.{automation_name}"}, blocking=True, ) - - assert len(hass.data[DOMAIN].telegrams._jobs) == 0 await knx.receive_write("0/0/1", (0x03, 0x2F)) assert len(calls) == 0 From 175e07fe3bcb91fadb20114d2cd05e9e3f1f7133 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 8 Jan 2024 09:31:44 +0100 Subject: [PATCH 041/133] Fix language flavors in holiday (#107392) --- homeassistant/components/holiday/calendar.py | 12 +++ tests/components/holiday/test_calendar.py | 85 ++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py index bb9a332cb73..e48cc11d677 100644 --- a/homeassistant/components/holiday/calendar.py +++ b/homeassistant/components/holiday/calendar.py @@ -43,6 +43,18 @@ async def async_setup_entry( ) language = lang break + if ( + obj_holidays.supported_languages + and language not in obj_holidays.supported_languages + and (default_language := obj_holidays.default_language) + ): + obj_holidays = country_holidays( + country, + subdiv=province, + years={dt_util.now().year, dt_util.now().year + 1}, + language=default_language, + ) + language = default_language async_add_entities( [ diff --git a/tests/components/holiday/test_calendar.py b/tests/components/holiday/test_calendar.py index 06011fb8e6b..df0ce6d50d5 100644 --- a/tests/components/holiday/test_calendar.py +++ b/tests/components/holiday/test_calendar.py @@ -227,3 +227,88 @@ async def test_no_next_event( assert state is not None assert state.state == "off" assert state.attributes == {"friendly_name": "Germany"} + + +async def test_language_not_exist( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test when language doesn't exist it will fallback to country default language.""" + + hass.config.language = "nb" # Norweigan language "Norks bokmål" + hass.config.country = "NO" + + freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC)) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_COUNTRY: "NO"}, + title="Norge", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("calendar.norge") + assert state is not None + assert state.state == "on" + assert state.attributes == { + "friendly_name": "Norge", + "all_day": True, + "description": "", + "end_time": "2023-01-02 00:00:00", + "location": "Norge", + "message": "Første nyttårsdag", + "start_time": "2023-01-01 00:00:00", + } + + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.norge", + "end_date_time": dt_util.now(), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.norge": { + "events": [ + { + "start": "2023-01-01", + "end": "2023-01-02", + "summary": "Første nyttårsdag", + "location": "Norge", + } + ] + } + } + + # Test with English as exist as optional language for Norway + hass.config.language = "en" + hass.config.country = "NO" + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.norge", + "end_date_time": dt_util.now(), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.norge": { + "events": [ + { + "start": "2023-01-01", + "end": "2023-01-02", + "summary": "New Year's Day", + "location": "Norge", + } + ] + } + } From 7c06f05108bb52485a25add6152f621192f7f789 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 7 Jan 2024 13:15:34 +0100 Subject: [PATCH 042/133] Handle OSError during setup for System Monitor (#107396) * Handle OSError during setup for System Monitor * Clean string copy * debug --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/systemmonitor/util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index 25b8aa2eb1d..742e0d40f3d 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -25,6 +25,11 @@ def get_all_disk_mounts() -> set[str]: "No permission for running user to access %s", part.mountpoint ) continue + except OSError as err: + _LOGGER.debug( + "Mountpoint %s was excluded because of: %s", part.mountpoint, err + ) + continue if usage.total > 0 and part.device != "": disks.add(part.mountpoint) _LOGGER.debug("Adding disks: %s", ", ".join(disks)) From a58483f93c69e874dd20f2b9074e03da4f698512 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 7 Jan 2024 11:27:13 +0100 Subject: [PATCH 043/133] Remove name from faa_delays (#107418) --- homeassistant/components/faa_delays/binary_sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py index 20bebcf08c8..df6ddc38de7 100644 --- a/homeassistant/components/faa_delays/binary_sensor.py +++ b/homeassistant/components/faa_delays/binary_sensor.py @@ -118,7 +118,6 @@ class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorE super().__init__(coordinator) self.entity_description = description _id = coordinator.data.code - self._attr_name = f"{_id} {description.name}" self._attr_unique_id = f"{_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, _id)}, From b333be89457513dc662cf3608edf864559b3b810 Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Sun, 7 Jan 2024 16:19:58 +0100 Subject: [PATCH 044/133] Fix Swiss public transport initial data for attributes (#107452) faster initial data for attributes --- .../components/swiss_public_transport/sensor.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 5d4a6813d2d..0e88cd2d3ad 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -122,15 +122,25 @@ class SwissPublicTransportSensor( entry_type=DeviceEntryType.SERVICE, ) + async def async_added_to_hass(self) -> None: + """Prepare the extra attributes at start.""" + self._async_update_attrs() + await super().async_added_to_hass() + @callback def _handle_coordinator_update(self) -> None: """Handle the state update and prepare the extra state attributes.""" + self._async_update_attrs() + return super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update the extra state attributes based on the coordinator data.""" self._attr_extra_state_attributes = { key: value for key, value in self.coordinator.data.items() if key not in {"departure"} } - return super()._handle_coordinator_update() @property def native_value(self) -> str: From 86822018d80b86a687b71921cc4ee0d7c73cd2eb Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 12 Jan 2024 10:32:35 +0100 Subject: [PATCH 045/133] Fix reauth flow for Comelit VEDO (#107461) --- homeassistant/components/comelit/config_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index cbd79ac1e1a..bbb671a29a7 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -72,6 +72,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): _reauth_entry: ConfigEntry | None _reauth_host: str _reauth_port: int + _reauth_type: str async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -109,6 +110,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): ) self._reauth_host = entry_data[CONF_HOST] self._reauth_port = entry_data.get(CONF_PORT, DEFAULT_PORT) + self._reauth_type = entry_data.get(CONF_TYPE, BRIDGE) self.context["title_placeholders"] = {"host": self._reauth_host} return await self.async_step_reauth_confirm() @@ -127,6 +129,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): { CONF_HOST: self._reauth_host, CONF_PORT: self._reauth_port, + CONF_TYPE: self._reauth_type, } | user_input, ) @@ -144,6 +147,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): CONF_HOST: self._reauth_host, CONF_PORT: self._reauth_port, CONF_PIN: user_input[CONF_PIN], + CONF_TYPE: self._reauth_type, }, ) self.hass.async_create_task( From 66307c5acb1d891ae30a17744d89dde71b9dc968 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:35:47 +0100 Subject: [PATCH 046/133] Fix asyncio.gather call (#107500) --- homeassistant/components/microsoft_face/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 6e47ad79f5b..af0567f99a1 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -2,8 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine import json import logging +from typing import Any import aiohttp from aiohttp.hdrs import CONTENT_TYPE @@ -267,11 +269,11 @@ class MicrosoftFace: """Store group/person data and IDs.""" return self._store - async def update_store(self): + async def update_store(self) -> None: """Load all group/person data into local store.""" groups = await self.call_api("get", "persongroups") - remove_tasks = [] + remove_tasks: list[Coroutine[Any, Any, None]] = [] new_entities = [] for group in groups: g_id = group["personGroupId"] @@ -293,7 +295,7 @@ class MicrosoftFace: self._store[g_id][person["name"]] = person["personId"] if remove_tasks: - await asyncio.gather(remove_tasks) + await asyncio.gather(*remove_tasks) await self._component.async_add_entities(new_entities) async def call_api(self, method, function, data=None, binary=False, params=None): From 73ba77deb6432c863690106ff339c724f1aa5f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=BCger?= Date: Mon, 8 Jan 2024 10:15:30 +0100 Subject: [PATCH 047/133] Fix Luftdaten sensor id string (#107506) Luftdaten: fix sensor id string --- homeassistant/components/luftdaten/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/luftdaten/strings.json b/homeassistant/components/luftdaten/strings.json index e990142923f..b7d0a90b511 100644 --- a/homeassistant/components/luftdaten/strings.json +++ b/homeassistant/components/luftdaten/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "station_id": "Sensor ID", + "sensor_id": "Sensor ID", "show_on_map": "Show on map" } } From 652fa7d693a785b0484802d100a34a18d3dedd0c Mon Sep 17 00:00:00 2001 From: nic <31355096+nabbi@users.noreply.github.com> Date: Fri, 5 Jan 2024 21:19:35 -0600 Subject: [PATCH 048/133] Bump zm-py version to v0.5.3 for zoneminder (#107331) zm-py version bump for zoneminder Signed-off-by: Nic Boet --- CODEOWNERS | 2 +- homeassistant/components/zoneminder/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 494f3d42bee..dd538ace0ec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1550,7 +1550,7 @@ build.json @home-assistant/supervisor /tests/components/zodiac/ @JulienTant /homeassistant/components/zone/ @home-assistant/core /tests/components/zone/ @home-assistant/core -/homeassistant/components/zoneminder/ @rohankapoorcom +/homeassistant/components/zoneminder/ @rohankapoorcom @nabbi /homeassistant/components/zwave_js/ @home-assistant/z-wave /tests/components/zwave_js/ @home-assistant/z-wave /homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 80ecbe53315..309ce43101c 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -1,9 +1,9 @@ { "domain": "zoneminder", "name": "ZoneMinder", - "codeowners": ["@rohankapoorcom"], + "codeowners": ["@rohankapoorcom", "@nabbi"], "documentation": "https://www.home-assistant.io/integrations/zoneminder", "iot_class": "local_polling", "loggers": ["zoneminder"], - "requirements": ["zm-py==0.5.2"] + "requirements": ["zm-py==0.5.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index add3e51aac2..5c3c05a6103 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2890,7 +2890,7 @@ zigpy-znp==0.12.1 zigpy==0.60.4 # homeassistant.components.zoneminder -zm-py==0.5.2 +zm-py==0.5.3 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 From bee76db1c3eb305d40ee4292fe4d166aa446964b Mon Sep 17 00:00:00 2001 From: nic <31355096+nabbi@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:30:44 -0600 Subject: [PATCH 049/133] Retry zoneminder connection setup (#107519) * zoneminder setup retry connection Makes ZM setup be async for enabling connection retry attempts This also requires zm-py version bump v0.5.4 as that dependency was patched in conjunction to resolve this issue Closes #105271 Signed-off-by: Nic Boet * ruff format Signed-off-by: Nic Boet * ruff fixes Signed-off-by: Nic Boet * RequestsConnectionError Signed-off-by: Nic Boet * revert async changes Signed-off-by: Nic Boet --------- Signed-off-by: Nic Boet --- homeassistant/components/zoneminder/__init__.py | 10 +++++++++- homeassistant/components/zoneminder/camera.py | 6 ++++-- homeassistant/components/zoneminder/manifest.json | 2 +- homeassistant/components/zoneminder/sensor.py | 5 ++++- homeassistant/components/zoneminder/switch.py | 6 ++++-- requirements_all.txt | 2 +- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 5e9c881af85..1ff73048440 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -1,6 +1,7 @@ """Support for ZoneMinder.""" import logging +from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from zoneminder.zm import ZoneMinder @@ -75,7 +76,14 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: ) hass.data[DOMAIN][host_name] = zm_client - success = zm_client.login() and success + try: + success = zm_client.login() and success + except RequestsConnectionError as ex: + _LOGGER.error( + "ZoneMinder connection failure to %s: %s", + host_name, + ex, + ) def set_active_state(call: ServiceCall) -> None: """Set the ZoneMinder run state to the given state name.""" diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index e87e6f814cc..d8b2aa805e7 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -8,6 +8,7 @@ from zoneminder.zm import ZoneMinder from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -28,8 +29,9 @@ def setup_platform( zm_client: ZoneMinder for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): if not (monitors := zm_client.get_monitors()): - _LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s") - return + raise PlatformNotReady( + "Camera could not fetch any monitors from ZoneMinder" + ) for monitor in monitors: _LOGGER.info("Initializing camera %s", monitor.id) diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 309ce43101c..f441a800555 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/zoneminder", "iot_class": "local_polling", "loggers": ["zoneminder"], - "requirements": ["zm-py==0.5.3"] + "requirements": ["zm-py==0.5.4"] } diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index c995e84343b..47863b5a5df 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -77,7 +78,9 @@ def setup_platform( zm_client: ZoneMinder for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): if not (monitors := zm_client.get_monitors()): - _LOGGER.warning("Could not fetch any monitors from ZoneMinder") + raise PlatformNotReady( + "Sensor could not fetch any monitors from ZoneMinder" + ) for monitor in monitors: sensors.append(ZMSensorMonitors(monitor)) diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index 985866272a6..b722ef53a77 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -11,6 +11,7 @@ from zoneminder.zm import ZoneMinder from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -42,8 +43,9 @@ def setup_platform( zm_client: ZoneMinder for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): if not (monitors := zm_client.get_monitors()): - _LOGGER.warning("Could not fetch monitors from ZoneMinder") - return + raise PlatformNotReady( + "Switch could not fetch any monitors from ZoneMinder" + ) for monitor in monitors: switches.append(ZMSwitchMonitors(monitor, on_state, off_state)) diff --git a/requirements_all.txt b/requirements_all.txt index 5c3c05a6103..b485035bf2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2890,7 +2890,7 @@ zigpy-znp==0.12.1 zigpy==0.60.4 # homeassistant.components.zoneminder -zm-py==0.5.3 +zm-py==0.5.4 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 From f18ab5e1ccf840ab3b5260f80115fa91c2e7375e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 12 Jan 2024 09:55:28 +0100 Subject: [PATCH 050/133] Don't include position in binary valve attributes (#107531) --- homeassistant/components/valve/__init__.py | 5 +- .../components/valve/snapshots/test_init.ambr | 56 +++++++++++++++++++ tests/components/valve/test_init.py | 21 +++++-- 3 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 tests/components/valve/snapshots/test_init.ambr diff --git a/homeassistant/components/valve/__init__.py b/homeassistant/components/valve/__init__.py index 9521d597303..c04e25355ff 100644 --- a/homeassistant/components/valve/__init__.py +++ b/homeassistant/components/valve/__init__.py @@ -186,9 +186,10 @@ class ValveEntity(Entity): @final @property - def state_attributes(self) -> dict[str, Any]: + def state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" - + if not self.reports_position: + return None return {ATTR_CURRENT_POSITION: self.current_valve_position} @property diff --git a/tests/components/valve/snapshots/test_init.ambr b/tests/components/valve/snapshots/test_init.ambr new file mode 100644 index 00000000000..b46d76b6f0c --- /dev/null +++ b/tests/components/valve/snapshots/test_init.ambr @@ -0,0 +1,56 @@ +# serializer version: 1 +# name: test_valve_setup + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Valve', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.valve', + 'last_changed': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_valve_setup.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 50, + 'friendly_name': 'Valve', + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.valve_2', + 'last_changed': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_valve_setup.2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Valve', + 'restored': True, + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.valve', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_valve_setup.3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Valve', + 'restored': True, + 'supported_features': , + }), + 'context': , + 'entity_id': 'valve.valve_2', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index 08b0771da8e..6f5c49830bb 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -2,6 +2,7 @@ from collections.abc import Generator import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.valve import ( DOMAIN, @@ -193,26 +194,34 @@ def mock_config_entry(hass) -> tuple[MockConfigEntry, list[ValveEntity]]: async def test_valve_setup( - hass: HomeAssistant, mock_config_entry: tuple[MockConfigEntry, list[ValveEntity]] + hass: HomeAssistant, + mock_config_entry: tuple[MockConfigEntry, list[ValveEntity]], + snapshot: SnapshotAssertion, ) -> None: """Test setup and tear down of valve platform and entity.""" config_entry = mock_config_entry[0] assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - entity_id = mock_config_entry[1][0].entity_id assert config_entry.state == ConfigEntryState.LOADED - assert hass.states.get(entity_id) + for entity in mock_config_entry[1]: + entity_id = entity.entity_id + state = hass.states.get(entity_id) + assert state + assert state == snapshot assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.NOT_LOADED - entity_state = hass.states.get(entity_id) - assert entity_state - assert entity_state.state == STATE_UNAVAILABLE + for entity in mock_config_entry[1]: + entity_id = entity.entity_id + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + assert state == snapshot async def test_services( From cf1a528b7a28467e98cbe8d967ecea1086ff45f4 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 8 Jan 2024 09:11:19 -0500 Subject: [PATCH 051/133] Bump blinkpy to 0.22.5 (#107537) bump blinkpy 0.22.5 --- 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 a1268919052..6e9d912f332 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.22.4"] + "requirements": ["blinkpy==0.22.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b485035bf2c..bbf19d8515e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -547,7 +547,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.4 +blinkpy==0.22.5 # homeassistant.components.bitcoin blockchain==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4884091cad..a2edc1aca86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -466,7 +466,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.4 +blinkpy==0.22.5 # homeassistant.components.blue_current bluecurrent-api==1.0.6 From 7bdabce68f9dc4e00897953c35fa77c34a15126e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 8 Jan 2024 17:01:19 +0100 Subject: [PATCH 052/133] Bump reolink_aio to 0.8.6 (#107541) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index d5116af0071..5670aea87ad 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.8.5"] + "requirements": ["reolink-aio==0.8.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index bbf19d8515e..7dcb857e0a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2376,7 +2376,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.5 +reolink-aio==0.8.6 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2edc1aca86..c2dab3aa258 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1798,7 +1798,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.5 +reolink-aio==0.8.6 # homeassistant.components.rflink rflink==0.0.65 From fcab683cc06dbc543a429b5d8fa88672c4fbd334 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:32:37 +0100 Subject: [PATCH 053/133] Bump mcstatus to v11.1.1 (#107546) * Bump mcstatus to 11.1.0 * Bump mcstatus to v11.1.1 --- homeassistant/components/minecraft_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/minecraft_server/const.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index 73a7dc18d09..a00936852f0 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["dnspython", "mcstatus"], "quality_scale": "gold", - "requirements": ["mcstatus==11.0.0"] + "requirements": ["mcstatus==11.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7dcb857e0a2..c3ea0369ccf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,7 +1240,7 @@ maxcube-api==0.4.3 mbddns==0.1.2 # homeassistant.components.minecraft_server -mcstatus==11.0.0 +mcstatus==11.1.1 # homeassistant.components.meater meater-python==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2dab3aa258..76079454609 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -976,7 +976,7 @@ maxcube-api==0.4.3 mbddns==0.1.2 # homeassistant.components.minecraft_server -mcstatus==11.0.0 +mcstatus==11.1.1 # homeassistant.components.meater meater-python==0.0.8 diff --git a/tests/components/minecraft_server/const.py b/tests/components/minecraft_server/const.py index 56be9132f19..92d6c647d8f 100644 --- a/tests/components/minecraft_server/const.py +++ b/tests/components/minecraft_server/const.py @@ -41,6 +41,7 @@ TEST_JAVA_STATUS_RESPONSE = JavaStatusResponse( version=JavaStatusVersion.build(TEST_JAVA_STATUS_RESPONSE_RAW["version"]), motd=Motd.parse(TEST_JAVA_STATUS_RESPONSE_RAW["description"], bedrock=False), icon=None, + enforces_secure_chat=False, latency=5, ) From 644a823c45ba601b587ebe427c7dc4a0304faf7c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 11 Jan 2024 02:50:45 +0200 Subject: [PATCH 054/133] Bump aioshelly to 7.1.0 (#107593) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b56ce07bc30..82833bf34af 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==7.0.0"], + "requirements": ["aioshelly==7.1.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index c3ea0369ccf..15fcfc91155 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==7.0.0 +aioshelly==7.1.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76079454609..e2f0c7b040e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==7.0.0 +aioshelly==7.1.0 # homeassistant.components.skybell aioskybell==22.7.0 From f1fc5abbc2d73aefd8bcabd8a02bb2364028b814 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 10 Jan 2024 12:09:10 +0100 Subject: [PATCH 055/133] Fix Tado unique mobile device dispatcher (#107631) * Add unique home ID device dispatch * Adding fixture for new setup * Minor refactor work * Add check for unlinked to different homes * If the interface returns an error * Proper error handling * Feedback fixes * Comments for error in client * Typo * Update homeassistant/components/tado/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tado/__init__.py Co-authored-by: Martin Hjelmare * Update devices fix standard * Dispatch out of loop * Update dispatcher * Clean up --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/tado/__init__.py | 43 ++++++++++++++++--- homeassistant/components/tado/const.py | 2 +- .../components/tado/device_tracker.py | 12 +++--- .../tado/fixtures/mobile_devices.json | 26 +++++++++++ tests/components/tado/util.py | 5 +++ 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 tests/components/tado/fixtures/mobile_devices.json diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 7f166ccf01a..871d6c2e6b1 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -186,12 +186,13 @@ class TadoConnector: def get_mobile_devices(self): """Return the Tado mobile devices.""" - return self.tado.getMobileDevices() + return self.tado.get_mobile_devices() @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the registered zones.""" self.update_devices() + self.update_mobile_devices() self.update_zones() self.update_home() @@ -203,17 +204,31 @@ class TadoConnector: _LOGGER.error("Unable to connect to Tado while updating mobile devices") return + if not mobile_devices: + _LOGGER.debug("No linked mobile devices found for home ID %s", self.home_id) + return + + # Errors are planned to be converted to exceptions + # in PyTado library, so this can be removed + if "errors" in mobile_devices and mobile_devices["errors"]: + _LOGGER.error( + "Error for home ID %s while updating mobile devices: %s", + self.home_id, + mobile_devices["errors"], + ) + return + for mobile_device in mobile_devices: self.data["mobile_device"][mobile_device["id"]] = mobile_device + _LOGGER.debug( + "Dispatching update to %s mobile device: %s", + self.home_id, + mobile_device, + ) - _LOGGER.debug( - "Dispatching update to %s mobile devices: %s", - self.home_id, - mobile_devices, - ) dispatcher_send( self.hass, - SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED, + SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self.home_id), ) def update_devices(self): @@ -224,6 +239,20 @@ class TadoConnector: _LOGGER.error("Unable to connect to Tado while updating devices") return + if not devices: + _LOGGER.debug("No linked devices found for home ID %s", self.home_id) + return + + # Errors are planned to be converted to exceptions + # in PyTado library, so this can be removed + if "errors" in devices and devices["errors"]: + _LOGGER.error( + "Error for home ID %s while updating devices: %s", + self.home_id, + devices["errors"], + ) + return + for device in devices: device_short_serial_no = device["shortSerialNo"] _LOGGER.debug("Updating device %s", device_short_serial_no) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index c14906c3a89..ee24af29b9d 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -179,7 +179,7 @@ TADO_TO_HA_SWING_MODE_MAP = { DOMAIN = "tado" SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}_{}" -SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED = "tado_mobile_device_update_received" +SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED = "tado_mobile_device_update_received_{}" UNIQUE_ID = "unique_id" DEFAULT_NAME = "Tado" diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 9c50318639d..3ec75dee4bf 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any import voluptuous as vol @@ -22,6 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType +from . import TadoConnector from .const import CONF_HOME_ID, DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED _LOGGER = logging.getLogger(__name__) @@ -90,7 +90,7 @@ async def async_setup_entry( entry.async_on_unload( async_dispatcher_connect( hass, - SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED, + SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(tado.home_id), update_devices, ) ) @@ -99,12 +99,12 @@ async def async_setup_entry( @callback def add_tracked_entities( hass: HomeAssistant, - tado: Any, + tado: TadoConnector, async_add_entities: AddEntitiesCallback, tracked: set[str], ) -> None: """Add new tracker entities from Tado.""" - _LOGGER.debug("Fetching Tado devices from API") + _LOGGER.debug("Fetching Tado devices from API for (newly) tracked entities") new_tracked = [] for device_key, device in tado.data["mobile_device"].items(): if device_key in tracked: @@ -128,7 +128,7 @@ class TadoDeviceTrackerEntity(TrackerEntity): self, device_id: str, device_name: str, - tado: Any, + tado: TadoConnector, ) -> None: """Initialize a Tado Device Tracker entity.""" super().__init__() @@ -169,7 +169,7 @@ class TadoDeviceTrackerEntity(TrackerEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED, + SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self._tado.home_id), self.on_demand_update, ) ) diff --git a/tests/components/tado/fixtures/mobile_devices.json b/tests/components/tado/fixtures/mobile_devices.json new file mode 100644 index 00000000000..80700a1e426 --- /dev/null +++ b/tests/components/tado/fixtures/mobile_devices.json @@ -0,0 +1,26 @@ +[ + { + "name": "Home", + "id": 123456, + "settings": { + "geoTrackingEnabled": false, + "specialOffersEnabled": false, + "onDemandLogRetrievalEnabled": false, + "pushNotifications": { + "lowBatteryReminder": true, + "awayModeReminder": true, + "homeModeReminder": true, + "openWindowReminder": true, + "energySavingsReportReminder": true, + "incidentDetection": true, + "energyIqReminder": false + } + }, + "deviceMetadata": { + "platform": "Android", + "osVersion": "14", + "model": "Samsung", + "locale": "nl" + } + } +] diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 21e0e255ed1..dd7c108c984 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -17,6 +17,7 @@ async def async_init_integration( token_fixture = "tado/token.json" devices_fixture = "tado/devices.json" + mobile_devices_fixture = "tado/mobile_devices.json" me_fixture = "tado/me.json" weather_fixture = "tado/weather.json" home_state_fixture = "tado/home_state.json" @@ -70,6 +71,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/devices", text=load_fixture(devices_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/mobileDevices", + text=load_fixture(mobile_devices_fixture), + ) m.get( "https://my.tado.com/api/v2/devices/WR1/", text=load_fixture(device_wr1_fixture), From 26da7402a2ee8e82bfeed6c882aa34ea4a34464a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:08:40 +0100 Subject: [PATCH 056/133] Fix tplink_lte setup (#107642) --- homeassistant/components/tplink_lte/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tplink_lte/__init__.py b/homeassistant/components/tplink_lte/__init__.py index 378fd0a35d4..5ac3085520e 100644 --- a/homeassistant/components/tplink_lte/__init__.py +++ b/homeassistant/components/tplink_lte/__init__.py @@ -94,7 +94,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: tasks = [_setup_lte(hass, conf) for conf in domain_config] if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) for conf in domain_config: for notify_conf in conf.get(CONF_NOTIFY, []): From 5b84e50dc0158474b4bd8f9dfc668e9e23852647 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 9 Jan 2024 19:16:45 +0100 Subject: [PATCH 057/133] Prevent overriding cached attribute as property (#107657) * Prevent overriding cached attribute as property * Remove debug --- homeassistant/helpers/entity.py | 4 ++++ tests/helpers/test_entity.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 743d3675a3b..1f3f96f300c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -13,6 +13,7 @@ import logging import math import sys from timeit import default_timer as timer +from types import FunctionType from typing import ( TYPE_CHECKING, Any, @@ -374,6 +375,9 @@ class CachedProperties(type): # Check if an _attr_ class attribute exits and move it to __attr_. We check # __dict__ here because we don't care about _attr_ class attributes in parents. if attr_name in cls.__dict__: + attr = getattr(cls, attr_name) + if isinstance(attr, (FunctionType, property)): + raise TypeError(f"Can't override {attr_name} in subclass") setattr(cls, private_attr_name, getattr(cls, attr_name)) annotations = cls.__annotations__ if attr_name in annotations: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index a18d8963947..2e2aac570ea 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -2039,6 +2039,47 @@ async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> No assert getattr(ent[1], property) == values[0] +async def test_cached_entity_property_override(hass: HomeAssistant) -> None: + """Test overriding cached _attr_ raises.""" + + class EntityWithClassAttribute1(entity.Entity): + """A derived class which overrides an _attr_ from a parent.""" + + _attr_attribution: str + + class EntityWithClassAttribute2(entity.Entity): + """A derived class which overrides an _attr_ from a parent.""" + + _attr_attribution = "blabla" + + class EntityWithClassAttribute3(entity.Entity): + """A derived class which overrides an _attr_ from a parent.""" + + _attr_attribution: str = "blabla" + + class EntityWithClassAttribute4(entity.Entity): + @property + def _attr_not_cached(self): + return "blabla" + + class EntityWithClassAttribute5(entity.Entity): + def _attr_not_cached(self): + return "blabla" + + with pytest.raises(TypeError): + + class EntityWithClassAttribute6(entity.Entity): + @property + def _attr_attribution(self): + return "🤡" + + with pytest.raises(TypeError): + + class EntityWithClassAttribute7(entity.Entity): + def _attr_attribution(self): + return "🤡" + + async def test_entity_report_deprecated_supported_features_values( caplog: pytest.LogCaptureFixture, ) -> None: From 0705be607f6cb3c87250cf9feca3f9c6aa853d30 Mon Sep 17 00:00:00 2001 From: Jonas Fors Lellky Date: Fri, 12 Jan 2024 10:52:17 +0100 Subject: [PATCH 058/133] Set max and min temp for flexit_bacnet climate entity (#107665) 107655: Set max and min temp for flexit_bacnet climate entity --- homeassistant/components/flexit_bacnet/climate.py | 4 ++++ homeassistant/components/flexit_bacnet/const.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/flexit_bacnet/climate.py b/homeassistant/components/flexit_bacnet/climate.py index c15cb59a6f3..79846bee019 100644 --- a/homeassistant/components/flexit_bacnet/climate.py +++ b/homeassistant/components/flexit_bacnet/climate.py @@ -27,6 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DOMAIN, + MAX_TEMP, + MIN_TEMP, PRESET_TO_VENTILATION_MODE_MAP, VENTILATION_TO_PRESET_MODE_MAP, ) @@ -67,6 +69,8 @@ class FlexitClimateEntity(ClimateEntity): _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_max_temp = MAX_TEMP + _attr_min_temp = MIN_TEMP def __init__(self, device: FlexitBACnet) -> None: """Initialize the unit.""" diff --git a/homeassistant/components/flexit_bacnet/const.py b/homeassistant/components/flexit_bacnet/const.py index 269a88c4cec..ed52b45f05e 100644 --- a/homeassistant/components/flexit_bacnet/const.py +++ b/homeassistant/components/flexit_bacnet/const.py @@ -15,6 +15,9 @@ from homeassistant.components.climate import ( DOMAIN = "flexit_bacnet" +MAX_TEMP = 30 +MIN_TEMP = 10 + VENTILATION_TO_PRESET_MODE_MAP = { VENTILATION_MODE_STOP: PRESET_NONE, VENTILATION_MODE_AWAY: PRESET_AWAY, From 3386e0e766709675f1991fd1f2b03178e2ac06e8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 12 Jan 2024 08:44:38 +0100 Subject: [PATCH 059/133] Fix duplicated resource issue in System Monitor (#107671) * Fix duplicated resource issue * Only slug the argument --- homeassistant/components/systemmonitor/sensor.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 28929d07a7c..da6e35238ec 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -405,7 +405,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.add(f"{_type}_{argument}") + loaded_resources.add(f"{_type}_{slugify(argument)}") entities.append( SystemMonitorSensor( sensor_registry, @@ -425,7 +425,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.add(f"{_type}_{argument}") + loaded_resources.add(f"{_type}_{slugify(argument)}") entities.append( SystemMonitorSensor( sensor_registry, @@ -449,7 +449,7 @@ async def async_setup_entry( sensor_registry[(_type, argument)] = SensorData( argument, None, None, None, None ) - loaded_resources.add(f"{_type}_{argument}") + loaded_resources.add(f"{_type}_{slugify(argument)}") entities.append( SystemMonitorSensor( sensor_registry, @@ -478,10 +478,13 @@ async def async_setup_entry( # of mount points automatically discovered for resource in legacy_resources: if resource.startswith("disk_"): + check_resource = slugify(resource) _LOGGER.debug( - "Check resource %s already loaded in %s", resource, loaded_resources + "Check resource %s already loaded in %s", + check_resource, + loaded_resources, ) - if resource not in loaded_resources: + if check_resource not in loaded_resources: split_index = resource.rfind("_") _type = resource[:split_index] argument = resource[split_index + 1 :] From f993e923a3415fbbcfd84f6984299344ef967015 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 10 Jan 2024 16:23:42 +0100 Subject: [PATCH 060/133] Fix invalid alexa climate or water_heater state report with double listed targetSetpoint (#107673) --- homeassistant/components/alexa/capabilities.py | 16 ++++++++++------ tests/components/alexa/test_common.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 502912ee8de..ab3bd8591fd 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1112,13 +1112,17 @@ class AlexaThermostatController(AlexaCapability): """Return what properties this entity supports.""" properties = [{"name": "thermostatMode"}] supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE: + if self.entity.domain == climate.DOMAIN: + if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: + properties.append({"name": "lowerSetpoint"}) + properties.append({"name": "upperSetpoint"}) + if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE: + properties.append({"name": "targetSetpoint"}) + elif ( + self.entity.domain == water_heater.DOMAIN + and supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE + ): properties.append({"name": "targetSetpoint"}) - if supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE: - properties.append({"name": "targetSetpoint"}) - if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: - properties.append({"name": "lowerSetpoint"}) - properties.append({"name": "upperSetpoint"}) return properties def properties_proactively_reported(self) -> bool: diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index d3ea1bcda3e..8c9cea526b6 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -224,9 +224,20 @@ class ReportedProperties: def assert_equal(self, namespace, name, value): """Assert a property is equal to a given value.""" + prop_set = None + prop_count = 0 for prop in self.properties: if prop["namespace"] == namespace and prop["name"] == name: assert prop["value"] == value - return prop + prop_set = prop + prop_count += 1 + + if prop_count > 1: + pytest.fail( + f"property {namespace}:{name} more than once in {self.properties!r}" + ) + + if prop_set: + return prop_set pytest.fail(f"property {namespace}:{name} not in {self.properties!r}") From 7f8a157788108feb397ea810c44b35d67e555f67 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 10 Jan 2024 16:20:47 +0100 Subject: [PATCH 061/133] Redact sensitive data in alexa debug logging (#107676) * Redact sensitive data in alexa debug logging * Add wrappers to diagnostics module * Test http api log is redacted --- homeassistant/components/alexa/auth.py | 9 ++--- homeassistant/components/alexa/const.py | 3 ++ homeassistant/components/alexa/diagnostics.py | 34 +++++++++++++++++++ homeassistant/components/alexa/handlers.py | 1 - homeassistant/components/alexa/smart_home.py | 14 ++++++-- .../components/alexa/state_report.py | 11 ++++-- .../components/alexa/test_smart_home_http.py | 15 +++++--- 7 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/alexa/diagnostics.py diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 58095340146..527e51b5390 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -15,6 +15,9 @@ from homeassistant.helpers import aiohttp_client from homeassistant.helpers.storage import Store from homeassistant.util import dt as dt_util +from .const import STORAGE_ACCESS_TOKEN, STORAGE_REFRESH_TOKEN +from .diagnostics import async_redact_lwa_params + _LOGGER = logging.getLogger(__name__) LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token" @@ -24,8 +27,6 @@ PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300 STORAGE_KEY = "alexa_auth" STORAGE_VERSION = 1 STORAGE_EXPIRE_TIME = "expire_time" -STORAGE_ACCESS_TOKEN = "access_token" -STORAGE_REFRESH_TOKEN = "refresh_token" class Auth: @@ -56,7 +57,7 @@ class Auth: } _LOGGER.debug( "Calling LWA to get the access token (first time), with: %s", - json.dumps(lwa_params), + json.dumps(async_redact_lwa_params(lwa_params)), ) return await self._async_request_new_token(lwa_params) @@ -133,7 +134,7 @@ class Auth: return None response_json = await response.json() - _LOGGER.debug("LWA response body : %s", response_json) + _LOGGER.debug("LWA response body : %s", async_redact_lwa_params(response_json)) access_token: str = response_json["access_token"] refresh_token: str = response_json["refresh_token"] diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index f71bc091106..abdef0cb566 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -90,6 +90,9 @@ API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} # we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode PRESET_MODE_NA = "-" +STORAGE_ACCESS_TOKEN = "access_token" +STORAGE_REFRESH_TOKEN = "refresh_token" + class Cause: """Possible causes for property changes. diff --git a/homeassistant/components/alexa/diagnostics.py b/homeassistant/components/alexa/diagnostics.py new file mode 100644 index 00000000000..54233a0f432 --- /dev/null +++ b/homeassistant/components/alexa/diagnostics.py @@ -0,0 +1,34 @@ +"""Diagnostics helpers for Alexa.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import callback + +STORAGE_ACCESS_TOKEN = "access_token" +STORAGE_REFRESH_TOKEN = "refresh_token" + +TO_REDACT_LWA = { + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + STORAGE_ACCESS_TOKEN, + STORAGE_REFRESH_TOKEN, +} + +TO_REDACT_AUTH = {"correlationToken", "token"} + + +@callback +def async_redact_lwa_params(lwa_params: dict[str, str]) -> dict[str, str]: + """Redact lwa_params.""" + return async_redact_data(lwa_params, TO_REDACT_LWA) + + +@callback +def async_redact_auth_data(mapping: Mapping[Any, Any]) -> dict[str, str]: + """React auth data.""" + return async_redact_data(mapping, TO_REDACT_AUTH) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 5613da52db5..68702bc0533 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -144,7 +144,6 @@ async def async_api_accept_grant( Async friendly. """ auth_code: str = directive.payload["grant"]["code"] - _LOGGER.debug("AcceptGrant code: %s", auth_code) if config.supports_auth: await config.async_accept_grant(auth_code) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index a8101896116..88f66e93fc1 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -25,6 +25,7 @@ from .const import ( CONF_LOCALE, EVENT_ALEXA_SMART_HOME, ) +from .diagnostics import async_redact_auth_data from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS from .state_report import AlexaDirective @@ -149,12 +150,21 @@ class SmartHomeView(HomeAssistantView): user: User = request["hass_user"] message: dict[str, Any] = await request.json() - _LOGGER.debug("Received Alexa Smart Home request: %s", message) + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug( + "Received Alexa Smart Home request: %s", + async_redact_auth_data(message), + ) response = await async_handle_message( hass, self.smart_home_config, message, context=core.Context(user_id=user.id) ) - _LOGGER.debug("Sending Alexa Smart Home response: %s", response) + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug( + "Sending Alexa Smart Home response: %s", + async_redact_auth_data(response), + ) + return b"" if response is None else self.json(response) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index f1cf13a0a7e..20e66dfa084 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -34,6 +34,7 @@ from .const import ( DOMAIN, Cause, ) +from .diagnostics import async_redact_auth_data from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink @@ -43,6 +44,8 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEOUT = 10 +TO_REDACT = {"correlationToken", "token"} + class AlexaDirective: """An incoming Alexa directive.""" @@ -379,7 +382,9 @@ async def async_send_changereport_message( response_text = await response.text() if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) + _LOGGER.debug( + "Sent: %s", json.dumps(async_redact_auth_data(message_serialized)) + ) _LOGGER.debug("Received (%s): %s", response.status, response_text) if response.status == HTTPStatus.ACCEPTED: @@ -533,7 +538,9 @@ async def async_send_doorbell_event_message( response_text = await response.text() if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) + _LOGGER.debug( + "Sent: %s", json.dumps(async_redact_auth_data(message_serialized)) + ) _LOGGER.debug("Received (%s): %s", response.status, response_text) if response.status == HTTPStatus.ACCEPTED: diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index b0f78e958d7..1426eac5c5d 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -1,6 +1,7 @@ """Test Smart Home HTTP endpoints.""" from http import HTTPStatus import json +import logging from typing import Any import pytest @@ -44,11 +45,16 @@ async def do_http_discovery(config, hass, hass_client): ], ) async def test_http_api( - hass: HomeAssistant, hass_client: ClientSessionGenerator, config: dict[str, Any] + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + hass_client: ClientSessionGenerator, + config: dict[str, Any], ) -> None: - """With `smart_home:` HTTP API is exposed.""" - response = await do_http_discovery(config, hass, hass_client) - response_data = await response.json() + """With `smart_home:` HTTP API is exposed and debug log is redacted.""" + with caplog.at_level(logging.DEBUG): + response = await do_http_discovery(config, hass, hass_client) + response_data = await response.json() + assert "'correlationToken': '**REDACTED**'" in caplog.text # Here we're testing just the HTTP view glue -- details of discovery are # covered in other tests. @@ -61,5 +67,4 @@ async def test_http_api_disabled( """Without `smart_home:`, the HTTP API is disabled.""" config = {"alexa": {}} response = await do_http_discovery(config, hass, hass_client) - assert response.status == HTTPStatus.NOT_FOUND From 2a46f201cbda09e3b5ca495c2aad9d17c98d2635 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 9 Jan 2024 23:01:19 +0100 Subject: [PATCH 062/133] Fix `device_class` type for Shelly Gen1 sleeping sensors (#107683) --- homeassistant/components/shelly/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 89dc10f0530..c7d89f2d284 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -36,6 +36,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType +from homeassistant.util.enum import try_parse_enum from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator @@ -969,7 +970,7 @@ def _build_block_description(entry: RegistryEntry) -> BlockSensorDescription: name="", icon=entry.original_icon, native_unit_of_measurement=entry.unit_of_measurement, - device_class=entry.original_device_class, + device_class=try_parse_enum(SensorDeviceClass, entry.original_device_class), ) From 765c520d7a722b82c0ea2b0408b3f088519d8043 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Jan 2024 17:08:49 -1000 Subject: [PATCH 063/133] Clamp tplink color temp to valid range (#107695) --- homeassistant/components/tplink/light.py | 32 ++++++++++++++++++------ tests/components/tplink/test_light.py | 20 +++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 8e77c68a880..071e0506c58 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -220,6 +220,26 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): hue, sat = tuple(int(val) for val in hs_color) await self.device.set_hsv(hue, sat, brightness, transition=transition) + async def _async_set_color_temp( + self, color_temp: float | int, brightness: int | None, transition: int | None + ) -> None: + device = self.device + valid_temperature_range = device.valid_temperature_range + requested_color_temp = round(color_temp) + # Clamp color temp to valid range + # since if the light in a group we will + # get requests for color temps for the range + # of the group and not the light + clamped_color_temp = min( + valid_temperature_range.max, + max(valid_temperature_range.min, requested_color_temp), + ) + await device.set_color_temp( + clamped_color_temp, + brightness=brightness, + transition=transition, + ) + async def _async_turn_on_with_brightness( self, brightness: int | None, transition: int | None ) -> None: @@ -234,10 +254,8 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Turn the light on.""" brightness, transition = self._async_extract_brightness_transition(**kwargs) if ATTR_COLOR_TEMP_KELVIN in kwargs: - await self.device.set_color_temp( - int(kwargs[ATTR_COLOR_TEMP_KELVIN]), - brightness=brightness, - transition=transition, + await self._async_set_color_temp( + kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition ) if ATTR_HS_COLOR in kwargs: await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition) @@ -324,10 +342,8 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): # we have to set an HSV value to clear the effect # before we can set a color temp await self.device.set_hsv(0, 0, brightness) - await self.device.set_color_temp( - int(kwargs[ATTR_COLOR_TEMP_KELVIN]), - brightness=brightness, - transition=transition, + await self._async_set_color_temp( + kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition ) elif ATTR_HS_COLOR in kwargs: await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition) diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 348fcc50ce0..ada454e0192 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -264,6 +264,26 @@ async def test_color_temp_light( bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None) bulb.set_color_temp.reset_mock() + # Verify color temp is clamped to the valid range + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 20000}, + blocking=True, + ) + bulb.set_color_temp.assert_called_with(9000, brightness=None, transition=None) + bulb.set_color_temp.reset_mock() + + # Verify color temp is clamped to the valid range + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 1}, + blocking=True, + ) + bulb.set_color_temp.assert_called_with(4000, brightness=None, transition=None) + bulb.set_color_temp.reset_mock() + async def test_brightness_only_light(hass: HomeAssistant) -> None: """Test a light.""" From 00b899ca3cb10f3b58ea892d3d932b785eed99d3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 12 Jan 2024 09:47:08 +0100 Subject: [PATCH 064/133] Fix cloud tts loading (#107714) --- homeassistant/components/cloud/__init__.py | 20 ++++---- tests/components/cloud/conftest.py | 33 ++++++++---- tests/components/cloud/test_http_api.py | 4 +- tests/components/cloud/test_init.py | 20 +++++++- tests/components/cloud/test_system_health.py | 1 + tests/components/cloud/test_tts.py | 54 +++++++++++++++++++- 6 files changed, 108 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 6e5cddd0f28..17c50778b2e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -291,7 +291,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } async def _on_start() -> None: - """Discover platforms.""" + """Handle cloud started after login.""" nonlocal loaded # Prevent multiple discovery @@ -299,14 +299,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return loaded = True - tts_info = {"platform_loaded": tts_platform_loaded} - - await async_load_platform(hass, Platform.TTS, DOMAIN, tts_info, config) - await tts_platform_loaded.wait() - - # The config entry should be loaded after the legacy tts platform is loaded - # to make sure that the tts integration is setup before we try to migrate - # old assist pipelines in the cloud stt entity. await hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) async def _on_connect() -> None: @@ -335,6 +327,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: account_link.async_setup(hass) + hass.async_create_task( + async_load_platform( + hass, + Platform.TTS, + DOMAIN, + {"platform_loaded": tts_platform_loaded}, + config, + ) + ) + async_call_later( hass=hass, delay=timedelta(hours=STARTUP_REPAIR_DELAY), diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 42852b15206..1e1877ae13c 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -76,16 +76,9 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]: # Attributes that we mock with default values. - mock_cloud.id_token = jwt.encode( - { - "email": "hello@home-assistant.io", - "custom:sub-exp": "2018-01-03", - "cognito:username": "abcdefghjkl", - }, - "test", - ) - mock_cloud.access_token = "test_access_token" - mock_cloud.refresh_token = "test_refresh_token" + mock_cloud.id_token = None + mock_cloud.access_token = None + mock_cloud.refresh_token = None # Properties that we keep as properties. @@ -122,11 +115,31 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]: When called, it should call the on_start callback. """ + mock_cloud.id_token = jwt.encode( + { + "email": "hello@home-assistant.io", + "custom:sub-exp": "2018-01-03", + "cognito:username": "abcdefghjkl", + }, + "test", + ) + mock_cloud.access_token = "test_access_token" + mock_cloud.refresh_token = "test_refresh_token" on_start_callback = mock_cloud.register_on_start.call_args[0][0] await on_start_callback() mock_cloud.login.side_effect = mock_login + async def mock_logout() -> None: + """Mock logout.""" + mock_cloud.id_token = None + mock_cloud.access_token = None + mock_cloud.refresh_token = None + await mock_cloud.stop() + await mock_cloud.client.logout_cleanups() + + mock_cloud.logout.side_effect = mock_logout + yield mock_cloud diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 29930632691..409d86d6e37 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -113,8 +113,8 @@ async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None: }, ) await hass.async_block_till_done() - on_start_callback = cloud.register_on_start.call_args[0][0] - await on_start_callback() + await cloud.login("test-user", "test-pass") + cloud.login.reset_mock() async def test_google_actions_sync( diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 850f8e12e02..c537169bf01 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -19,7 +19,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from tests.common import MockUser +from tests.common import MockConfigEntry, MockUser async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: @@ -230,6 +230,7 @@ async def test_async_get_or_create_cloudhook( """Test async_get_or_create_cloudhook.""" assert await async_setup_component(hass, "cloud", {"cloud": {}}) await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") webhook_id = "mock-webhook-id" cloudhook_url = "https://cloudhook.nabu.casa/abcdefg" @@ -262,7 +263,7 @@ async def test_async_get_or_create_cloudhook( async_create_cloudhook_mock.assert_not_called() # Simulate logged out - cloud.id_token = None + await cloud.logout() # Not logged in with pytest.raises(CloudNotAvailable): @@ -274,3 +275,18 @@ async def test_async_get_or_create_cloudhook( # Not connected with pytest.raises(CloudNotConnected): await async_get_or_create_cloudhook(hass, webhook_id) + + +async def test_cloud_logout( + hass: HomeAssistant, + cloud: MagicMock, +) -> None: + """Test cloud setup with existing config entry when user is logged out.""" + assert cloud.is_logged_in is False + + mock_config_entry = MockConfigEntry(domain=DOMAIN) + mock_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + + assert cloud.is_logged_in is False diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index 9f1af8aaeb4..5480cd557fd 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -42,6 +42,7 @@ async def test_cloud_system_health( }, ) await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") cloud.remote.snitun_server = "us-west-1" cloud.remote.certificate_status = CertificateStatus.READY diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index dc32747182d..4069edcb744 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -4,7 +4,7 @@ from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, MagicMock -from hass_nabucasa.voice import MAP_VOICE, VoiceError +from hass_nabucasa.voice import MAP_VOICE, VoiceError, VoiceTokenError import pytest import voluptuous as vol @@ -189,3 +189,55 @@ async def test_get_tts_audio( assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] == "female" assert mock_process_tts.call_args.kwargs["output"] == "mp3" + + +@pytest.mark.parametrize( + ("data", "expected_url_suffix"), + [ + ({"platform": DOMAIN}, DOMAIN), + ({"engine_id": DOMAIN}, DOMAIN), + ], +) +async def test_get_tts_audio_logged_out( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + cloud: MagicMock, + data: dict[str, Any], + expected_url_suffix: str, +) -> None: + """Test cloud get tts audio when user is logged out.""" + mock_process_tts = AsyncMock( + side_effect=VoiceTokenError("No token!"), + ) + cloud.voice.process_tts = mock_process_tts + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + client = await hass_client() + + url = "/api/tts_get_url" + data |= {"message": "There is someone at the door."} + + req = await client.post(url, json=data) + assert req.status == HTTPStatus.OK + response = await req.json() + + assert response == { + "url": ( + "http://example.local:8123/api/tts_proxy/" + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + ), + "path": ( + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + ), + } + await hass.async_block_till_done() + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == "en-US" + assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["output"] == "mp3" From d89659f1961a81aedf7204d177fcc9cf33417c5f Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 10 Jan 2024 14:03:02 +0100 Subject: [PATCH 065/133] Allow configuration of min_gradient from UI to be negative in Trend (#107720) Allow configuration of min_gradient to be negative from UI --- homeassistant/components/trend/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/trend/config_flow.py b/homeassistant/components/trend/config_flow.py index 457522dca82..3d29618281a 100644 --- a/homeassistant/components/trend/config_flow.py +++ b/homeassistant/components/trend/config_flow.py @@ -67,7 +67,6 @@ async def get_extended_options_schema(handler: SchemaCommonFlowHandler) -> vol.S CONF_MIN_GRADIENT, default=DEFAULT_MIN_GRADIENT ): selector.NumberSelector( selector.NumberSelectorConfig( - min=0, step="any", mode=selector.NumberSelectorMode.BOX, ), From b87bbd15297554ac68377fd4595dc405536f5b03 Mon Sep 17 00:00:00 2001 From: YogevBokobza Date: Wed, 10 Jan 2024 21:41:16 +0200 Subject: [PATCH 066/133] Bump aioswitcher to 3.4.1 (#107730) * switcher: added support for device_key logic included in aioswitcher==3.4.1 * switcher: small fix * switcher: after lint * switcher: fix missing device_key in tests * remove device_key function * fix missing device_key in tests --- homeassistant/components/switcher_kis/__init__.py | 3 ++- homeassistant/components/switcher_kis/button.py | 4 +++- homeassistant/components/switcher_kis/climate.py | 4 +++- homeassistant/components/switcher_kis/cover.py | 4 +++- homeassistant/components/switcher_kis/diagnostics.py | 2 +- homeassistant/components/switcher_kis/manifest.json | 2 +- homeassistant/components/switcher_kis/switch.py | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switcher_kis/consts.py | 8 ++++++++ tests/components/switcher_kis/test_diagnostics.py | 1 + 11 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index fb6ded99346..051c5d2b72a 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -89,8 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # New device - create device _LOGGER.info( - "Discovered Switcher device - id: %s, name: %s, type: %s (%s)", + "Discovered Switcher device - id: %s, key: %s, name: %s, type: %s (%s)", device.device_id, + device.device_key, device.name, device.device_type.value, device.device_type.hex_rep, diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py index 5a1b7c821d2..2085398232f 100644 --- a/homeassistant/components/switcher_kis/button.py +++ b/homeassistant/components/switcher_kis/button.py @@ -142,7 +142,9 @@ class SwitcherThermostatButtonEntity( try: async with SwitcherType2Api( - self.coordinator.data.ip_address, self.coordinator.data.device_id + self.coordinator.data.ip_address, + self.coordinator.data.device_id, + self.coordinator.data.device_key, ) as swapi: response = await self.entity_description.press_fn(swapi, self._remote) except (asyncio.TimeoutError, OSError, RuntimeError) as err: diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index 809e3d6a3ad..272d3ccf6ef 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -162,7 +162,9 @@ class SwitcherClimateEntity( try: async with SwitcherType2Api( - self.coordinator.data.ip_address, self.coordinator.data.device_id + self.coordinator.data.ip_address, + self.coordinator.data.device_id, + self.coordinator.data.device_key, ) as swapi: response = await swapi.control_breeze_device(self._remote, **kwargs) except (asyncio.TimeoutError, OSError, RuntimeError) as err: diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py index c627f361d7d..1e34ddd2325 100644 --- a/homeassistant/components/switcher_kis/cover.py +++ b/homeassistant/components/switcher_kis/cover.py @@ -98,7 +98,9 @@ class SwitcherCoverEntity( try: async with SwitcherType2Api( - self.coordinator.data.ip_address, self.coordinator.data.device_id + self.coordinator.data.ip_address, + self.coordinator.data.device_id, + self.coordinator.data.device_key, ) as swapi: response = await getattr(swapi, api)(*args) except (asyncio.TimeoutError, OSError, RuntimeError) as err: diff --git a/homeassistant/components/switcher_kis/diagnostics.py b/homeassistant/components/switcher_kis/diagnostics.py index 93b3c36bd21..765a3dde9e7 100644 --- a/homeassistant/components/switcher_kis/diagnostics.py +++ b/homeassistant/components/switcher_kis/diagnostics.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from .const import DATA_DEVICE, DOMAIN -TO_REDACT = {"device_id", "ip_address", "mac_address"} +TO_REDACT = {"device_id", "device_key", "ip_address", "mac_address"} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 9accda95912..055c92cc2fa 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["aioswitcher"], "quality_scale": "platinum", - "requirements": ["aioswitcher==3.3.0"] + "requirements": ["aioswitcher==3.4.1"] } diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index ef8564b3770..f37e16aa513 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -111,7 +111,9 @@ class SwitcherBaseSwitchEntity( try: async with SwitcherType1Api( - self.coordinator.data.ip_address, self.coordinator.data.device_id + self.coordinator.data.ip_address, + self.coordinator.data.device_id, + self.coordinator.data.device_key, ) as swapi: response = await getattr(swapi, api)(*args) except (asyncio.TimeoutError, OSError, RuntimeError) as err: diff --git a/requirements_all.txt b/requirements_all.txt index 15fcfc91155..4bc66778cb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -368,7 +368,7 @@ aioslimproto==2.3.3 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.3.0 +aioswitcher==3.4.1 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2f0c7b040e..5b43cfa28c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -341,7 +341,7 @@ aioslimproto==2.3.3 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.3.0 +aioswitcher==3.4.1 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index eaf6a69cb3d..aa0370bd347 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -26,6 +26,10 @@ DUMMY_DEVICE_ID1 = "a123bc" DUMMY_DEVICE_ID2 = "cafe12" DUMMY_DEVICE_ID3 = "bada77" DUMMY_DEVICE_ID4 = "bbd164" +DUMMY_DEVICE_KEY1 = "18" +DUMMY_DEVICE_KEY2 = "01" +DUMMY_DEVICE_KEY3 = "12" +DUMMY_DEVICE_KEY4 = "07" DUMMY_DEVICE_NAME1 = "Plug 23BC" DUMMY_DEVICE_NAME2 = "Heater FE12" DUMMY_DEVICE_NAME3 = "Breeze AB39" @@ -67,6 +71,7 @@ DUMMY_PLUG_DEVICE = SwitcherPowerPlug( DeviceType.POWER_PLUG, DeviceState.ON, DUMMY_DEVICE_ID1, + DUMMY_DEVICE_KEY1, DUMMY_IP_ADDRESS1, DUMMY_MAC_ADDRESS1, DUMMY_DEVICE_NAME1, @@ -78,6 +83,7 @@ DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater( DeviceType.V4, DeviceState.ON, DUMMY_DEVICE_ID2, + DUMMY_DEVICE_KEY2, DUMMY_IP_ADDRESS2, DUMMY_MAC_ADDRESS2, DUMMY_DEVICE_NAME2, @@ -91,6 +97,7 @@ DUMMY_SHUTTER_DEVICE = SwitcherShutter( DeviceType.RUNNER, DeviceState.ON, DUMMY_DEVICE_ID4, + DUMMY_DEVICE_KEY4, DUMMY_IP_ADDRESS4, DUMMY_MAC_ADDRESS4, DUMMY_DEVICE_NAME4, @@ -102,6 +109,7 @@ DUMMY_THERMOSTAT_DEVICE = SwitcherThermostat( DeviceType.BREEZE, DeviceState.ON, DUMMY_DEVICE_ID3, + DUMMY_DEVICE_KEY3, DUMMY_IP_ADDRESS3, DUMMY_MAC_ADDRESS3, DUMMY_DEVICE_NAME3, diff --git a/tests/components/switcher_kis/test_diagnostics.py b/tests/components/switcher_kis/test_diagnostics.py index f238bceb39e..f49ab99ba6c 100644 --- a/tests/components/switcher_kis/test_diagnostics.py +++ b/tests/components/switcher_kis/test_diagnostics.py @@ -25,6 +25,7 @@ async def test_diagnostics( { "auto_shutdown": "02:00:00", "device_id": REDACTED, + "device_key": REDACTED, "device_state": { "__type": "", "repr": "", From 40547974fb15bce06c927d1607d62bab28f57ae1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 11 Jan 2024 07:17:48 +0100 Subject: [PATCH 067/133] Fix mqtt text text min max config params can not be equal (#107738) Fix mqtt text text min max kan not be equal --- homeassistant/components/mqtt/text.py | 4 +- tests/components/mqtt/test_text.py | 59 ++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index da93a6b619e..fb121c25a9c 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -70,8 +70,8 @@ MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset( def valid_text_size_configuration(config: ConfigType) -> ConfigType: """Validate that the text length configuration is valid, throws if it isn't.""" - if config[CONF_MIN] >= config[CONF_MAX]: - raise vol.Invalid("text length min must be >= max") + if config[CONF_MIN] > config[CONF_MAX]: + raise vol.Invalid("text length min must be <= max") if config[CONF_MAX] > MAX_LENGTH_STATE_STATE: raise vol.Invalid(f"max text length must be <= {MAX_LENGTH_STATE_STATE}") diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index a602f1e3065..3aa2f96f478 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -115,6 +115,63 @@ async def test_controlling_state_via_topic( assert state.state == "" +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + text.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "min": 5, + "max": 5, + } + } + } + ], +) +async def test_forced_text_length( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a text entity that only allows a fixed length.""" + await mqtt_mock_entry() + + state = hass.states.get("text.test") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "12345") + state = hass.states.get("text.test") + assert state.state == "12345" + + caplog.clear() + # Text too long + async_fire_mqtt_message(hass, "state-topic", "123456") + state = hass.states.get("text.test") + assert state.state == "12345" + assert ( + "ValueError: Entity text.test provides state 123456 " + "which is too long (maximum length 5)" in caplog.text + ) + + caplog.clear() + # Text too short + async_fire_mqtt_message(hass, "state-topic", "1") + state = hass.states.get("text.test") + assert state.state == "12345" + assert ( + "ValueError: Entity text.test provides state 1 " + "which is too short (minimum length 5)" in caplog.text + ) + # Valid update + async_fire_mqtt_message(hass, "state-topic", "54321") + state = hass.states.get("text.test") + assert state.state == "54321" + + @pytest.mark.parametrize( "hass_config", [ @@ -211,7 +268,7 @@ async def test_attribute_validation_max_greater_then_min( ) -> None: """Test the validation of min and max configuration attributes.""" assert await mqtt_mock_entry() - assert "text length min must be >= max" in caplog.text + assert "text length min must be <= max" in caplog.text @pytest.mark.parametrize( From 504e4a792331965fc590c4c87b90ad42269f8a7f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 12 Jan 2024 09:56:13 +0100 Subject: [PATCH 068/133] Fix "not-logged" edge cases for Comelit VEDO (#107741) --- homeassistant/components/comelit/coordinator.py | 8 ++------ homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index 6559e2ffb87..4ff75ba5307 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -81,15 +81,11 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[dict[str, Any]]): try: await self.api.login() return await self._async_update_system_data() - except exceptions.CannotConnect as err: - _LOGGER.warning("Connection error for %s", self._host) - await self.api.close() - raise UpdateFailed(f"Error fetching data: {repr(err)}") from err + except (exceptions.CannotConnect, exceptions.CannotRetrieveData) as err: + raise UpdateFailed(repr(err)) from err except exceptions.CannotAuthenticate: raise ConfigEntryAuthFailed - return {} - @abstractmethod async def _async_update_system_data(self) -> dict[str, Any]: """Class method for updating data.""" diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index 8b50ccdf767..8c47564b165 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/comelit", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "requirements": ["aiocomelit==0.7.0"] + "requirements": ["aiocomelit==0.7.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4bc66778cb9..30902382b23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -215,7 +215,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.7.0 +aiocomelit==0.7.3 # homeassistant.components.dhcp aiodiscover==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b43cfa28c8..44e0f7b1341 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -194,7 +194,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.7.0 +aiocomelit==0.7.3 # homeassistant.components.dhcp aiodiscover==1.6.0 From 28da1ac69e0d6f0f75c24a11eadaf31219bc9055 Mon Sep 17 00:00:00 2001 From: Eugene Tiutiunnyk <4804824+eugenet8k@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:23:40 -0800 Subject: [PATCH 069/133] Fix Mac address check in kef integration (#107746) Fix the check for Mac address in kef integration (#106072) It might be due to an update of `getmac` dependency in some case the mac was resolved to "00:00:00:00:00:00" instead of the anticipated `None`. With that the original bug #47678 where a duplicated entity would be created in case of HA is restarted while the KEF speaker is offline came back. The PR #52902 was applied back in time to fix that issue. Now, this change is a continuation of the previous efforts. The solution was tested for about two months and it does address the bug with creating duplicated entities in case of KEF speakers being offline. --- homeassistant/components/kef/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 96f52ef7e03..b8407fd8bde 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -118,7 +118,7 @@ async def async_setup_platform( mode = get_ip_mode(host) mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host})) - if mac is None: + if mac is None or mac == "00:00:00:00:00:00": raise PlatformNotReady("Cannot get the ip address of kef speaker.") unique_id = f"kef-{mac}" From 70d1e6a2706897bc735ddc62f0096688289190ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Jan 2024 22:56:52 -1000 Subject: [PATCH 070/133] Fix ld2410_ble not being able to setup because it has a stale connection (#107754) --- homeassistant/components/ld2410_ble/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ld2410_ble/__init__.py b/homeassistant/components/ld2410_ble/__init__.py index e127a4a9836..57e3dfa4617 100644 --- a/homeassistant/components/ld2410_ble/__init__.py +++ b/homeassistant/components/ld2410_ble/__init__.py @@ -2,7 +2,11 @@ import logging -from bleak_retry_connector import BleakError, close_stale_connections, get_device +from bleak_retry_connector import ( + BleakError, + close_stale_connections_by_address, + get_device, +) from ld2410_ble import LD2410BLE from homeassistant.components import bluetooth @@ -24,6 +28,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LD2410 BLE from a config entry.""" address: str = entry.data[CONF_ADDRESS] + + await close_stale_connections_by_address(address) + ble_device = bluetooth.async_ble_device_from_address( hass, address.upper(), True ) or await get_device(address) @@ -32,8 +39,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Could not find LD2410B device with address {address}" ) - await close_stale_connections(ble_device) - ld2410_ble = LD2410BLE(ble_device) coordinator = LD2410BLECoordinator(hass, ld2410_ble) From 05964d6badab72c952dc10e21091a2f2b8175765 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 10 Jan 2024 19:38:56 -0500 Subject: [PATCH 071/133] Bump pyunifiprotect to 4.23.1 (#107758) --- 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 2fbf8f31071..d7c501a0bec 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.22.5", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.23.1", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 30902382b23..6372a0bb811 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2280,7 +2280,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.5 +pyunifiprotect==4.23.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44e0f7b1341..c03bdf69df6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1726,7 +1726,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.22.5 +pyunifiprotect==4.23.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 38f9fd57342cfe02ccd36270cc32d5606eea618f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 10 Jan 2024 23:04:37 -0500 Subject: [PATCH 072/133] Bump pyunifiprotect to 4.23.2 (#107769) --- 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 d7c501a0bec..edb2e28cc88 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.23.1", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.23.2", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 6372a0bb811..567d2cfde2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2280,7 +2280,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.1 +pyunifiprotect==4.23.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c03bdf69df6..5ccab4ae859 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1726,7 +1726,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.1 +pyunifiprotect==4.23.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From f7ad7c4235aaa5fad71f590dcb5b069889c3cdf7 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 11 Jan 2024 00:02:16 -0500 Subject: [PATCH 073/133] Rework events for UniFi Protect (#107771) --- .../components/unifiprotect/binary_sensor.py | 53 ++++++++----------- .../components/unifiprotect/models.py | 12 +---- .../components/unifiprotect/sensor.py | 4 +- .../components/unifiprotect/switch.py | 36 ++++++------- tests/components/unifiprotect/test_switch.py | 14 ++--- 5 files changed, 52 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 1104ecb98e1..b73e4669fbd 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -173,15 +173,6 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_value="is_vehicle_detection_on", ufp_perm=PermRequired.NO_WRITE, ), - ProtectBinaryEntityDescription( - key="smart_face", - name="Detections: Face", - icon="mdi:mdi-face", - entity_category=EntityCategory.DIAGNOSTIC, - ufp_required_field="can_detect_face", - ufp_value="is_face_detection_on", - ufp_perm=PermRequired.NO_WRITE, - ), ProtectBinaryEntityDescription( key="smart_package", name="Detections: Package", @@ -202,13 +193,22 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_smoke", - name="Detections: Smoke/CO", + name="Detections: Smoke", icon="mdi:fire", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_smoke", ufp_value="is_smoke_detection_on", ufp_perm=PermRequired.NO_WRITE, ), + ProtectBinaryEntityDescription( + key="smart_cmonx", + name="Detections: CO", + icon="mdi:molecule-co", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_co", + ufp_value="is_co_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), ) LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( @@ -342,7 +342,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, - ufp_value="is_motion_detected", + ufp_value="is_motion_currently_detected", ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), @@ -350,7 +350,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_obj_any", name="Object Detected", icon="mdi:eye", - ufp_value="is_smart_detected", + ufp_value="is_smart_currently_detected", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_detect_event", ), @@ -358,7 +358,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_obj_person", name="Person Detected", icon="mdi:walk", - ufp_value="is_smart_detected", + ufp_value="is_person_currently_detected", ufp_required_field="can_detect_person", ufp_enabled="is_person_detection_on", ufp_event_obj="last_person_detect_event", @@ -367,25 +367,16 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_obj_vehicle", name="Vehicle Detected", icon="mdi:car", - ufp_value="is_smart_detected", + ufp_value="is_vehicle_currently_detected", ufp_required_field="can_detect_vehicle", ufp_enabled="is_vehicle_detection_on", ufp_event_obj="last_vehicle_detect_event", ), - ProtectBinaryEventEntityDescription( - key="smart_obj_face", - name="Face Detected", - icon="mdi:mdi-face", - ufp_value="is_smart_detected", - ufp_required_field="can_detect_face", - ufp_enabled="is_face_detection_on", - ufp_event_obj="last_face_detect_event", - ), ProtectBinaryEventEntityDescription( key="smart_obj_package", name="Package Detected", icon="mdi:package-variant-closed", - ufp_value="is_smart_detected", + ufp_value="is_package_currently_detected", ufp_required_field="can_detect_package", ufp_enabled="is_package_detection_on", ufp_event_obj="last_package_detect_event", @@ -394,7 +385,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_any", name="Audio Object Detected", icon="mdi:eye", - ufp_value="is_smart_detected", + ufp_value="is_audio_currently_detected", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_audio_detect_event", ), @@ -402,7 +393,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( key="smart_audio_smoke", name="Smoke Alarm Detected", icon="mdi:fire", - ufp_value="is_smart_detected", + ufp_value="is_smoke_currently_detected", ufp_required_field="can_detect_smoke", ufp_enabled="is_smoke_detection_on", ufp_event_obj="last_smoke_detect_event", @@ -410,10 +401,10 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ProtectBinaryEventEntityDescription( key="smart_audio_cmonx", name="CO Alarm Detected", - icon="mdi:fire", - ufp_value="is_smart_detected", - ufp_required_field="can_detect_smoke", - ufp_enabled="is_smoke_detection_on", + icon="mdi:molecule-co", + ufp_value="is_cmonx_currently_detected", + ufp_required_field="can_detect_co", + ufp_enabled="is_co_detection_on", ufp_event_obj="last_cmonx_detect_event", ), ) @@ -619,7 +610,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity): @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: super()._async_update_device_from_protect(device) - is_on = self.entity_description.get_is_on(self._event) + is_on = self.entity_description.get_is_on(self.device, self._event) self._attr_is_on: bool | None = is_on if not is_on: self._event = None diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 7f5612a72a8..08f5c2075e6 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel from homeassistant.helpers.entity import EntityDescription -from homeassistant.util import dt as dt_util from .utils import get_nested_attr @@ -114,17 +113,10 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]): return cast(Event, getattr(obj, self.ufp_event_obj, None)) return None - def get_is_on(self, event: Event | None) -> bool: + def get_is_on(self, obj: T, event: Event | None) -> bool: """Return value if event is active.""" - if event is None: - return False - now = dt_util.utcnow() - value = now > event.start - if value and event.end is not None and now > event.end: - value = False - - return value + return event is not None and self.get_ufp_value(obj) @dataclass(frozen=True) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 3e2bd6ee858..abeb4859e6d 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -527,7 +527,7 @@ EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( name="License Plate Detected", icon="mdi:car", translation_key="license_plate", - ufp_value="is_smart_detected", + ufp_value="is_license_plate_currently_detected", ufp_required_field="can_detect_license_plate", ufp_event_obj="last_license_plate_detect_event", ), @@ -781,7 +781,7 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity): EventEntityMixin._async_update_device_from_protect(self, device) event = self._event entity_description = self.entity_description - is_on = entity_description.get_is_on(event) + is_on = entity_description.get_is_on(self.device, self._event) is_license_plate = ( entity_description.ufp_event_obj == "last_license_plate_detect_event" ) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index d8a3fc1c5bc..8466ffb6118 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -135,6 +135,16 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method="set_osd_bitrate", ufp_perm=PermRequired.WRITE, ), + ProtectSwitchEntityDescription( + key="color_night_vision", + name="Color Night Vision", + icon="mdi:light-flood-down", + entity_category=EntityCategory.CONFIG, + ufp_required_field="has_color_night_vision", + ufp_value="isp_settings.is_color_night_vision_enabled", + ufp_set_method="set_color_night_vision", + ufp_perm=PermRequired.WRITE, + ), ProtectSwitchEntityDescription( key="motion", name="Detections: Motion", @@ -167,17 +177,6 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method="set_vehicle_detection", ufp_perm=PermRequired.WRITE, ), - ProtectSwitchEntityDescription( - key="smart_face", - name="Detections: Face", - icon="mdi:human-greeting", - entity_category=EntityCategory.CONFIG, - ufp_required_field="can_detect_face", - ufp_value="is_face_detection_on", - ufp_enabled="is_recording_enabled", - ufp_set_method="set_face_detection", - ufp_perm=PermRequired.WRITE, - ), ProtectSwitchEntityDescription( key="smart_package", name="Detections: Package", @@ -202,7 +201,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_smoke", - name="Detections: Smoke/CO", + name="Detections: Smoke", icon="mdi:fire", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_smoke", @@ -212,13 +211,14 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( - key="color_night_vision", - name="Color Night Vision", - icon="mdi:light-flood-down", + key="smart_cmonx", + name="Detections: CO", + icon="mdi:molecule-co", entity_category=EntityCategory.CONFIG, - ufp_required_field="has_color_night_vision", - ufp_value="isp_settings.is_color_night_vision_enabled", - ufp_set_method="set_color_night_vision", + ufp_required_field="can_detect_co", + ufp_value="is_co_detection_on", + ufp_enabled="is_recording_enabled", + ufp_set_method="set_cmonx_detection", ufp_perm=PermRequired.WRITE, ), ) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 17db53d05ec..70a21a324d0 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -33,12 +33,14 @@ from .utils import ( CAMERA_SWITCHES_BASIC = [ d for d in CAMERA_SWITCHES - if d.name != "Detections: Face" - and d.name != "Detections: Package" - and d.name != "Detections: License Plate" - and d.name != "Detections: Smoke/CO" - and d.name != "SSH Enabled" - and d.name != "Color Night Vision" + if ( + not d.name.startswith("Detections:") + and d.name != "SSH Enabled" + and d.name != "Color Night Vision" + ) + or d.name == "Detections: Motion" + or d.name == "Detections: Person" + or d.name == "Detections: Vehicle" ] CAMERA_SWITCHES_NO_EXTRA = [ d for d in CAMERA_SWITCHES_BASIC if d.name not in ("High FPS", "Privacy Mode") From 48766c08e06ad0ae70ef8e6fdf76021ad85618a5 Mon Sep 17 00:00:00 2001 From: Ido Flatow Date: Thu, 11 Jan 2024 11:39:50 +0200 Subject: [PATCH 074/133] Fix switcher kis logging incorrect property for device's name (#107775) * use of incorrect property for device's name * Update switch.py according to Ruff formatter --- homeassistant/components/switcher_kis/switch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index f37e16aa513..88867393834 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -105,7 +105,9 @@ class SwitcherBaseSwitchEntity( async def _async_call_api(self, api: str, *args: Any) -> None: """Call Switcher API.""" - _LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args) + _LOGGER.debug( + "Calling api for %s, api: '%s', args: %s", self.coordinator.name, api, args + ) response: SwitcherBaseResponse = None error = None From e3a44e499c1d52c13f0bc0d0fc6251af676552d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Jan 2024 22:45:49 -1000 Subject: [PATCH 075/133] Bump bluetooth deps (#107816) --- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 6 +++--- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/bluetooth/conftest.py | 8 ++++---- tests/components/bluetooth/test_scanner.py | 1 + 8 files changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 7308f3a83ff..0e2a26381d2 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -17,9 +17,9 @@ "bleak==0.21.1", "bleak-retry-connector==3.4.0", "bluetooth-adapters==0.16.2", - "bluetooth-auto-recovery==1.2.3", + "bluetooth-auto-recovery==1.3.0", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.0", - "habluetooth==2.0.2" + "habluetooth==2.1.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 655f46a8838..5eebfa4181b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ bcrypt==4.0.1 bleak-retry-connector==3.4.0 bleak==0.21.1 bluetooth-adapters==0.16.2 -bluetooth-auto-recovery==1.2.3 +bluetooth-auto-recovery==1.3.0 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 @@ -24,10 +24,10 @@ dbus-fast==2.21.0 fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.1.0 -habluetooth==2.0.2 +habluetooth==2.1.0 hass-nabucasa==0.75.1 hassil==1.5.1 -home-assistant-bluetooth==1.11.0 +home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240104.0 home-assistant-intents==2024.1.2 httpx==0.26.0 diff --git a/pyproject.toml b/pyproject.toml index bbf45725716..bf3cb3411b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.26.0", - "home-assistant-bluetooth==1.11.0", + "home-assistant-bluetooth==1.12.0", "ifaddr==0.2.0", "Jinja2==3.1.2", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index 55cbdc31730..f86893bce46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.26.0 -home-assistant-bluetooth==1.11.0 +home-assistant-bluetooth==1.12.0 ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 567d2cfde2f..66d879b995f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -566,7 +566,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.16.2 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.3 +bluetooth-auto-recovery==1.3.0 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -998,7 +998,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.0.2 +habluetooth==2.1.0 # homeassistant.components.cloud hass-nabucasa==0.75.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ccab4ae859..ce3e2dbc82b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -478,7 +478,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.16.2 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.2.3 +bluetooth-auto-recovery==1.3.0 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -803,7 +803,7 @@ ha-philipsjs==3.1.1 habitipy==0.2.0 # homeassistant.components.bluetooth -habluetooth==2.0.2 +habluetooth==2.1.0 # homeassistant.components.cloud hass-nabucasa==0.75.1 diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 4ec6c4e5388..a7e776f3a26 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -55,7 +55,7 @@ def macos_adapter(): ), patch( "bluetooth_adapters.systems.platform.system", return_value="Darwin", - ): + ), patch("habluetooth.scanner.SYSTEM", "Darwin"): yield @@ -65,7 +65,7 @@ def windows_adapter(): with patch( "bluetooth_adapters.systems.platform.system", return_value="Windows", - ): + ), patch("habluetooth.scanner.SYSTEM", "Windows"): yield @@ -81,7 +81,7 @@ def no_adapter_fixture(): ), patch( "bluetooth_adapters.systems.platform.system", return_value="Linux", - ), patch( + ), patch("habluetooth.scanner.SYSTEM", "Linux"), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", ), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", @@ -102,7 +102,7 @@ def one_adapter_fixture(): ), patch( "bluetooth_adapters.systems.platform.system", return_value="Linux", - ), patch( + ), patch("habluetooth.scanner.SYSTEM", "Linux"), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", ), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 7673acb80dc..837c058fa6b 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -571,6 +571,7 @@ async def test_restart_takes_longer_than_watchdog_time( assert "already restarting" in caplog.text +@pytest.mark.skipif("platform.system() != 'Darwin'") async def test_setup_and_stop_macos( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None ) -> None: From 97b596a00d1ede0f9782674af6bc5080fca29ca6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Jan 2024 23:25:33 +0100 Subject: [PATCH 076/133] Fix Tailwind cover stuck in closing state (#107827) --- homeassistant/components/tailwind/cover.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tailwind/cover.py b/homeassistant/components/tailwind/cover.py index 935fa01eee0..335c3404cdd 100644 --- a/homeassistant/components/tailwind/cover.py +++ b/homeassistant/components/tailwind/cover.py @@ -121,5 +121,6 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity): translation_domain=DOMAIN, translation_key="communication_error", ) from exc - self._attr_is_closing = False + finally: + self._attr_is_closing = False await self.coordinator.async_request_refresh() From 061d2d3ccfa068d1600eb6c344489c4489fc7ecd Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 12 Jan 2024 01:30:55 -0800 Subject: [PATCH 077/133] Fix for exception in screenlogic.set_color_mode (#107850) --- homeassistant/components/screenlogic/services.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py index 17c52932e09..c9c66183daf 100644 --- a/homeassistant/components/screenlogic/services.py +++ b/homeassistant/components/screenlogic/services.py @@ -61,10 +61,7 @@ def async_load_screenlogic_services(hass: HomeAssistant): color_num, ) try: - if not await coordinator.gateway.async_set_color_lights(color_num): - raise HomeAssistantError( - f"Failed to call service '{SERVICE_SET_COLOR_MODE}'" - ) + await coordinator.gateway.async_set_color_lights(color_num) # Debounced refresh to catch any secondary # changes in the device await coordinator.async_request_refresh() From 51c75b020d748483db9d6ba5d36f672a88ecaa4c Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 13 Jan 2024 01:50:42 +1100 Subject: [PATCH 078/133] Bump aio_geojson_generic_client to 0.4 (#107866) --- homeassistant/components/geo_json_events/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 9f77f9b112e..8f4b36657dd 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aio_geojson_generic_client"], - "requirements": ["aio-geojson-generic-client==0.3"] + "requirements": ["aio-geojson-generic-client==0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 66d879b995f..872a05ee8fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -167,7 +167,7 @@ afsapi==0.2.7 agent-py==0.0.23 # homeassistant.components.geo_json_events -aio-geojson-generic-client==0.3 +aio-geojson-generic-client==0.4 # homeassistant.components.geonetnz_quakes aio-geojson-geonetnz-quakes==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce3e2dbc82b..df239a77e63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ afsapi==0.2.7 agent-py==0.0.23 # homeassistant.components.geo_json_events -aio-geojson-generic-client==0.3 +aio-geojson-generic-client==0.4 # homeassistant.components.geonetnz_quakes aio-geojson-geonetnz-quakes==0.15 From 9c6cb5347c1343f72452c1c031977d204367587a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Jan 2024 16:10:51 +0100 Subject: [PATCH 079/133] Bump version to 2024.1.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 c91743e7ba9..9ddb002c261 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index bf3cb3411b5..00d8b70f492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.2" +version = "2024.1.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 596f19055e64ee0cf93fa0cb964db38a05886aa3 Mon Sep 17 00:00:00 2001 From: Paarth Shah Date: Tue, 16 Jan 2024 01:07:51 -0800 Subject: [PATCH 080/133] Fix MatrixBot not resolving room aliases per-command (#106347) --- homeassistant/components/matrix/__init__.py | 7 +- tests/components/matrix/conftest.py | 72 ++++++-- tests/components/matrix/test_commands.py | 180 ++++++++++++++++++++ tests/components/matrix/test_matrix_bot.py | 66 +------ 4 files changed, 246 insertions(+), 79 deletions(-) create mode 100644 tests/components/matrix/test_commands.py diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 44a65a2de59..e91ee4d270c 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -223,9 +223,12 @@ class MatrixBot: def _load_commands(self, commands: list[ConfigCommand]) -> None: for command in commands: # Set the command for all listening_rooms, unless otherwise specified. - command.setdefault(CONF_ROOMS, list(self._listening_rooms.values())) + if rooms := command.get(CONF_ROOMS): + command[CONF_ROOMS] = [self._listening_rooms[room] for room in rooms] + else: + command[CONF_ROOMS] = list(self._listening_rooms.values()) - # COMMAND_SCHEMA guarantees that exactly one of CONF_WORD and CONF_expression are set. + # COMMAND_SCHEMA guarantees that exactly one of CONF_WORD and CONF_EXPRESSION are set. if (word_command := command.get(CONF_WORD)) is not None: for room_id in command[CONF_ROOMS]: self._word_commands.setdefault(room_id, {}) diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index 1198d7e6012..3e7d4833d6f 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -31,6 +31,8 @@ from homeassistant.components.matrix import ( CONF_WORD, EVENT_MATRIX_COMMAND, MatrixBot, + RoomAlias, + RoomAnyID, RoomID, ) from homeassistant.components.matrix.const import DOMAIN as MATRIX_DOMAIN @@ -51,13 +53,15 @@ from tests.common import async_capture_events TEST_NOTIFIER_NAME = "matrix_notify" TEST_HOMESERVER = "example.com" -TEST_DEFAULT_ROOM = "!DefaultNotificationRoom:example.com" -TEST_ROOM_A_ID = "!RoomA-ID:example.com" -TEST_ROOM_B_ID = "!RoomB-ID:example.com" -TEST_ROOM_B_ALIAS = "#RoomB-Alias:example.com" -TEST_JOINABLE_ROOMS = { +TEST_DEFAULT_ROOM = RoomID("!DefaultNotificationRoom:example.com") +TEST_ROOM_A_ID = RoomID("!RoomA-ID:example.com") +TEST_ROOM_B_ID = RoomID("!RoomB-ID:example.com") +TEST_ROOM_B_ALIAS = RoomAlias("#RoomB-Alias:example.com") +TEST_ROOM_C_ID = RoomID("!RoomC-ID:example.com") +TEST_JOINABLE_ROOMS: dict[RoomAnyID, RoomID] = { TEST_ROOM_A_ID: TEST_ROOM_A_ID, TEST_ROOM_B_ALIAS: TEST_ROOM_B_ID, + TEST_ROOM_C_ID: TEST_ROOM_C_ID, } TEST_BAD_ROOM = "!UninvitedRoom:example.com" TEST_MXID = "@user:example.com" @@ -74,7 +78,7 @@ class _MockAsyncClient(AsyncClient): async def close(self): return None - async def room_resolve_alias(self, room_alias: str): + async def room_resolve_alias(self, room_alias: RoomAnyID): if room_id := TEST_JOINABLE_ROOMS.get(room_alias): return RoomResolveAliasResponse( room_alias=room_alias, room_id=room_id, servers=[TEST_HOMESERVER] @@ -150,6 +154,16 @@ MOCK_CONFIG_DATA = { CONF_EXPRESSION: "My name is (?P.*)", CONF_NAME: "ExpressionTriggerEventName", }, + { + CONF_WORD: "WordTriggerSubset", + CONF_NAME: "WordTriggerSubsetEventName", + CONF_ROOMS: [TEST_ROOM_B_ALIAS, TEST_ROOM_C_ID], + }, + { + CONF_EXPRESSION: "Your name is (?P.*)", + CONF_NAME: "ExpressionTriggerSubsetEventName", + CONF_ROOMS: [TEST_ROOM_B_ALIAS, TEST_ROOM_C_ID], + }, ], }, NOTIFY_DOMAIN: { @@ -164,15 +178,32 @@ MOCK_WORD_COMMANDS = { "WordTrigger": { "word": "WordTrigger", "name": "WordTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], + "rooms": list(TEST_JOINABLE_ROOMS.values()), } }, TEST_ROOM_B_ID: { "WordTrigger": { "word": "WordTrigger", "name": "WordTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], - } + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + "WordTriggerSubset": { + "word": "WordTriggerSubset", + "name": "WordTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, + }, + TEST_ROOM_C_ID: { + "WordTrigger": { + "word": "WordTrigger", + "name": "WordTriggerEventName", + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + "WordTriggerSubset": { + "word": "WordTriggerSubset", + "name": "WordTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, }, } @@ -181,15 +212,32 @@ MOCK_EXPRESSION_COMMANDS = { { "expression": re.compile("My name is (?P.*)"), "name": "ExpressionTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], + "rooms": list(TEST_JOINABLE_ROOMS.values()), } ], TEST_ROOM_B_ID: [ { "expression": re.compile("My name is (?P.*)"), "name": "ExpressionTriggerEventName", - "rooms": [TEST_ROOM_A_ID, TEST_ROOM_B_ID], - } + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + { + "expression": re.compile("Your name is (?P.*)"), + "name": "ExpressionTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, + ], + TEST_ROOM_C_ID: [ + { + "expression": re.compile("My name is (?P.*)"), + "name": "ExpressionTriggerEventName", + "rooms": list(TEST_JOINABLE_ROOMS.values()), + }, + { + "expression": re.compile("Your name is (?P.*)"), + "name": "ExpressionTriggerSubsetEventName", + "rooms": [TEST_ROOM_B_ID, TEST_ROOM_C_ID], + }, ], } diff --git a/tests/components/matrix/test_commands.py b/tests/components/matrix/test_commands.py new file mode 100644 index 00000000000..cbf85ccc597 --- /dev/null +++ b/tests/components/matrix/test_commands.py @@ -0,0 +1,180 @@ +"""Test MatrixBot's ability to parse and respond to commands in matrix rooms.""" +from functools import partial +from itertools import chain +from typing import Any + +from nio import MatrixRoom, RoomMessageText +from pydantic.dataclasses import dataclass +import pytest + +from homeassistant.components.matrix import MatrixBot, RoomID +from homeassistant.core import Event, HomeAssistant + +from tests.components.matrix.conftest import ( + MOCK_EXPRESSION_COMMANDS, + MOCK_WORD_COMMANDS, + TEST_MXID, + TEST_ROOM_A_ID, + TEST_ROOM_B_ID, + TEST_ROOM_C_ID, +) + +ALL_ROOMS = (TEST_ROOM_A_ID, TEST_ROOM_B_ID, TEST_ROOM_C_ID) +SUBSET_ROOMS = (TEST_ROOM_B_ID, TEST_ROOM_C_ID) + + +@dataclass +class CommandTestParameters: + """Dataclass of parameters representing the command config parameters and expected result state. + + Switches behavior based on `room_id` and `expected_event_room_data`. + """ + + room_id: RoomID + room_message: RoomMessageText + expected_event_data_extra: dict[str, Any] | None + + @property + def expected_event_data(self) -> dict[str, Any] | None: + """Fully-constructed expected event data. + + Commands that are named with 'Subset' are expected not to be read from Room A. + """ + + if ( + self.expected_event_data_extra is None + or "Subset" in self.expected_event_data_extra["command"] + and self.room_id not in SUBSET_ROOMS + ): + return None + return { + "sender": "@SomeUser:example.com", + "room": self.room_id, + } | self.expected_event_data_extra + + +room_message_base = partial( + RoomMessageText, + formatted_body=None, + format=None, + source={ + "event_id": "fake_event_id", + "sender": "@SomeUser:example.com", + "origin_server_ts": 123456789, + }, +) +word_command_global = partial( + CommandTestParameters, + room_message=room_message_base(body="!WordTrigger arg1 arg2"), + expected_event_data_extra={ + "command": "WordTriggerEventName", + "args": ["arg1", "arg2"], + }, +) +expr_command_global = partial( + CommandTestParameters, + room_message=room_message_base(body="My name is FakeName"), + expected_event_data_extra={ + "command": "ExpressionTriggerEventName", + "args": {"name": "FakeName"}, + }, +) +word_command_subset = partial( + CommandTestParameters, + room_message=room_message_base(body="!WordTriggerSubset arg1 arg2"), + expected_event_data_extra={ + "command": "WordTriggerSubsetEventName", + "args": ["arg1", "arg2"], + }, +) +expr_command_subset = partial( + CommandTestParameters, + room_message=room_message_base(body="Your name is FakeName"), + expected_event_data_extra={ + "command": "ExpressionTriggerSubsetEventName", + "args": {"name": "FakeName"}, + }, +) +# Messages without commands should trigger nothing +fake_command_global = partial( + CommandTestParameters, + room_message=room_message_base(body="This is not a real command!"), + expected_event_data_extra=None, +) +# Valid commands sent by the bot user should trigger nothing +self_command_global = partial( + CommandTestParameters, + room_message=room_message_base( + body="!WordTrigger arg1 arg2", + source={ + "event_id": "fake_event_id", + "sender": TEST_MXID, + "origin_server_ts": 123456789, + }, + ), + expected_event_data_extra=None, +) + + +@pytest.mark.parametrize( + "command_params", + chain( + (word_command_global(room_id) for room_id in ALL_ROOMS), + (expr_command_global(room_id) for room_id in ALL_ROOMS), + (word_command_subset(room_id) for room_id in SUBSET_ROOMS), + (expr_command_subset(room_id) for room_id in SUBSET_ROOMS), + ), +) +async def test_commands( + hass: HomeAssistant, + matrix_bot: MatrixBot, + command_events: list[Event], + command_params: CommandTestParameters, +): + """Test that the configured commands are used correctly.""" + room = MatrixRoom(room_id=command_params.room_id, own_user_id=matrix_bot._mx_id) + + await hass.async_start() + assert len(command_events) == 0 + await matrix_bot._handle_room_message(room, command_params.room_message) + await hass.async_block_till_done() + + # MatrixBot should emit exactly one Event with matching data from this Command + assert len(command_events) == 1 + event = command_events[0] + assert event.data == command_params.expected_event_data + + +@pytest.mark.parametrize( + "command_params", + chain( + (word_command_subset(TEST_ROOM_A_ID),), + (expr_command_subset(TEST_ROOM_A_ID),), + (fake_command_global(room_id) for room_id in ALL_ROOMS), + (self_command_global(room_id) for room_id in ALL_ROOMS), + ), +) +async def test_non_commands( + hass: HomeAssistant, + matrix_bot: MatrixBot, + command_events: list[Event], + command_params: CommandTestParameters, +): + """Test that normal/non-qualifying messages don't wrongly trigger commands.""" + room = MatrixRoom(room_id=command_params.room_id, own_user_id=matrix_bot._mx_id) + + await hass.async_start() + assert len(command_events) == 0 + await matrix_bot._handle_room_message(room, command_params.room_message) + await hass.async_block_till_done() + + # MatrixBot should not treat this message as a Command + assert len(command_events) == 0 + + +async def test_commands_parsing(hass: HomeAssistant, matrix_bot: MatrixBot): + """Test that the configured commands were parsed correctly.""" + + await hass.async_start() + assert matrix_bot._word_commands == MOCK_WORD_COMMANDS + assert matrix_bot._expression_commands == MOCK_EXPRESSION_COMMANDS diff --git a/tests/components/matrix/test_matrix_bot.py b/tests/components/matrix/test_matrix_bot.py index 0048f6665e8..bfd6d5824cb 100644 --- a/tests/components/matrix/test_matrix_bot.py +++ b/tests/components/matrix/test_matrix_bot.py @@ -1,5 +1,4 @@ """Configure and test MatrixBot.""" -from nio import MatrixRoom, RoomMessageText from homeassistant.components.matrix import ( DOMAIN as MATRIX_DOMAIN, @@ -9,12 +8,7 @@ from homeassistant.components.matrix import ( from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant -from .conftest import ( - MOCK_EXPRESSION_COMMANDS, - MOCK_WORD_COMMANDS, - TEST_NOTIFIER_NAME, - TEST_ROOM_A_ID, -) +from .conftest import TEST_NOTIFIER_NAME async def test_services(hass: HomeAssistant, matrix_bot: MatrixBot): @@ -29,61 +23,3 @@ async def test_services(hass: HomeAssistant, matrix_bot: MatrixBot): # Verify that the matrix notifier is registered assert (notify_service := services.get(NOTIFY_DOMAIN)) assert TEST_NOTIFIER_NAME in notify_service - - -async def test_commands(hass, matrix_bot: MatrixBot, command_events): - """Test that the configured commands were parsed correctly.""" - - await hass.async_start() - assert len(command_events) == 0 - - assert matrix_bot._word_commands == MOCK_WORD_COMMANDS - assert matrix_bot._expression_commands == MOCK_EXPRESSION_COMMANDS - - room_id = TEST_ROOM_A_ID - room = MatrixRoom(room_id=room_id, own_user_id=matrix_bot._mx_id) - - # Test single-word command. - word_command_message = RoomMessageText( - body="!WordTrigger arg1 arg2", - formatted_body=None, - format=None, - source={ - "event_id": "fake_event_id", - "sender": "@SomeUser:example.com", - "origin_server_ts": 123456789, - }, - ) - await matrix_bot._handle_room_message(room, word_command_message) - await hass.async_block_till_done() - assert len(command_events) == 1 - event = command_events.pop() - assert event.data == { - "command": "WordTriggerEventName", - "sender": "@SomeUser:example.com", - "room": room_id, - "args": ["arg1", "arg2"], - } - - # Test expression command. - room = MatrixRoom(room_id=room_id, own_user_id=matrix_bot._mx_id) - expression_command_message = RoomMessageText( - body="My name is FakeName", - formatted_body=None, - format=None, - source={ - "event_id": "fake_event_id", - "sender": "@SomeUser:example.com", - "origin_server_ts": 123456789, - }, - ) - await matrix_bot._handle_room_message(room, expression_command_message) - await hass.async_block_till_done() - assert len(command_events) == 1 - event = command_events.pop() - assert event.data == { - "command": "ExpressionTriggerEventName", - "sender": "@SomeUser:example.com", - "room": room_id, - "args": {"name": "FakeName"}, - } From a8be7c27ad254538fc2f74f791323be143edb053 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:34:25 +0100 Subject: [PATCH 081/133] Bump Pyenphase to 1.16.0 (#107719) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 4ae7760a56b..67d07f0d502 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.15.2"], + "requirements": ["pyenphase==1.16.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 872a05ee8fb..55721ae3b9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1744,7 +1744,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.15.2 +pyenphase==1.16.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df239a77e63..2697cdcb8b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1328,7 +1328,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.15.2 +pyenphase==1.16.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 5c99c6e823261db1ac449e363b8c15e610b46215 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 16 Jan 2024 09:23:04 +0000 Subject: [PATCH 082/133] Fix loading empty yaml files with include_dir_named (#107853) --- homeassistant/util/yaml/loader.py | 5 +---- tests/util/yaml/test_init.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 60e917a6a99..5da5a84cc48 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -355,10 +355,7 @@ def _include_dir_named_yaml(loader: LoaderType, node: yaml.nodes.Node) -> NodeDi filename = os.path.splitext(os.path.basename(fname))[0] if os.path.basename(fname) == SECRET_YAML: continue - loaded_yaml = load_yaml(fname, loader.secrets) - if loaded_yaml is None: - continue - mapping[filename] = loaded_yaml + mapping[filename] = load_yaml(fname, loader.secrets) return _add_reference(mapping, loader, node) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 1e31d8c6955..30637fe2785 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -193,7 +193,7 @@ def test_include_dir_list_recursive( ), ( {"/test/first.yaml": "1", "/test/second.yaml": None}, - {"first": 1}, + {"first": 1, "second": None}, ), ], ) From e2ef8896870a41e295e9205fa88145b6df3bd61e Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:10:54 +0100 Subject: [PATCH 083/133] Bump openwebifpy to 4.2.1 (#107894) --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 42fbcb5b9bc..e298b3b714f 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/enigma2", "iot_class": "local_polling", "loggers": ["openwebif"], - "requirements": ["openwebifpy==4.0.4"] + "requirements": ["openwebifpy==4.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 55721ae3b9b..76a2a230e27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1425,7 +1425,7 @@ openhomedevice==2.2.0 opensensemap-api==0.2.0 # homeassistant.components.enigma2 -openwebifpy==4.0.4 +openwebifpy==4.2.1 # homeassistant.components.luci openwrt-luci-rpc==1.1.16 From c0c9fb0f008efe09c221e0bb7ffeefbb4057d1c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Jan 2024 07:27:52 -1000 Subject: [PATCH 084/133] Bump aiohomekit to 3.1.3 (#107929) changelog: https://github.com/Jc2k/aiohomekit/compare/3.1.2...3.1.3 fixes maybe #97888 --- 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 4af79a6f811..799058b0e20 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.1.2"], + "requirements": ["aiohomekit==3.1.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 76a2a230e27..7da414d2742 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,7 +257,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.2 +aiohomekit==3.1.3 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2697cdcb8b0..713c4806606 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.1.2 +aiohomekit==3.1.3 # homeassistant.components.http aiohttp-fast-url-dispatcher==0.3.0 From 80387be06130ba9886f7687ba6845aad6956147e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 13 Jan 2024 14:34:24 +0100 Subject: [PATCH 085/133] Skip disk types in System Monitor (#107943) * Skip disk types in System Monitor * change back --- homeassistant/components/systemmonitor/util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index 742e0d40f3d..aeb7816784b 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -7,6 +7,8 @@ import psutil _LOGGER = logging.getLogger(__name__) +SKIP_DISK_TYPES = {"proc", "tmpfs", "devtmpfs"} + def get_all_disk_mounts() -> set[str]: """Return all disk mount points on system.""" @@ -18,6 +20,9 @@ def get_all_disk_mounts() -> set[str]: # ENOENT, pop-up a Windows GUI error for a non-ready # partition or just hang. continue + if part.fstype in SKIP_DISK_TYPES: + # Ignore disks which are memory + continue try: usage = psutil.disk_usage(part.mountpoint) except PermissionError: From 488acc325231de4b184e032445d95f9db59875d1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 13 Jan 2024 13:48:02 +0100 Subject: [PATCH 086/133] Fix duplicate unique id in System Monitor (again) (#107947) Fix duplicate unique id in System Monitor --- homeassistant/components/systemmonitor/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index da6e35238ec..95437c7fa4c 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -405,7 +405,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.add(f"{_type}_{slugify(argument)}") + loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( sensor_registry, @@ -425,7 +425,7 @@ async def async_setup_entry( is_enabled = check_legacy_resource( f"{_type}_{argument}", legacy_resources ) - loaded_resources.add(f"{_type}_{slugify(argument)}") + loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( sensor_registry, @@ -449,7 +449,7 @@ async def async_setup_entry( sensor_registry[(_type, argument)] = SensorData( argument, None, None, None, None ) - loaded_resources.add(f"{_type}_{slugify(argument)}") + loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( sensor_registry, From 9551ff31ec10d0c3e0988b17deaef1d0bc304834 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:53:48 +0100 Subject: [PATCH 087/133] Bump pyenphase to 1.17.0 (#107950) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 67d07f0d502..4b3a4eadb3d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.16.0"], + "requirements": ["pyenphase==1.17.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 7da414d2742..8e24dab290a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1744,7 +1744,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.16.0 +pyenphase==1.17.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 713c4806606..c7c5e7b0672 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1328,7 +1328,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.16.0 +pyenphase==1.17.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 20b88e30f57af0496e16c473d1a82e8f9457561c Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 13 Jan 2024 22:33:02 +0100 Subject: [PATCH 088/133] Update sleep period for Shelly devices with buggy fw (#107961) * update sleep period for Shelly devices with buggy fw * code quality * update model list * add test * Apply review comments * fix test * use costant --- homeassistant/components/shelly/__init__.py | 19 +++++++++++++++++++ homeassistant/components/shelly/const.py | 13 +++++++++++++ tests/components/shelly/test_init.py | 18 ++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 6b7a00db8e2..6b8d100ea8f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -30,12 +30,15 @@ from homeassistant.helpers.device_registry import ( from homeassistant.helpers.typing import ConfigType from .const import ( + BLOCK_EXPECTED_SLEEP_PERIOD, + BLOCK_WRONG_SLEEP_PERIOD, CONF_COAP_PORT, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, DEFAULT_COAP_PORT, DOMAIN, LOGGER, + MODELS_WITH_WRONG_SLEEP_PERIOD, PUSH_UPDATE_ISSUE_ID, ) from .coordinator import ( @@ -162,6 +165,22 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] + # Some old firmware have a wrong sleep period hardcoded value. + # Following code block will force the right value for affected devices + if ( + sleep_period == BLOCK_WRONG_SLEEP_PERIOD + and entry.data["model"] in MODELS_WITH_WRONG_SLEEP_PERIOD + ): + LOGGER.warning( + "Updating stored sleep period for %s: from %s to %s", + entry.title, + sleep_period, + BLOCK_EXPECTED_SLEEP_PERIOD, + ) + data = {**entry.data} + data[CONF_SLEEP_PERIOD] = sleep_period = BLOCK_EXPECTED_SLEEP_PERIOD + hass.config_entries.async_update_entry(entry, data=data) + async def _async_block_device_setup() -> None: """Set up a block based device that is online.""" shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 1e2c22691fb..6cc513015d3 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -14,7 +14,10 @@ from aioshelly.const import ( MODEL_DIMMER, MODEL_DIMMER_2, MODEL_DUO, + MODEL_DW, + MODEL_DW_2, MODEL_GAS, + MODEL_HT, MODEL_MOTION, MODEL_MOTION_2, MODEL_RGBW2, @@ -55,6 +58,12 @@ MODELS_SUPPORTING_LIGHT_EFFECTS: Final = ( MODEL_RGBW2, ) +MODELS_WITH_WRONG_SLEEP_PERIOD: Final = ( + MODEL_DW, + MODEL_DW_2, + MODEL_HT, +) + # Bulbs that support white & color modes DUAL_MODE_LIGHT_MODELS: Final = ( MODEL_BULB, @@ -176,6 +185,10 @@ KELVIN_MAX_VALUE: Final = 6500 KELVIN_MIN_VALUE_WHITE: Final = 2700 KELVIN_MIN_VALUE_COLOR: Final = 3000 +# Sleep period +BLOCK_WRONG_SLEEP_PERIOD = 21600 +BLOCK_EXPECTED_SLEEP_PERIOD = 43200 + UPTIME_DEVIATION: Final = 5 # Time to wait before reloading entry upon device config change diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 643fc775cc4..bc0ba045a55 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -11,8 +11,12 @@ from aioshelly.exceptions import ( import pytest from homeassistant.components.shelly.const import ( + BLOCK_EXPECTED_SLEEP_PERIOD, + BLOCK_WRONG_SLEEP_PERIOD, CONF_BLE_SCANNER_MODE, + CONF_SLEEP_PERIOD, DOMAIN, + MODELS_WITH_WRONG_SLEEP_PERIOD, BLEScannerMode, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -309,3 +313,17 @@ async def test_entry_missing_gen(hass: HomeAssistant, mock_block_device) -> None assert entry.state is ConfigEntryState.LOADED assert hass.states.get("switch.test_name_channel_1").state is STATE_ON + + +@pytest.mark.parametrize(("model"), MODELS_WITH_WRONG_SLEEP_PERIOD) +async def test_sleeping_block_device_wrong_sleep_period( + hass: HomeAssistant, mock_block_device, model +) -> None: + """Test sleeping block device with wrong sleep period.""" + entry = await init_integration( + hass, 1, model=model, sleep_period=BLOCK_WRONG_SLEEP_PERIOD, skip_setup=True + ) + assert entry.data[CONF_SLEEP_PERIOD] == BLOCK_WRONG_SLEEP_PERIOD + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.data[CONF_SLEEP_PERIOD] == BLOCK_EXPECTED_SLEEP_PERIOD From 8a3eb149b7256dd258cbd74140e91823158e5095 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:32:25 -0500 Subject: [PATCH 089/133] Reload ZHA only a single time when the connection is lost multiple times (#107963) * Reload only a single time when the connection is lost multiple times * Ignore when reset task finishes, allow only one reset per `ZHAGateway` --- homeassistant/components/zha/core/gateway.py | 15 ++++++++-- tests/components/zha/test_gateway.py | 30 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 12e439f1059..3efdc77934a 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -142,7 +142,9 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.shutting_down = False + self._reload_task: asyncio.Task | None = None def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" @@ -231,12 +233,17 @@ class ZHAGateway: def connection_lost(self, exc: Exception) -> None: """Handle connection lost event.""" + _LOGGER.debug("Connection to the radio was lost: %r", exc) + if self.shutting_down: return - _LOGGER.debug("Connection to the radio was lost: %r", exc) + # Ensure we do not queue up multiple resets + if self._reload_task is not None: + _LOGGER.debug("Ignoring reset, one is already running") + return - self.hass.async_create_task( + self._reload_task = self.hass.async_create_task( self.hass.config_entries.async_reload(self.config_entry.entry_id) ) @@ -760,6 +767,10 @@ class ZHAGateway: async def shutdown(self) -> None: """Stop ZHA Controller Application.""" + if self.shutting_down: + _LOGGER.debug("Ignoring duplicate shutdown event") + return + _LOGGER.debug("Shutting down ZHA ControllerApplication") self.shutting_down = True diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 4f520920704..9c3cf7aa2f8 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -291,3 +291,33 @@ async def test_gateway_force_multi_pan_channel( _, config = zha_gateway.get_application_controller_data() assert config["network"]["channel"] == expected_channel + + +async def test_single_reload_on_multiple_connection_loss( + hass: HomeAssistant, + zigpy_app_controller: ControllerApplication, + config_entry: MockConfigEntry, +): + """Test that we only reload once when we lose the connection multiple times.""" + config_entry.add_to_hass(hass) + + zha_gateway = ZHAGateway(hass, {}, config_entry) + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): + await zha_gateway.async_initialize() + + with patch.object( + hass.config_entries, "async_reload", wraps=hass.config_entries.async_reload + ) as mock_reload: + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + zha_gateway.connection_lost(RuntimeError()) + + assert len(mock_reload.mock_calls) == 1 + + await hass.async_block_till_done() From 507cccdd532e6ab38dbf3fa04bed30d31ab8ae11 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 13 Jan 2024 20:39:34 +0100 Subject: [PATCH 090/133] Don't load entities for docker virtual ethernet interfaces in System Monitor (#107966) --- homeassistant/components/systemmonitor/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index aeb7816784b..75b437c19eb 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -45,6 +45,9 @@ def get_all_network_interfaces() -> set[str]: """Return all network interfaces on system.""" interfaces: set[str] = set() for interface, _ in psutil.net_if_addrs().items(): + if interface.startswith("veth"): + # Don't load docker virtual network interfaces + continue interfaces.add(interface) _LOGGER.debug("Adding interfaces: %s", ", ".join(interfaces)) return interfaces From 9c6f87dd117beff5ed36c556cfbaa4c12ed32dae Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 19 Jan 2024 02:40:36 +1000 Subject: [PATCH 091/133] Improve coordinator logic in Tessie to allow sleep (#107988) * Poll status before state * Tests --- .../components/tessie/binary_sensor.py | 4 +-- homeassistant/components/tessie/const.py | 10 +++++- .../components/tessie/coordinator.py | 22 ++++++++----- tests/components/tessie/common.py | 6 ++-- tests/components/tessie/conftest.py | 16 ++++++++- tests/components/tessie/test_coordinator.py | 33 ++++++++++--------- 6 files changed, 60 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 5edbb108568..e4c0d5d5c66 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, TessieStatus +from .const import DOMAIN, TessieState from .coordinator import TessieStateUpdateCoordinator from .entity import TessieEntity @@ -30,7 +30,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( TessieBinarySensorEntityDescription( key="state", device_class=BinarySensorDeviceClass.CONNECTIVITY, - is_on=lambda x: x == TessieStatus.ONLINE, + is_on=lambda x: x == TessieState.ONLINE, ), TessieBinarySensorEntityDescription( key="charge_state_battery_heater_on", diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index 2ba4e514579..7dea7e65555 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -13,13 +13,21 @@ MODELS = { } -class TessieStatus(StrEnum): +class TessieState(StrEnum): """Tessie status.""" ASLEEP = "asleep" ONLINE = "online" +class TessieStatus(StrEnum): + """Tessie status.""" + + ASLEEP = "asleep" + AWAKE = "awake" + WAITING = "waiting_for_sleep" + + class TessieSeatHeaterOptions(StrEnum): """Tessie seat heater options.""" diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 75cac088bde..c2106af665f 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientResponseError -from tessie_api import get_state +from tessie_api import get_state, get_status from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -45,11 +45,21 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self) -> dict[str, Any]: """Update vehicle data using Tessie API.""" try: + status = await get_status( + session=self.session, + api_key=self.api_key, + vin=self.vin, + ) + if status["status"] == TessieStatus.ASLEEP: + # Vehicle is asleep, no need to poll for data + self.data["state"] = status["status"] + return self.data + vehicle = await get_state( session=self.session, api_key=self.api_key, vin=self.vin, - use_cache=False, + use_cache=True, ) except ClientResponseError as e: if e.status == HTTPStatus.UNAUTHORIZED: @@ -57,13 +67,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise ConfigEntryAuthFailed from e raise e - if vehicle["state"] == TessieStatus.ONLINE: - # Vehicle is online, all data is fresh - return self._flatten(vehicle) - - # Vehicle is asleep, only update state - self.data["state"] = vehicle["state"] - return self.data + return self._flatten(vehicle) def _flatten( self, data: dict[str, Any], parent: str | None = None diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index ae80526e5d9..ccff7f62b1b 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -6,7 +6,7 @@ from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError from aiohttp.client import RequestInfo -from homeassistant.components.tessie.const import DOMAIN +from homeassistant.components.tessie.const import DOMAIN, TessieStatus from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant @@ -14,7 +14,9 @@ from tests.common import MockConfigEntry, load_json_object_fixture TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN) TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN) -TEST_VEHICLE_STATE_ASLEEP = load_json_object_fixture("asleep.json", DOMAIN) +TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE} +TEST_VEHICLE_STATUS_ASLEEP = {"status": TessieStatus.ASLEEP} + TEST_RESPONSE = {"result": True} TEST_RESPONSE_ERROR = {"result": False, "reason": "reason why"} diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index c7a344d54c5..02b3d56691e 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -5,7 +5,11 @@ from unittest.mock import patch import pytest -from .common import TEST_STATE_OF_ALL_VEHICLES, TEST_VEHICLE_STATE_ONLINE +from .common import ( + TEST_STATE_OF_ALL_VEHICLES, + TEST_VEHICLE_STATE_ONLINE, + TEST_VEHICLE_STATUS_AWAKE, +) @pytest.fixture @@ -18,6 +22,16 @@ def mock_get_state(): yield mock_get_state +@pytest.fixture +def mock_get_status(): + """Mock get_status function.""" + with patch( + "homeassistant.components.tessie.coordinator.get_status", + return_value=TEST_VEHICLE_STATUS_AWAKE, + ) as mock_get_status: + yield mock_get_status + + @pytest.fixture def mock_get_state_of_all_vehicles(): """Mock get_state_of_all_vehicles function.""" diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 311222466fd..65f91c6f33e 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -10,8 +10,7 @@ from .common import ( ERROR_AUTH, ERROR_CONNECTION, ERROR_UNKNOWN, - TEST_VEHICLE_STATE_ASLEEP, - TEST_VEHICLE_STATE_ONLINE, + TEST_VEHICLE_STATUS_ASLEEP, setup_platform, ) @@ -20,59 +19,61 @@ from tests.common import async_fire_time_changed WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) -async def test_coordinator_online(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_online( + hass: HomeAssistant, mock_get_state, mock_get_status +) -> None: """Tests that the coordinator handles online vehicles.""" - mock_get_state.return_value = TEST_VEHICLE_STATE_ONLINE await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() + mock_get_status.assert_called_once() mock_get_state.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_ON -async def test_coordinator_asleep(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_asleep(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles asleep vehicles.""" - mock_get_state.return_value = TEST_VEHICLE_STATE_ASLEEP await setup_platform(hass) + mock_get_status.return_value = TEST_VEHICLE_STATUS_ASLEEP async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_OFF -async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles client errors.""" - mock_get_state.side_effect = ERROR_UNKNOWN + mock_get_status.side_effect = ERROR_UNKNOWN await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE -async def test_coordinator_auth(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_auth(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles timeout errors.""" - mock_get_state.side_effect = ERROR_AUTH + mock_get_status.side_effect = ERROR_AUTH await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() -async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> None: +async def test_coordinator_connection(hass: HomeAssistant, mock_get_status) -> None: """Tests that the coordinator handles connection errors.""" - mock_get_state.side_effect = ERROR_CONNECTION + mock_get_status.side_effect = ERROR_CONNECTION await setup_platform(hass) async_fire_time_changed(hass, utcnow() + WAIT) await hass.async_block_till_done() - mock_get_state.assert_called_once() + mock_get_status.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE From ed31adc6dbfe037c6df9a9d820a980308daf50ed Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 15 Jan 2024 20:53:56 +0200 Subject: [PATCH 092/133] Fix Shelly Gen1 entity description restore (#108052) * Fix Shelly Gen1 entity description restore * Update tests/components/shelly/test_sensor.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- .../components/shelly/binary_sensor.py | 13 --------- homeassistant/components/shelly/entity.py | 28 +++++-------------- homeassistant/components/shelly/number.py | 21 +------------- homeassistant/components/shelly/sensor.py | 18 +----------- tests/components/shelly/test_sensor.py | 18 ++++++++++-- 5 files changed, 24 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index b07747f298e..4ad51e5cc0f 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.restore_state import RestoreEntity from .const import CONF_SLEEP_PERIOD @@ -210,16 +209,6 @@ RPC_SENSORS: Final = { } -def _build_block_description(entry: RegistryEntry) -> BlockBinarySensorDescription: - """Build description when restoring block attribute entities.""" - return BlockBinarySensorDescription( - key="", - name="", - icon=entry.original_icon, - device_class=entry.original_device_class, - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -248,7 +237,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockSleepingBinarySensor, - _build_block_description, ) else: async_setup_entry_attribute_entities( @@ -257,7 +245,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockBinarySensor, - _build_block_description, ) async_setup_entry_rest( hass, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 796402c8bba..3132f1f571e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -39,7 +39,6 @@ def async_setup_entry_attribute_entities( async_add_entities: AddEntitiesCallback, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, - description_class: Callable[[RegistryEntry], BlockEntityDescription], ) -> None: """Set up entities for attributes.""" coordinator = get_entry_data(hass)[config_entry.entry_id].block @@ -56,7 +55,6 @@ def async_setup_entry_attribute_entities( coordinator, sensors, sensor_class, - description_class, ) @@ -113,7 +111,6 @@ def async_restore_block_attribute_entities( coordinator: ShellyBlockCoordinator, sensors: Mapping[tuple[str, str], BlockEntityDescription], sensor_class: Callable, - description_class: Callable[[RegistryEntry], BlockEntityDescription], ) -> None: """Restore block attributes entities.""" entities = [] @@ -128,11 +125,12 @@ def async_restore_block_attribute_entities( continue attribute = entry.unique_id.split("-")[-1] - description = description_class(entry) + block_type = entry.unique_id.split("-")[-2].split("_")[0] - entities.append( - sensor_class(coordinator, None, attribute, description, entry, sensors) - ) + if description := sensors.get((block_type, attribute)): + entities.append( + sensor_class(coordinator, None, attribute, description, entry) + ) if not entities: return @@ -444,7 +442,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity): """Available.""" available = super().available - if not available or not self.entity_description.available: + if not available or not self.entity_description.available or self.block is None: return available return self.entity_description.available(self.block) @@ -559,10 +557,8 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): attribute: str, description: BlockEntityDescription, entry: RegistryEntry | None = None, - sensors: Mapping[tuple[str, str], BlockEntityDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" - self.sensors = sensors self.last_state: State | None = None self.coordinator = coordinator self.attribute = attribute @@ -587,11 +583,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): @callback def _update_callback(self) -> None: """Handle device update.""" - if ( - self.block is not None - or not self.coordinator.device.initialized - or self.sensors is None - ): + if self.block is not None or not self.coordinator.device.initialized: super()._update_callback() return @@ -607,13 +599,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): if sensor_id != entity_sensor: continue - description = self.sensors.get((block.type, sensor_id)) - if description is None: - continue - self.block = block - self.entity_description = description - LOGGER.debug("Entity %s attached to block", self.name) super()._update_callback() return diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 77d066a6106..5d35e71ce5d 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -1,7 +1,6 @@ """Number for Shelly.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass from typing import Any, Final, cast @@ -56,22 +55,6 @@ NUMBERS: Final = { } -def _build_block_description(entry: RegistryEntry) -> BlockNumberDescription: - """Build description when restoring block attribute entities.""" - assert entry.capabilities - return BlockNumberDescription( - key="", - name="", - icon=entry.original_icon, - native_unit_of_measurement=entry.unit_of_measurement, - device_class=entry.original_device_class, - native_min_value=cast(float, entry.capabilities.get("min")), - native_max_value=cast(float, entry.capabilities.get("max")), - native_step=cast(float, entry.capabilities.get("step")), - mode=cast(NumberMode, entry.capabilities.get("mode")), - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -85,7 +68,6 @@ async def async_setup_entry( async_add_entities, NUMBERS, BlockSleepingNumber, - _build_block_description, ) @@ -101,11 +83,10 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber): attribute: str, description: BlockNumberDescription, entry: RegistryEntry | None = None, - sensors: Mapping[tuple[str, str], BlockNumberDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" self.restored_data: NumberExtraStoredData | None = None - super().__init__(coordinator, block, attribute, description, entry, sensors) + super().__init__(coordinator, block, attribute, description, entry) async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index c7d89f2d284..b439a19e318 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,7 +1,6 @@ """Sensor for Shelly.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass from typing import Final, cast @@ -36,7 +35,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType -from homeassistant.util.enum import try_parse_enum from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator @@ -963,17 +961,6 @@ RPC_SENSORS: Final = { } -def _build_block_description(entry: RegistryEntry) -> BlockSensorDescription: - """Build description when restoring block attribute entities.""" - return BlockSensorDescription( - key="", - name="", - icon=entry.original_icon, - native_unit_of_measurement=entry.unit_of_measurement, - device_class=try_parse_enum(SensorDeviceClass, entry.original_device_class), - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -1002,7 +989,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockSleepingSensor, - _build_block_description, ) else: async_setup_entry_attribute_entities( @@ -1011,7 +997,6 @@ async def async_setup_entry( async_add_entities, SENSORS, BlockSensor, - _build_block_description, ) async_setup_entry_rest( hass, config_entry, async_add_entities, REST_SENSORS, RestSensor @@ -1075,10 +1060,9 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, RestoreSensor): attribute: str, description: BlockSensorDescription, entry: RegistryEntry | None = None, - sensors: Mapping[tuple[str, str], BlockSensorDescription] | None = None, ) -> None: """Initialize the sleeping sensor.""" - super().__init__(coordinator, block, attribute, description, entry, sensors) + super().__init__(coordinator, block, attribute, description, entry) self.restored_data: SensorExtraStoredData | None = None async def async_added_to_hass(self) -> None: diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 380f4f5999e..86c6356191b 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -6,9 +6,15 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, @@ -153,7 +159,11 @@ async def test_block_restored_sleeping_sensor( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == "20.4" + state = hass.states.get(entity_id) + assert state + assert state.state == "20.4" + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) @@ -237,7 +247,9 @@ async def test_block_not_matched_restored_sleeping_sensor( assert hass.states.get(entity_id).state == "20.4" # Make device online - monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "type", "other_type") + monkeypatch.setattr( + mock_block_device.blocks[SENSOR_BLOCK_ID], "description", "other_desc" + ) monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_update() await hass.async_block_till_done() From 70492a80cc0a1eb9a077214af72de482025d5b49 Mon Sep 17 00:00:00 2001 From: Cody C <50791984+codyc1515@users.noreply.github.com> Date: Mon, 15 Jan 2024 21:41:44 +1300 Subject: [PATCH 093/133] Fix malformed user input error on MJPEG config flow (#108058) --- homeassistant/components/mjpeg/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mjpeg/config_flow.py b/homeassistant/components/mjpeg/config_flow.py index 61c80bcde38..024766f4c63 100644 --- a/homeassistant/components/mjpeg/config_flow.py +++ b/homeassistant/components/mjpeg/config_flow.py @@ -54,7 +54,7 @@ def async_get_schema( if show_name: schema = { - vol.Optional(CONF_NAME, default=defaults.get(CONF_NAME)): str, + vol.Required(CONF_NAME, default=defaults.get(CONF_NAME)): str, **schema, } From 7fee6c5279bf1dc04e1e2d4cf57ec157f7f1c33c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 15 Jan 2024 11:08:38 +0100 Subject: [PATCH 094/133] Fix turning on the light with a specific color (#108080) --- homeassistant/components/matter/light.py | 12 ++++++++++++ tests/components/matter/test_light.py | 12 +++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 52a6b4162fe..43c47046162 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -89,6 +89,10 @@ class MatterLight(MatterEntity, LightEntity): colorY=int(matter_xy[1]), # It's required in TLV. We don't implement transition time yet. transitionTime=0, + # allow setting the color while the light is off, + # by setting the optionsMask to 1 (=ExecuteIfOff) + optionsMask=1, + optionsOverride=1, ) ) @@ -103,6 +107,10 @@ class MatterLight(MatterEntity, LightEntity): saturation=int(matter_hs[1]), # It's required in TLV. We don't implement transition time yet. transitionTime=0, + # allow setting the color while the light is off, + # by setting the optionsMask to 1 (=ExecuteIfOff) + optionsMask=1, + optionsOverride=1, ) ) @@ -114,6 +122,10 @@ class MatterLight(MatterEntity, LightEntity): colorTemperatureMireds=color_temp, # It's required in TLV. We don't implement transition time yet. transitionTime=0, + # allow setting the color while the light is off, + # by setting the optionsMask to 1 (=ExecuteIfOff) + optionsMask=1, + optionsOverride=1, ) ) diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index 78ffa477b33..fb988d26a1c 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -202,6 +202,8 @@ async def test_color_temperature_light( command=clusters.ColorControl.Commands.MoveToColorTemperature( colorTemperatureMireds=300, transitionTime=0, + optionsMask=1, + optionsOverride=1, ), ), call( @@ -278,7 +280,11 @@ async def test_extended_color_light( node_id=light_node.node_id, endpoint_id=1, command=clusters.ColorControl.Commands.MoveToColor( - colorX=0.5 * 65536, colorY=0.5 * 65536, transitionTime=0 + colorX=0.5 * 65536, + colorY=0.5 * 65536, + transitionTime=0, + optionsMask=1, + optionsOverride=1, ), ), call( @@ -311,8 +317,8 @@ async def test_extended_color_light( hue=167, saturation=254, transitionTime=0, - optionsMask=0, - optionsOverride=0, + optionsMask=1, + optionsOverride=1, ), ), call( From 497d2f5677e70f95c041f24f1c06c88d31853a3e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 15 Jan 2024 12:10:17 +0100 Subject: [PATCH 095/133] Bump Jinja2 to 3.1.3 (#108082) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5eebfa4181b..d9df52ea800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ home-assistant-intents==2024.1.2 httpx==0.26.0 ifaddr==0.2.0 janus==1.0.0 -Jinja2==3.1.2 +Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 orjson==3.9.9 diff --git a/pyproject.toml b/pyproject.toml index 00d8b70f492..33965196ee4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ "httpx==0.26.0", "home-assistant-bluetooth==1.12.0", "ifaddr==0.2.0", - "Jinja2==3.1.2", + "Jinja2==3.1.3", "lru-dict==1.3.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. diff --git a/requirements.txt b/requirements.txt index f86893bce46..e1878a33584 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ ciso8601==2.3.0 httpx==0.26.0 home-assistant-bluetooth==1.12.0 ifaddr==0.2.0 -Jinja2==3.1.2 +Jinja2==3.1.3 lru-dict==1.3.0 PyJWT==2.8.0 cryptography==41.0.7 From 99f9f0205a1d31fccc998eae09ec4f116d0321c7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 15 Jan 2024 20:33:30 +0100 Subject: [PATCH 096/133] Use compat for supported features in media player (#108102) --- homeassistant/components/media_player/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 113048421e1..673f0a44374 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1295,7 +1295,7 @@ async def websocket_browse_media( connection.send_error(msg["id"], "entity_not_found", "Entity not found") return - if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features: + if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat: connection.send_message( websocket_api.error_message( msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media" From 304b950f1a2bd9dfbbd1b813e5c90af2f96f8e41 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:36:28 -0500 Subject: [PATCH 097/133] Speed up ZHA initialization and improve startup responsiveness (#108103) * Limit concurrency of startup traffic to allow for interactive usage * Drop `retryable_req`, we already have request retrying * Oops, `min` -> `max` * Add a comment describing why `async_initialize` is not concurrent * Fix existing unit tests * Break out fetching mains state into its own function to unit test --- .../zha/core/cluster_handlers/__init__.py | 3 +- homeassistant/components/zha/core/device.py | 17 ++-- homeassistant/components/zha/core/endpoint.py | 22 ++++- homeassistant/components/zha/core/gateway.py | 49 ++++++++--- homeassistant/components/zha/core/helpers.py | 47 ----------- tests/components/zha/conftest.py | 2 +- tests/components/zha/test_gateway.py | 82 ++++++++++++++++++- 7 files changed, 149 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index 2b78c90aa19..00439343e81 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -42,7 +42,7 @@ from ..const import ( ZHA_CLUSTER_HANDLER_MSG_DATA, ZHA_CLUSTER_HANDLER_READS_PER_REQ, ) -from ..helpers import LogMixin, retryable_req, safe_read +from ..helpers import LogMixin, safe_read if TYPE_CHECKING: from ..endpoint import Endpoint @@ -362,7 +362,6 @@ class ClusterHandler(LogMixin): self.debug("skipping cluster handler configuration") self._status = ClusterHandlerStatus.CONFIGURED - @retryable_req(delays=(1, 1, 3)) async def async_initialize(self, from_cache: bool) -> None: """Initialize cluster handler.""" if not from_cache and self._endpoint.device.skip_configuration: diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1a3d3a2da1f..468e89fbbf0 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -592,12 +592,17 @@ class ZHADevice(LogMixin): self.debug("started initialization") await self._zdo_handler.async_initialize(from_cache) self._zdo_handler.debug("'async_initialize' stage succeeded") - await asyncio.gather( - *( - endpoint.async_initialize(from_cache) - for endpoint in self._endpoints.values() - ) - ) + + # We intentionally do not use `gather` here! This is so that if, for example, + # three `device.async_initialize()`s are spawned, only three concurrent requests + # will ever be in flight at once. Startup concurrency is managed at the device + # level. + for endpoint in self._endpoints.values(): + try: + await endpoint.async_initialize(from_cache) + except Exception: # pylint: disable=broad-exception-caught + self.debug("Failed to initialize endpoint", exc_info=True) + self.debug("power source: %s", self.power_source) self.status = DeviceStatus.INITIALIZED self.debug("completed initialization") diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index 04c253128ee..4dbfccf6f25 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Awaitable, Callable +import functools import logging from typing import TYPE_CHECKING, Any, Final, TypeVar @@ -11,6 +12,7 @@ from zigpy.typing import EndpointType as ZigpyEndpointType from homeassistant.const import Platform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util.async_ import gather_with_limited_concurrency from . import const, discovery, registries from .cluster_handlers import ClusterHandler @@ -169,20 +171,32 @@ class Endpoint: async def async_initialize(self, from_cache: bool = False) -> None: """Initialize claimed cluster handlers.""" - await self._execute_handler_tasks("async_initialize", from_cache) + await self._execute_handler_tasks( + "async_initialize", from_cache, max_concurrency=1 + ) async def async_configure(self) -> None: """Configure claimed cluster handlers.""" await self._execute_handler_tasks("async_configure") - async def _execute_handler_tasks(self, func_name: str, *args: Any) -> None: + async def _execute_handler_tasks( + self, func_name: str, *args: Any, max_concurrency: int | None = None + ) -> None: """Add a throttled cluster handler task and swallow exceptions.""" cluster_handlers = [ *self.claimed_cluster_handlers.values(), *self.client_cluster_handlers.values(), ] tasks = [getattr(ch, func_name)(*args) for ch in cluster_handlers] - results = await asyncio.gather(*tasks, return_exceptions=True) + + gather: Callable[..., Awaitable] + + if max_concurrency is None: + gather = asyncio.gather + else: + gather = functools.partial(gather_with_limited_concurrency, max_concurrency) + + results = await gather(*tasks, return_exceptions=True) for cluster_handler, outcome in zip(cluster_handlers, results): if isinstance(outcome, Exception): cluster_handler.warning( diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 3efdc77934a..cca8aa93e99 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -11,7 +11,7 @@ import itertools import logging import re import time -from typing import TYPE_CHECKING, Any, NamedTuple, Self +from typing import TYPE_CHECKING, Any, NamedTuple, Self, cast from zigpy.application import ControllerApplication from zigpy.config import ( @@ -36,6 +36,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType +from homeassistant.util.async_ import gather_with_limited_concurrency from . import discovery from .const import ( @@ -292,6 +293,39 @@ class ZHAGateway: # entity registry tied to the devices discovery.GROUP_PROBE.discover_group_entities(zha_group) + @property + def radio_concurrency(self) -> int: + """Maximum configured radio concurrency.""" + return self.application_controller._concurrent_requests_semaphore.max_value # pylint: disable=protected-access + + async def async_fetch_updated_state_mains(self) -> None: + """Fetch updated state for mains powered devices.""" + _LOGGER.debug("Fetching current state for mains powered devices") + + now = time.time() + + # Only delay startup to poll mains-powered devices that are online + online_devices = [ + dev + for dev in self.devices.values() + if dev.is_mains_powered + and dev.last_seen is not None + and (now - dev.last_seen) < dev.consider_unavailable_time + ] + + # Prioritize devices that have recently been contacted + online_devices.sort(key=lambda dev: cast(float, dev.last_seen), reverse=True) + + # Make sure that we always leave slots for non-startup requests + max_poll_concurrency = max(1, self.radio_concurrency - 4) + + await gather_with_limited_concurrency( + max_poll_concurrency, + *(dev.async_initialize(from_cache=False) for dev in online_devices), + ) + + _LOGGER.debug("completed fetching current state for mains powered devices") + async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" @@ -302,17 +336,8 @@ class ZHAGateway: async def fetch_updated_state() -> None: """Fetch updated state for mains powered devices.""" - _LOGGER.debug("Fetching current state for mains powered devices") - await asyncio.gather( - *( - dev.async_initialize(from_cache=False) - for dev in self.devices.values() - if dev.is_mains_powered - ) - ) - _LOGGER.debug( - "completed fetching current state for mains powered devices - allowing polled requests" - ) + await self.async_fetch_updated_state_mains() + _LOGGER.debug("Allowing polled requests") self.hass.data[DATA_ZHA].allow_polling = True # background the fetching of state for mains powered devices diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index bb87cb2cf58..72d09d239e1 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -5,17 +5,13 @@ https://home-assistant.io/integrations/zha/ """ from __future__ import annotations -import asyncio import binascii import collections from collections.abc import Callable, Iterator import dataclasses from dataclasses import dataclass import enum -import functools -import itertools import logging -from random import uniform import re from typing import TYPE_CHECKING, Any, TypeVar @@ -318,49 +314,6 @@ class LogMixin: return self.log(logging.ERROR, msg, *args, **kwargs) -def retryable_req( - delays=(1, 5, 10, 15, 30, 60, 120, 180, 360, 600, 900, 1800), raise_=False -): - """Make a method with ZCL requests retryable. - - This adds delays keyword argument to function. - len(delays) is number of tries. - raise_ if the final attempt should raise the exception. - """ - - def decorator(func): - @functools.wraps(func) - async def wrapper(cluster_handler, *args, **kwargs): - exceptions = (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) - try_count, errors = 1, [] - for delay in itertools.chain(delays, [None]): - try: - return await func(cluster_handler, *args, **kwargs) - except exceptions as ex: - errors.append(ex) - if delay: - delay = uniform(delay * 0.75, delay * 1.25) - cluster_handler.debug( - "%s: retryable request #%d failed: %s. Retrying in %ss", - func.__name__, - try_count, - ex, - round(delay, 1), - ) - try_count += 1 - await asyncio.sleep(delay) - else: - cluster_handler.warning( - "%s: all attempts have failed: %s", func.__name__, errors - ) - if raise_: - raise - - return wrapper - - return decorator - - def convert_install_code(value: str) -> bytes: """Convert string to install code bytes and validate length.""" diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 55405d0a51c..a30c6f35052 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -135,7 +135,7 @@ def _wrap_mock_instance(obj: Any) -> MagicMock: real_attr = getattr(obj, attr_name) mock_attr = getattr(mock, attr_name) - if callable(real_attr): + if callable(real_attr) and not hasattr(real_attr, "__aenter__"): mock_attr.side_effect = real_attr else: setattr(mock, attr_name, real_attr) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 9c3cf7aa2f8..f19ed9bd4a9 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,12 +1,14 @@ """Test ZHA Gateway.""" import asyncio -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, PropertyMock, patch import pytest from zigpy.application import ControllerApplication import zigpy.profiles.zha as zha +import zigpy.types import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting +import zigpy.zdo.types from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.group import GroupMember @@ -321,3 +323,81 @@ async def test_single_reload_on_multiple_connection_loss( assert len(mock_reload.mock_calls) == 1 await hass.async_block_till_done() + + +@pytest.mark.parametrize("radio_concurrency", [1, 2, 8]) +async def test_startup_concurrency_limit( + radio_concurrency: int, + hass: HomeAssistant, + zigpy_app_controller: ControllerApplication, + config_entry: MockConfigEntry, + zigpy_device_mock, +): + """Test ZHA gateway limits concurrency on startup.""" + config_entry.add_to_hass(hass) + zha_gateway = ZHAGateway(hass, {}, config_entry) + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): + await zha_gateway.async_initialize() + + for i in range(50): + zigpy_dev = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ieee=f"11:22:33:44:{i:08x}", + nwk=0x1234 + i, + ) + zigpy_dev.node_desc.mac_capability_flags |= ( + zigpy.zdo.types.NodeDescriptor.MACCapabilityFlags.MainsPowered + ) + + zha_gateway._async_get_or_create_device(zigpy_dev, restored=True) + + # Keep track of request concurrency during initialization + current_concurrency = 0 + concurrencies = [] + + async def mock_send_packet(*args, **kwargs): + nonlocal current_concurrency + + current_concurrency += 1 + concurrencies.append(current_concurrency) + + await asyncio.sleep(0.001) + + current_concurrency -= 1 + concurrencies.append(current_concurrency) + + type(zha_gateway).radio_concurrency = PropertyMock(return_value=radio_concurrency) + assert zha_gateway.radio_concurrency == radio_concurrency + + with patch( + "homeassistant.components.zha.core.device.ZHADevice.async_initialize", + side_effect=mock_send_packet, + ): + await zha_gateway.async_fetch_updated_state_mains() + + await zha_gateway.shutdown() + + # Make sure concurrency was always limited + assert current_concurrency == 0 + assert min(concurrencies) == 0 + + if radio_concurrency > 1: + assert 1 <= max(concurrencies) < zha_gateway.radio_concurrency + else: + assert 1 == max(concurrencies) == zha_gateway.radio_concurrency From 7fb2a8a3cd5aea4e3f4210dcb633e80829365cda Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 16 Jan 2024 05:10:09 -0700 Subject: [PATCH 098/133] Bump `aioridwell` to 2024.01.0 (#108126) --- homeassistant/components/ridwell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index 72a29182169..c02cc012e0f 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aioridwell"], - "requirements": ["aioridwell==2023.07.0"] + "requirements": ["aioridwell==2024.01.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8e24dab290a..78fcc6f5abb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ aioqsw==0.3.5 aiorecollect==2023.09.0 # homeassistant.components.ridwell -aioridwell==2023.07.0 +aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed aioruckus==0.34 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7c5e7b0672..3841e78796e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ aioqsw==0.3.5 aiorecollect==2023.09.0 # homeassistant.components.ridwell -aioridwell==2023.07.0 +aioridwell==2024.01.0 # homeassistant.components.ruckus_unleashed aioruckus==0.34 From 5521ab0b35c90121bc4b9d84e25cf988829c6927 Mon Sep 17 00:00:00 2001 From: cnico Date: Tue, 16 Jan 2024 06:56:54 +0100 Subject: [PATCH 099/133] Bump flipr-api to 1.5.1 (#108130) Flipr-api version update for resolution of issue https://github.com/home-assistant/core/issues/105778 --- homeassistant/components/flipr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json index 73a0b3edb26..898cd640349 100644 --- a/homeassistant/components/flipr/manifest.json +++ b/homeassistant/components/flipr/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/flipr", "iot_class": "cloud_polling", "loggers": ["flipr_api"], - "requirements": ["flipr-api==1.5.0"] + "requirements": ["flipr-api==1.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 78fcc6f5abb..d163f46d7d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -840,7 +840,7 @@ fjaraskupan==2.2.0 flexit_bacnet==2.1.0 # homeassistant.components.flipr -flipr-api==1.5.0 +flipr-api==1.5.1 # homeassistant.components.flux_led flux-led==1.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3841e78796e..d3764c74c58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -675,7 +675,7 @@ fjaraskupan==2.2.0 flexit_bacnet==2.1.0 # homeassistant.components.flipr -flipr-api==1.5.0 +flipr-api==1.5.1 # homeassistant.components.flux_led flux-led==1.0.4 From d2feee86b7e7c231c7b1ebac13b03e8bc171fe1a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 16 Jan 2024 08:05:35 -0800 Subject: [PATCH 100/133] Add debugging to assist in debugging already configured error (#108134) --- homeassistant/components/google/config_flow.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 33d913fe8f1..ed6d36d8ec7 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -210,6 +210,12 @@ class OAuth2FlowHandler( _LOGGER.error("Error reading primary calendar: %s", err) return self.async_abort(reason="cannot_connect") await self.async_set_unique_id(primary_calendar.id) + + if found := self.hass.config_entries.async_entry_for_domain_unique_id( + self.handler, primary_calendar.id + ): + _LOGGER.debug("Found existing '%s' entry: %s", primary_calendar.id, found) + self._abort_if_unique_id_configured() return self.async_create_entry( title=primary_calendar.id, From 901b7b62782f13a301b1f973d94e2e2e1237a09d Mon Sep 17 00:00:00 2001 From: John Allen Date: Wed, 17 Jan 2024 15:06:11 -0500 Subject: [PATCH 101/133] Send target temp to Shelly TRV in F when needed (#108188) --- homeassistant/components/shelly/climate.py | 15 ++++++++++++++ tests/components/shelly/test_climate.py | 23 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 7cc0027bbaf..64129131d0a 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -316,6 +316,21 @@ class BlockSleepingClimate( """Set new target temperature.""" if (current_temp := kwargs.get(ATTR_TEMPERATURE)) is None: return + + # Shelly TRV accepts target_t in Fahrenheit or Celsius, but you must + # send the units that the device expects + if self.block is not None and self.block.channel is not None: + therm = self.coordinator.device.settings["thermostats"][ + int(self.block.channel) + ] + LOGGER.debug("Themostat settings: %s", therm) + if therm.get("target_t", {}).get("units", "C") == "F": + current_temp = TemperatureConverter.convert( + cast(float, current_temp), + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + ) + await self.set_state_full_path(target_t_enabled=1, target_t=f"{current_temp}") async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 980981de754..28235325af4 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -146,6 +146,29 @@ async def test_climate_set_temperature( mock_block_device.http_request.assert_called_once_with( "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "23.0"} ) + mock_block_device.http_request.reset_mock() + + # Test conversion from C to F + monkeypatch.setattr( + mock_block_device, + "settings", + { + "thermostats": [ + {"target_t": {"units": "F"}}, + ] + }, + ) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": "68.0"} + ) async def test_climate_set_preset_mode( From da5d4fe4ae15dcdf873f7daa91d2c509e824c145 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 18 Jan 2024 03:34:18 +0100 Subject: [PATCH 102/133] Use cache update for WIFI blinds (#108224) --- homeassistant/components/motion_blinds/coordinator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motion_blinds/coordinator.py b/homeassistant/components/motion_blinds/coordinator.py index cfc7d319b38..e8dc5494f25 100644 --- a/homeassistant/components/motion_blinds/coordinator.py +++ b/homeassistant/components/motion_blinds/coordinator.py @@ -5,7 +5,7 @@ import logging from socket import timeout from typing import Any -from motionblinds import ParseException +from motionblinds import DEVICE_TYPES_WIFI, ParseException from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -59,7 +59,9 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): def update_blind(self, blind): """Fetch data from a blind.""" try: - if self._wait_for_push: + if blind.device_type in DEVICE_TYPES_WIFI: + blind.Update_from_cache() + elif self._wait_for_push: blind.Update() else: blind.Update_trigger() From 6ecb562a800bc7ebbb5593e3fafbf3aa72bff402 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Jan 2024 22:28:15 +0100 Subject: [PATCH 103/133] Bump reolink_aio to 0.8.7 (#108248) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 5670aea87ad..40e85b9680b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.8.6"] + "requirements": ["reolink-aio==0.8.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index d163f46d7d6..80db9c8f26e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2376,7 +2376,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.6 +reolink-aio==0.8.7 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3764c74c58..f7579e24a8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1798,7 +1798,7 @@ renault-api==0.2.1 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.8.6 +reolink-aio==0.8.7 # homeassistant.components.rflink rflink==0.0.65 From 59e12ad0c16c3e29378b6a812dd0a9df616bb860 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 17 Jan 2024 20:54:13 +0100 Subject: [PATCH 104/133] Bump PyTado to 0.17.4 (#108255) Bump to 17.4 --- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index bae637f3180..79fe565261b 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.17.3"] + "requirements": ["python-tado==0.17.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 80db9c8f26e..c1240cd9148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2241,7 +2241,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.17.3 +python-tado==0.17.4 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7579e24a8a..55016596bfb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1696,7 +1696,7 @@ python-smarttub==0.0.36 python-songpal==0.16 # homeassistant.components.tado -python-tado==0.17.3 +python-tado==0.17.4 # homeassistant.components.telegram_bot python-telegram-bot==13.1 From 005af2eb4c60fd5236ba64506cf4220eeb236b9f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 18 Jan 2024 03:33:31 +0100 Subject: [PATCH 105/133] Bump aiounifi to v69 to improve websocket logging (#108265) --- homeassistant/components/unifi/controller.py | 6 +++++- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index a941e836ae2..833d2001980 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -7,6 +7,7 @@ import ssl from types import MappingProxyType from typing import Any, Literal +import aiohttp from aiohttp import CookieJar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -374,7 +375,10 @@ class UniFiController: async def _websocket_runner() -> None: """Start websocket.""" - await self.api.start_websocket() + try: + await self.api.start_websocket() + except (aiohttp.ClientConnectorError, aiounifi.WebsocketError): + LOGGER.error("Websocket disconnected") self.available = False async_dispatcher_send(self.hass, self.signal_reachable) self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 4a43a65d5bb..90b4421f164 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==68"], + "requirements": ["aiounifi==69"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index c1240cd9148..90f0a24453e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -377,7 +377,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==68 +aiounifi==69 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55016596bfb..a7e2fe393cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==68 +aiounifi==69 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From e2a6097141ff337bd093815d411ee60143b18ced Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:34:10 -0500 Subject: [PATCH 106/133] Bump ZHA dependency zigpy to 0.60.6 (#108266) Bump zigpy to 0.60.6 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 06ebfaaa6a0..de429b299c0 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -26,7 +26,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.109", "zigpy-deconz==0.22.4", - "zigpy==0.60.4", + "zigpy==0.60.6", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/requirements_all.txt b/requirements_all.txt index 90f0a24453e..27bd47f3739 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2887,7 +2887,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.4 +zigpy==0.60.6 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7e2fe393cc..df3eceb14c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2186,7 +2186,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.4 +zigpy==0.60.6 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 From 916e5de9d120bda6beb43ec24604d1c493874c51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Jan 2024 15:44:20 +0100 Subject: [PATCH 107/133] Bump version to 2024.1.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 9ddb002c261..45f48c4e89e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 33965196ee4..b8b63cf011c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.3" +version = "2024.1.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 43f1c0927fcd169472a51e285ba8335eb28f62a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Jan 2024 19:26:37 +0100 Subject: [PATCH 108/133] Revert "Add debugging to assist in debugging already configured error (#108134)" This reverts commit d2feee86b7e7c231c7b1ebac13b03e8bc171fe1a. --- homeassistant/components/google/config_flow.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index ed6d36d8ec7..33d913fe8f1 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -210,12 +210,6 @@ class OAuth2FlowHandler( _LOGGER.error("Error reading primary calendar: %s", err) return self.async_abort(reason="cannot_connect") await self.async_set_unique_id(primary_calendar.id) - - if found := self.hass.config_entries.async_entry_for_domain_unique_id( - self.handler, primary_calendar.id - ): - _LOGGER.debug("Found existing '%s' entry: %s", primary_calendar.id, found) - self._abort_if_unique_id_configured() return self.async_create_entry( title=primary_calendar.id, From fb700cc84799b0b07f892ffffb0ace68282b3966 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 20 Jan 2024 03:48:08 +0100 Subject: [PATCH 109/133] Bump async-upnp-client to 0.38.1 (#108382) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index e2a07a3e351..ab5d035dd54 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.0", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 6173c9a3843..d4a74725467 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.38.0"], + "requirements": ["async-upnp-client==0.38.1"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 2b388cf706a..780d47e4743 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.38.0" + "async-upnp-client==0.38.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index e6f18190c0b..8afed8b4fd1 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.38.0"] + "requirements": ["async-upnp-client==0.38.1"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 4b6badb0d3c..8ce32158016 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.0", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.1", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 4881d8c576d..f2a11aaf1fe 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.0"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.1"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d9df52ea800..5da0d79d48f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiohttp-zlib-ng==0.1.3 aiohttp==3.9.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.38.0 +async-upnp-client==0.38.1 atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==23.11.0 diff --git a/requirements_all.txt b/requirements_all.txt index 27bd47f3739..cff90b6a6cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -469,7 +469,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.0 +async-upnp-client==0.38.1 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df3eceb14c6..8877471660a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -421,7 +421,7 @@ arcam-fmj==1.4.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.0 +async-upnp-client==0.38.1 # homeassistant.components.sleepiq asyncsleepiq==1.4.1 From ea8fc64dd690e8163eb4d12dae899395c491575e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Jan 2024 15:12:32 +0100 Subject: [PATCH 110/133] Fix empty files included by !include_dir_named (#108489) Co-authored-by: Joost Lekkerkerker --- homeassistant/util/yaml/loader.py | 7 ++++++- tests/util/yaml/test_init.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 5da5a84cc48..5d66ea23dcb 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -355,7 +355,12 @@ def _include_dir_named_yaml(loader: LoaderType, node: yaml.nodes.Node) -> NodeDi filename = os.path.splitext(os.path.basename(fname))[0] if os.path.basename(fname) == SECRET_YAML: continue - mapping[filename] = load_yaml(fname, loader.secrets) + loaded_yaml = load_yaml(fname, loader.secrets) + if loaded_yaml is None: + # Special case, an empty file included by !include_dir_named is treated + # as an empty dictionary + loaded_yaml = NodeDictClass() + mapping[filename] = loaded_yaml return _add_reference(mapping, loader, node) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 30637fe2785..93c8ed50498 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -193,7 +193,7 @@ def test_include_dir_list_recursive( ), ( {"/test/first.yaml": "1", "/test/second.yaml": None}, - {"first": 1, "second": None}, + {"first": 1, "second": {}}, ), ], ) From 71370f65d5bf4b53e5a1d7641e8eb8001bf32f26 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Jan 2024 15:51:05 +0100 Subject: [PATCH 111/133] Bump version to 2024.1.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 45f48c4e89e..f9d250c6732 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b8b63cf011c..f678d14d214 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.4" +version = "2024.1.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4c4dc6a0860074b5e09d13279c556eef7ad16477 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Jan 2024 18:08:35 +0100 Subject: [PATCH 112/133] Pin pandas to 2.1.4 (#108509) --- 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 5da0d79d48f..821b6fdf141 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -189,3 +189,6 @@ lxml==4.9.4 # dacite: Ensure we have a version that is able to handle type unions for # Roborock, NAM, Brother, and GIOS. dacite>=1.7.0 + +# Musle wheels for pandas 2.2.0 cannot be build for any architecture. +pandas==2.1.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7f652b14302..15bcbf1b7f3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -182,6 +182,9 @@ lxml==4.9.4 # dacite: Ensure we have a version that is able to handle type unions for # Roborock, NAM, Brother, and GIOS. dacite>=1.7.0 + +# Musle wheels for pandas 2.2.0 cannot be build for any architecture. +pandas==2.1.4 """ GENERATED_MESSAGE = ( From 8c6547f1b64f4a3d9f10090b97383353c9367892 Mon Sep 17 00:00:00 2001 From: vexofp Date: Tue, 2 Jan 2024 05:59:40 -0500 Subject: [PATCH 113/133] Pass default SSLContext instances to Octoprint custom HTTP sessions (#105351) --- homeassistant/components/octoprint/__init__.py | 5 ++++- homeassistant/components/octoprint/config_flow.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 5fd2182ca00..50ba6c964f3 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -26,6 +26,7 @@ from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify as util_slugify +from homeassistant.util.ssl import get_default_context, get_default_no_verify_context from .const import DOMAIN from .coordinator import OctoprintDataUpdateCoordinator @@ -159,7 +160,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: connector = aiohttp.TCPConnector( force_close=True, - ssl=False if not entry.data[CONF_VERIFY_SSL] else None, + ssl=get_default_no_verify_context() + if not entry.data[CONF_VERIFY_SSL] + else get_default_context(), ) session = aiohttp.ClientSession(connector=connector) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 09ac53ecf5b..696898400bf 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv +from homeassistant.util.ssl import get_default_context, get_default_no_verify_context from .const import DOMAIN @@ -264,7 +265,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): connector = aiohttp.TCPConnector( force_close=True, - ssl=False if not verify_ssl else None, + ssl=get_default_no_verify_context() + if not verify_ssl + else get_default_context(), ) session = aiohttp.ClientSession(connector=connector) self._sessions.append(session) From 18f663d4980cf2d07e81a247034fb9bbc0e2d0b4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 23 Jan 2024 01:50:00 -0800 Subject: [PATCH 114/133] Reduce overhead for google calendar state updates (#108133) --- homeassistant/components/google/calendar.py | 30 +++++++++++++++++-- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 3e34a7234a4..88f59ff44f7 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime, timedelta +import itertools import logging from typing import Any, cast @@ -18,6 +19,7 @@ from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.timeline import Timeline +from ical.iter import SortableItemValue from homeassistant.components.calendar import ( CREATE_EVENT_SCHEMA, @@ -76,6 +78,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) +# Maximum number of upcoming events to consider for state changes between +# coordinator updates. +MAX_UPCOMING_EVENTS = 20 # Avoid syncing super old data on initial syncs. Note that old but active # recurring events are still included. @@ -244,6 +249,22 @@ async def async_setup_entry( ) +def _truncate_timeline(timeline: Timeline, max_events: int) -> Timeline: + """Truncate the timeline to a maximum number of events. + + This is used to avoid repeated expansion of recurring events during + state machine updates. + """ + upcoming = timeline.active_after(dt_util.now()) + truncated = list(itertools.islice(upcoming, max_events)) + return Timeline( + [ + SortableItemValue(event.timespan_of(dt_util.DEFAULT_TIME_ZONE), event) + for event in truncated + ] + ) + + class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): """Coordinator for calendar RPC calls that use an efficient sync.""" @@ -263,6 +284,7 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): update_interval=MIN_TIME_BETWEEN_UPDATES, ) self.sync = sync + self._upcoming_timeline: Timeline | None = None async def _async_update_data(self) -> Timeline: """Fetch data from API endpoint.""" @@ -271,9 +293,11 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): except ApiException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - return await self.sync.store_service.async_get_timeline( + timeline = await self.sync.store_service.async_get_timeline( dt_util.DEFAULT_TIME_ZONE ) + self._upcoming_timeline = _truncate_timeline(timeline, MAX_UPCOMING_EVENTS) + return timeline async def async_get_events( self, start_date: datetime, end_date: datetime @@ -291,8 +315,8 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): @property def upcoming(self) -> Iterable[Event] | None: """Return upcoming events if any.""" - if self.data: - return self.data.active_after(dt_util.now()) + if self._upcoming_timeline: + return self._upcoming_timeline.active_after(dt_util.now()) return None diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 27e462a380e..d0705f9382a 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3"] + "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==6.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index cff90b6a6cd..bbe4a6fd90c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1076,6 +1076,7 @@ ibeacon-ble==1.0.1 # homeassistant.components.watson_iot ibmiotf==0.3.4 +# homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo ical==6.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8877471660a..a2bbeea023a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -860,6 +860,7 @@ iaqualink==0.5.0 # homeassistant.components.ibeacon ibeacon-ble==1.0.1 +# homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo ical==6.1.1 From 3a510f84a737a5d12a95f4b5b53b459eb75b7c13 Mon Sep 17 00:00:00 2001 From: jmwaldrip Date: Mon, 22 Jan 2024 12:22:54 -0800 Subject: [PATCH 115/133] Fix SleepIQ setting FootWarmer timer (#108433) * Fixing foot warmer timer bug * Fixing bug where temperature wasnt assigned to number entity causing tests to fail --- homeassistant/components/sleepiq/number.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index 520e11bb331..4f90ef7dbdc 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -5,7 +5,13 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any, cast -from asyncsleepiq import SleepIQActuator, SleepIQBed, SleepIQFootWarmer, SleepIQSleeper +from asyncsleepiq import ( + FootWarmingTemps, + SleepIQActuator, + SleepIQBed, + SleepIQFootWarmer, + SleepIQSleeper, +) from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry @@ -79,6 +85,10 @@ def _get_sleeper_unique_id(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str: async def _async_set_foot_warmer_time( foot_warmer: SleepIQFootWarmer, time: int ) -> None: + temperature = FootWarmingTemps(foot_warmer.temperature) + if temperature != FootWarmingTemps.OFF: + await foot_warmer.turn_on(temperature, time) + foot_warmer.timer = time From 279f7264e74c67b584b429dc363ea831718e6062 Mon Sep 17 00:00:00 2001 From: Florian Kisser Date: Sun, 21 Jan 2024 02:37:13 +0100 Subject: [PATCH 116/133] Fix zha illuminance measured value mapping (#108547) --- homeassistant/components/zha/sensor.py | 6 +++++- tests/components/zha/test_sensor.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index ea5d09dd6f4..8bf8ca96d77 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -481,8 +481,12 @@ class Illuminance(Sensor): _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = LIGHT_LUX - def formatter(self, value: int) -> int: + def formatter(self, value: int) -> int | None: """Convert illumination data.""" + if value == 0: + return 0 + if value == 0xFFFF: + return None return round(pow(10, ((value - 1) / 10000))) diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index d9a61b12357..0d3035f9717 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -136,6 +136,12 @@ async def async_test_illuminance(hass, cluster, entity_id): await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20}) assert_state(hass, entity_id, "1", LIGHT_LUX) + await send_attributes_report(hass, cluster, {1: 0, 0: 0, 2: 20}) + assert_state(hass, entity_id, "0", LIGHT_LUX) + + await send_attributes_report(hass, cluster, {1: 0, 0: 0xFFFF, 2: 20}) + assert_state(hass, entity_id, "unknown", LIGHT_LUX) + async def async_test_metering(hass, cluster, entity_id): """Test Smart Energy metering sensor.""" From 4db6f7ce59e8372572d1b495ccc6b2f492f61fdd Mon Sep 17 00:00:00 2001 From: Matrix Date: Sun, 28 Jan 2024 13:38:42 +0800 Subject: [PATCH 117/133] Bump yolink-api to 0.3.6 fix aiomqtt breaking changes (#108555) * bump yolink-api to 0.3.5 * bump yolink-api to 0.3.6 --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index a42687a3551..6fd62ce571c 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.3.4"] + "requirements": ["yolink-api==0.3.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index bbe4a6fd90c..beed6c6eb01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2843,7 +2843,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.3.4 +yolink-api==0.3.6 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2bbeea023a..1ef4e6809a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2151,7 +2151,7 @@ yalexs==1.10.0 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.3.4 +yolink-api==0.3.6 # homeassistant.components.youless youless-api==1.0.1 From bbe80c60491aca4971e4355235380ac62d3d5b1a Mon Sep 17 00:00:00 2001 From: Michal Ziemski Date: Tue, 23 Jan 2024 15:14:41 +0100 Subject: [PATCH 118/133] Update openerz-api to 0.3.0 (#108575) --- homeassistant/components/openerz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openerz/manifest.json b/homeassistant/components/openerz/manifest.json index 181e0bd870a..c7a5a202568 100644 --- a/homeassistant/components/openerz/manifest.json +++ b/homeassistant/components/openerz/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/openerz", "iot_class": "cloud_polling", "loggers": ["openerz_api"], - "requirements": ["openerz-api==0.2.0"] + "requirements": ["openerz-api==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index beed6c6eb01..062423be67e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1414,7 +1414,7 @@ openai==1.3.8 # opencv-python-headless==4.6.0.66 # homeassistant.components.openerz -openerz-api==0.2.0 +openerz-api==0.3.0 # homeassistant.components.openevse openevsewifi==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ef4e6809a9..acc7b389699 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1111,7 +1111,7 @@ open-meteo==0.3.1 openai==1.3.8 # homeassistant.components.openerz -openerz-api==0.2.0 +openerz-api==0.3.0 # homeassistant.components.openhome openhomedevice==2.2.0 From b17b4c3350518661f9278ecfffb148b825fe8edf Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 22 Jan 2024 13:51:17 +0100 Subject: [PATCH 119/133] Bump aiovodafone to 0.5.4 (#108592) --- homeassistant/components/vodafone_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index 20ea4db057e..ced871b7616 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vodafone_station", "iot_class": "local_polling", "loggers": ["aiovodafone"], - "requirements": ["aiovodafone==0.4.3"] + "requirements": ["aiovodafone==0.5.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 062423be67e..3d253b3a953 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ aiounifi==69 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.4.3 +aiovodafone==0.5.4 # homeassistant.components.waqi aiowaqi==3.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acc7b389699..7871af2ffb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ aiounifi==69 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.4.3 +aiovodafone==0.5.4 # homeassistant.components.waqi aiowaqi==3.0.1 From 1b8c91dcb711a77a0340cabb8631af11f8be9f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Sun, 21 Jan 2024 22:20:07 +0100 Subject: [PATCH 120/133] Bump airthings-ble to 0.6.0 (#108612) --- homeassistant/components/airthings_ble/__init__.py | 3 ++- homeassistant/components/airthings_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index 1d62442f14d..3a97813741b 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -40,10 +40,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Could not find Airthings device with address {address}" ) + airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric) + async def _async_update_method() -> AirthingsDevice: """Get data from Airthings BLE.""" ble_device = bluetooth.async_ble_device_from_address(hass, address) - airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric) try: data = await airthings.update_device(ble_device) # type: ignore[arg-type] diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index cb7114ff8ff..03b42410d66 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "iot_class": "local_polling", - "requirements": ["airthings-ble==0.5.6-2"] + "requirements": ["airthings-ble==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3d253b3a953..99bde907caa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.6-2 +airthings-ble==0.6.0 # homeassistant.components.airthings airthings-cloud==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7871af2ffb2..3262ea26b88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,7 +377,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.5.6-2 +airthings-ble==0.6.0 # homeassistant.components.airthings airthings-cloud==0.1.0 From 58c96ff79643e4fdd7d74234d60ef886c06de2fb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 23 Jan 2024 23:36:44 +0100 Subject: [PATCH 121/133] Fix alexa fails reporting the state in specific cases (#108743) * Fix alexa fails reporting the state in specific cases * More cases --- .../components/alexa/capabilities.py | 32 +++++++++++-------- homeassistant/components/alexa/entities.py | 5 +-- homeassistant/components/alexa/handlers.py | 8 ++--- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index ab3bd8591fd..d30f3f7376d 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -860,8 +860,8 @@ class AlexaInputController(AlexaCapability): def inputs(self) -> list[dict[str, str]] | None: """Return the list of valid supported inputs.""" - source_list: list[Any] = self.entity.attributes.get( - media_player.ATTR_INPUT_SOURCE_LIST, [] + source_list: list[Any] = ( + self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or [] ) return AlexaInputController.get_valid_inputs(source_list) @@ -1196,7 +1196,7 @@ class AlexaThermostatController(AlexaCapability): return None supported_modes: list[str] = [] - hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES, []) + hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) or [] for mode in hvac_modes: if thermostat_mode := API_THERMOSTAT_MODES.get(mode): supported_modes.append(thermostat_mode) @@ -1422,18 +1422,22 @@ class AlexaModeController(AlexaCapability): # Humidifier mode if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": - mode = self.entity.attributes.get(humidifier.ATTR_MODE, None) - if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []): + mode = self.entity.attributes.get(humidifier.ATTR_MODE) + modes: list[str] = ( + self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES) or [] + ) + if mode in modes: return f"{humidifier.ATTR_MODE}.{mode}" # Water heater operation mode if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}": operation_mode = self.entity.attributes.get( - water_heater.ATTR_OPERATION_MODE, None + water_heater.ATTR_OPERATION_MODE ) - if operation_mode in self.entity.attributes.get( - water_heater.ATTR_OPERATION_LIST, [] - ): + operation_modes: list[str] = ( + self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST) or [] + ) + if operation_mode in operation_modes: return f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}" # Cover Position @@ -1492,7 +1496,7 @@ class AlexaModeController(AlexaCapability): self._resource = AlexaModeResource( [AlexaGlobalCatalog.SETTING_PRESET], False ) - preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES, []) + preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES) or [] for preset_mode in preset_modes: self._resource.add_mode( f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode] @@ -1508,7 +1512,7 @@ class AlexaModeController(AlexaCapability): # Humidifier modes if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False) - modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []) + modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES) or [] for mode in modes: self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode]) # Humidifiers or Fans with a single mode completely break Alexa discovery, @@ -1522,8 +1526,8 @@ class AlexaModeController(AlexaCapability): # Water heater operation modes if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}": self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False) - operation_modes = self.entity.attributes.get( - water_heater.ATTR_OPERATION_LIST, [] + operation_modes = ( + self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST) or [] ) for operation_mode in operation_modes: self._resource.add_mode( @@ -2368,7 +2372,7 @@ class AlexaEqualizerController(AlexaCapability): """Return the sound modes supported in the configurations object.""" configurations = None supported_sound_modes = self.get_valid_inputs( - self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST, []) + self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or [] ) if supported_sound_modes: configurations = {"modes": {"supported": supported_sound_modes}} diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index d0e265b8454..70679f8dafb 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -478,7 +478,7 @@ class ClimateCapabilities(AlexaEntity): if ( self.entity.domain == climate.DOMAIN and climate.HVACMode.OFF - in self.entity.attributes.get(climate.ATTR_HVAC_MODES, []) + in (self.entity.attributes.get(climate.ATTR_HVAC_MODES) or []) or self.entity.domain == water_heater.DOMAIN and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF) ): @@ -742,7 +742,8 @@ class MediaPlayerCapabilities(AlexaEntity): and domain != "denonavr" ): inputs = AlexaEqualizerController.get_valid_inputs( - self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST, []) + self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) + or [] ) if len(inputs) > 0: yield AlexaEqualizerController(self.entity) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 68702bc0533..463693f7da6 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -570,7 +570,7 @@ async def async_api_select_input( # Attempt to map the ALL UPPERCASE payload name to a source. # Strips trailing 1 to match single input devices. - source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, []) + source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or [] for source in source_list: formatted_source = ( source.lower().replace("-", "").replace("_", "").replace(" ", "") @@ -987,7 +987,7 @@ async def async_api_set_thermostat_mode( ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None) if ha_preset: - presets = entity.attributes.get(climate.ATTR_PRESET_MODES, []) + presets = entity.attributes.get(climate.ATTR_PRESET_MODES) or [] if ha_preset not in presets: msg = f"The requested thermostat mode {ha_preset} is not supported" @@ -997,7 +997,7 @@ async def async_api_set_thermostat_mode( data[climate.ATTR_PRESET_MODE] = ha_preset elif mode == "CUSTOM": - operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, []) + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) or [] custom_mode = directive.payload["thermostatMode"]["customName"] custom_mode = next( (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), @@ -1013,7 +1013,7 @@ async def async_api_set_thermostat_mode( data[climate.ATTR_HVAC_MODE] = custom_mode else: - operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, []) + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) or [] ha_modes: dict[str, str] = { k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode } From eeafb61b8f05c83890ed690a8cc45dfc829f0118 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 23 Jan 2024 21:04:59 -0500 Subject: [PATCH 122/133] Reduce log level of ZHA endpoint handler init (#108749) * Reduce the log level of endpoint handler init failure to debug * Reduce log level in unit test --- homeassistant/components/zha/core/endpoint.py | 6 +++--- tests/components/zha/test_cluster_handlers.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index 4dbfccf6f25..eb91ec96c59 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -199,11 +199,11 @@ class Endpoint: results = await gather(*tasks, return_exceptions=True) for cluster_handler, outcome in zip(cluster_handlers, results): if isinstance(outcome, Exception): - cluster_handler.warning( + cluster_handler.debug( "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome ) - continue - cluster_handler.debug("'%s' stage succeeded", func_name) + else: + cluster_handler.debug("'%s' stage succeeded", func_name) def async_new_entity( self, diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index 39f201e668e..46efe306b91 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -591,8 +591,8 @@ async def test_ep_cluster_handlers_configure(cluster_handler) -> None: assert ch.async_configure.call_count == 1 assert ch.async_configure.await_count == 1 - assert ch_3.warning.call_count == 2 - assert ch_5.warning.call_count == 2 + assert ch_3.debug.call_count == 2 + assert ch_5.debug.call_count == 2 async def test_poll_control_configure(poll_control_ch) -> None: From 12126ebfdafb659d3d980e4b577f2bd6eabacd23 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Jan 2024 19:47:07 +0100 Subject: [PATCH 123/133] Fix google_assistant climate modes might be None (#108793) --- homeassistant/components/google_assistant/trait.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 9b8a95f0b4a..189d1354e26 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1152,12 +1152,12 @@ class TemperatureSettingTrait(_Trait): modes = [] attrs = self.state.attributes - for mode in attrs.get(climate.ATTR_HVAC_MODES, []): + for mode in attrs.get(climate.ATTR_HVAC_MODES) or []: google_mode = self.hvac_to_google.get(mode) if google_mode and google_mode not in modes: modes.append(google_mode) - for preset in attrs.get(climate.ATTR_PRESET_MODES, []): + for preset in attrs.get(climate.ATTR_PRESET_MODES) or []: google_mode = self.preset_to_google.get(preset) if google_mode and google_mode not in modes: modes.append(google_mode) @@ -2094,9 +2094,10 @@ class InputSelectorTrait(_Trait): def sync_attributes(self): """Return mode attributes for a sync request.""" attrs = self.state.attributes + sourcelist: list[str] = attrs.get(media_player.ATTR_INPUT_SOURCE_LIST) or [] inputs = [ {"key": source, "names": [{"name_synonym": [source], "lang": "en"}]} - for source in attrs.get(media_player.ATTR_INPUT_SOURCE_LIST, []) + for source in sourcelist ] payload = {"availableInputs": inputs, "orderedInputs": True} From d220cb5102351b38ec37edc1527d832c135c55ff Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Jan 2024 22:14:15 +0100 Subject: [PATCH 124/133] Fix unhandled exception on humidifier intent when available_modes is None (#108802) --- homeassistant/components/humidifier/intent.py | 2 +- tests/components/humidifier/test_intent.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index d949874cc67..103521aeb04 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -110,7 +110,7 @@ class SetModeHandler(intent.IntentHandler): intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") mode = slots["mode"]["value"] - if mode not in state.attributes.get(ATTR_AVAILABLE_MODES, []): + if mode not in (state.attributes.get(ATTR_AVAILABLE_MODES) or []): raise intent.IntentHandleError( f"Entity {state.name} does not support {mode} mode" ) diff --git a/tests/components/humidifier/test_intent.py b/tests/components/humidifier/test_intent.py index cbdd5c3da26..d8c9f199f57 100644 --- a/tests/components/humidifier/test_intent.py +++ b/tests/components/humidifier/test_intent.py @@ -188,7 +188,10 @@ async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None: assert len(mode_calls) == 0 -async def test_intent_set_unknown_mode(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("available_modes", (["home", "away"], None)) +async def test_intent_set_unknown_mode( + hass: HomeAssistant, available_modes: list[str] | None +) -> None: """Test the set mode intent for unsupported mode.""" hass.states.async_set( "humidifier.bedroom_humidifier", @@ -196,8 +199,8 @@ async def test_intent_set_unknown_mode(hass: HomeAssistant) -> None: { ATTR_HUMIDITY: 40, ATTR_SUPPORTED_FEATURES: 1, - ATTR_AVAILABLE_MODES: ["home", "away"], - ATTR_MODE: "home", + ATTR_AVAILABLE_MODES: available_modes, + ATTR_MODE: None, }, ) mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) From d697fd23b77a4e2f45cedb9df898f10ab98c52c3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 24 Jan 2024 22:15:00 +0100 Subject: [PATCH 125/133] Fix processing supported color modes for emulated_hue (#108803) --- homeassistant/components/emulated_hue/hue_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 0730eced60c..94ac97b6b36 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -386,7 +386,7 @@ class HueOneLightChangeView(HomeAssistantView): # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: - color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) or [] # Parse the request parsed: dict[str, Any] = { @@ -765,7 +765,7 @@ def _entity_unique_id(entity_id: str) -> str: def state_to_json(config: Config, state: State) -> dict[str, Any]: """Convert an entity to its Hue bridge JSON representation.""" - color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) or [] unique_id = _entity_unique_id(state.entity_id) state_dict = get_entity_state_dict(config, state) From b66339dbfe49e7d79b9989821dbe5ced45c35339 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 25 Jan 2024 14:45:11 +0100 Subject: [PATCH 126/133] Reduce log level for creating ZHA cluster handler (#108809) --- homeassistant/components/zha/core/endpoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index eb91ec96c59..490a4e05ea2 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -132,7 +132,7 @@ class Endpoint: if not cluster_handler_class.matches(cluster, self): cluster_handler_class = ClusterHandler - _LOGGER.info( + _LOGGER.debug( "Creating cluster handler for cluster id: %s class: %s", cluster_id, cluster_handler_class, From 0532c4343a34906d197972368bef328895e480f5 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Sat, 27 Jan 2024 02:34:29 -0500 Subject: [PATCH 127/133] Fix stalls in config flow of APCUPSD (#108931) Fix deadlock in config flow of APCUPSD --- homeassistant/components/apcupsd/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/apcupsd/config_flow.py b/homeassistant/components/apcupsd/config_flow.py index 57002d7a2b2..99c78fd5d33 100644 --- a/homeassistant/components/apcupsd/config_flow.py +++ b/homeassistant/components/apcupsd/config_flow.py @@ -52,9 +52,8 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): # Test the connection to the host and get the current status for serial number. coordinator = APCUPSdCoordinator(self.hass, host, port) - await coordinator.async_request_refresh() - await self.hass.async_block_till_done() + if isinstance(coordinator.last_exception, (UpdateFailed, asyncio.TimeoutError)): errors = {"base": "cannot_connect"} return self.async_show_form( From 5903ec084b29f378a61957b033eccef7b6136a1b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 28 Jan 2024 19:18:22 +0100 Subject: [PATCH 128/133] Add strings to Sensirion BLE (#109001) --- .../components/sensirion_ble/strings.json | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 homeassistant/components/sensirion_ble/strings.json diff --git a/homeassistant/components/sensirion_ble/strings.json b/homeassistant/components/sensirion_ble/strings.json new file mode 100644 index 00000000000..d1d544c2381 --- /dev/null +++ b/homeassistant/components/sensirion_ble/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:common::config_flow::data::device%]" + } + }, + "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%]" + } + } +} From 923259a8c63188cedf31a77b9479496155e95a11 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Sun, 28 Jan 2024 19:07:14 +0100 Subject: [PATCH 129/133] Fix entity naming for heatpump heatings in ViCare (#109013) Update strings.json --- homeassistant/components/vicare/strings.json | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 6c08215a9c1..87b5bb6cc14 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -141,52 +141,52 @@ "name": "Heating gas consumption this year" }, "gas_summary_consumption_heating_currentday": { - "name": "Heating gas consumption current day" + "name": "Heating gas consumption today" }, "gas_summary_consumption_heating_currentmonth": { - "name": "Heating gas consumption current month" + "name": "Heating gas consumption this month" }, "gas_summary_consumption_heating_currentyear": { - "name": "Heating gas consumption current year" + "name": "Heating gas consumption this year" }, "gas_summary_consumption_heating_lastsevendays": { "name": "Heating gas consumption last seven days" }, "hotwater_gas_summary_consumption_heating_currentday": { - "name": "DHW gas consumption current day" + "name": "DHW gas consumption today" }, "hotwater_gas_summary_consumption_heating_currentmonth": { - "name": "DHW gas consumption current month" + "name": "DHW gas consumption this month" }, "hotwater_gas_summary_consumption_heating_currentyear": { - "name": "DHW gas consumption current year" + "name": "DHW gas consumption this year" }, "hotwater_gas_summary_consumption_heating_lastsevendays": { "name": "DHW gas consumption last seven days" }, "energy_summary_consumption_heating_currentday": { - "name": "Energy consumption of gas heating current day" + "name": "Heating energy consumption today" }, "energy_summary_consumption_heating_currentmonth": { - "name": "Energy consumption of gas heating current month" + "name": "Heating energy consumption this month" }, "energy_summary_consumption_heating_currentyear": { - "name": "Energy consumption of gas heating current year" + "name": "Heating energy consumption this year" }, "energy_summary_consumption_heating_lastsevendays": { - "name": "Energy consumption of gas heating last seven days" + "name": "Heating energy consumption last seven days" }, "energy_dhw_summary_consumption_heating_currentday": { - "name": "Energy consumption of hot water gas heating current day" + "name": "DHW energy consumption today" }, "energy_dhw_summary_consumption_heating_currentmonth": { - "name": "Energy consumption of hot water gas heating current month" + "name": "DHW energy consumption this month" }, "energy_dhw_summary_consumption_heating_currentyear": { - "name": "Energy consumption of hot water gas heating current year" + "name": "DHW energy consumption this year" }, "energy_summary_dhw_consumption_heating_lastsevendays": { - "name": "Energy consumption of hot water gas heating last seven days" + "name": "DHW energy consumption last seven days" }, "power_production_current": { "name": "Power production current" From 8429a7979661098498df71d34076e6c71a50579b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jan 2024 12:15:18 -1000 Subject: [PATCH 130/133] Bump aiohttp to 3.9.3 (#109025) Co-authored-by: Joost Lekkerkerker --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/websocket_api/test_auth.py | 2 +- tests/components/websocket_api/test_http.py | 6 +++--- tests/components/websocket_api/test_init.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 821b6fdf141..9d030118dae 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,7 +3,7 @@ aiodiscover==1.6.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.1.3 -aiohttp==3.9.1 +aiohttp==3.9.3 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.38.1 diff --git a/pyproject.toml b/pyproject.toml index f678d14d214..82162ff02d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.11.0" dependencies = [ - "aiohttp==3.9.1", + "aiohttp==3.9.3", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.1.3", diff --git a/requirements.txt b/requirements.txt index e1878a33584..eaf5b8a9e22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.9.1 +aiohttp==3.9.3 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.1.3 diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index d5ff879de78..dd18342abec 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -220,7 +220,7 @@ async def test_auth_close_after_revoke( await hass.auth.async_remove_refresh_token(refresh_token) msg = await websocket_client.receive() - assert msg.type == aiohttp.WSMsgType.CLOSE + assert msg.type == aiohttp.WSMsgType.CLOSED assert websocket_client.closed diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index e69b5629b63..f6723f0a592 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -42,7 +42,7 @@ async def test_pending_msg_overflow( for idx in range(10): await websocket_client.send_json({"id": idx + 1, "type": "ping"}) msg = await websocket_client.receive() - assert msg.type == WSMsgType.close + assert msg.type == WSMsgType.CLOSED async def test_cleanup_on_cancellation( @@ -248,7 +248,7 @@ async def test_pending_msg_peak( ) msg = await websocket_client.receive() - assert msg.type == WSMsgType.close + assert msg.type == WSMsgType.CLOSED assert "Client unable to keep up with pending messages" in caplog.text assert "Stayed over 5 for 5 seconds" in caplog.text assert "overload" in caplog.text @@ -296,7 +296,7 @@ async def test_pending_msg_peak_recovery( msg = await websocket_client.receive() assert msg.type == WSMsgType.TEXT msg = await websocket_client.receive() - assert msg.type == WSMsgType.close + assert msg.type == WSMsgType.CLOSED assert "Client unable to keep up with pending messages" not in caplog.text diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 468b35fef51..c4c83925311 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -40,7 +40,7 @@ async def test_quiting_hass(hass: HomeAssistant, websocket_client) -> None: msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSE + assert msg.type == WSMsgType.CLOSED async def test_unknown_command(websocket_client) -> None: From b7410fecb82cfab7c3f2c5e106fdf251be98bbb8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:59:56 -0500 Subject: [PATCH 131/133] Bump ZHA dependency zigpy to 0.60.7 (#109082) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index de429b299c0..024fea9227a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -26,7 +26,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.109", "zigpy-deconz==0.22.4", - "zigpy==0.60.6", + "zigpy==0.60.7", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/requirements_all.txt b/requirements_all.txt index 99bde907caa..22d8e110fe7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2888,7 +2888,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.6 +zigpy==0.60.7 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3262ea26b88..cb89514bf1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2187,7 +2187,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.60.6 +zigpy==0.60.7 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 From 1c6c925a2b39d0942eaf3c34fdbca535854daccb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Jan 2024 10:07:23 +0100 Subject: [PATCH 132/133] Add missing abort message for Spotify (#109102) * Shield for unregistered Spotify users * Shield for unregistered Spotify users --- homeassistant/components/spotify/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 02077cbdb43..e58d2098bde 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -17,7 +17,8 @@ "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", - "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" + "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", + "connection_error": "Could not fetch account information. Is the user registered in the Spotify Developer Dashboard?" }, "create_entry": { "default": "Successfully authenticated with Spotify." From 1dff998a25de8d3c8d8f80ecb493aa277fd0edca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Jan 2024 15:22:32 +0100 Subject: [PATCH 133/133] Bump version to 2024.1.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 f9d250c6732..0f483da47d8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 1 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 82162ff02d6..f6d936fb637 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.1.5" +version = "2024.1.6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"